From 32fc43b57ed55c37f3e9f19debdf167ac0346c94 Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Wed, 8 Oct 2025 13:50:49 -0600 Subject: [PATCH 01/21] veriforce demo --- .nvmrc | 2 +- TABLEAU_CONFIG.md | 38 + dev.sh | 16 + package.json | 5 +- public/img/demos/Veriforce_contractor_2.svg | 713 ++++++++++++++++++ .../themes/veriforce/veriforce-logo-big.png | Bin 0 -> 28909 bytes .../img/themes/veriforce/veriforce-logo.jpeg | Bin 0 -> 23943 bytes .../img/themes/veriforce/veriforce-logo.png | 15 + src/app/api/tableau/views/route.js | 153 ++++ src/app/api/tableau/workbooks/route.js | 211 ++++++ src/app/api/test/session/route.js | 38 + src/app/api/user/route.js | 3 +- src/app/demo/cumulus/Home.jsx | 21 +- .../clientportfolio/ClientPortfolio.jsx | 73 +- src/app/demo/makana/Home.jsx | 21 +- src/app/demo/makana/members/Orders.jsx | 42 +- src/app/demo/makana/mother/Products.jsx | 48 +- src/app/demo/superstore/Home.jsx | 21 +- src/app/demo/superstore/orders/Orders.jsx | 42 +- src/app/demo/superstore/products/Products.jsx | 48 +- src/app/demo/veriforce/Home.jsx | 147 ++++ src/app/demo/veriforce/README.md | 190 +++++ .../demo/veriforce/agent/AgentDashboard.jsx | 267 +++++++ src/app/demo/veriforce/agent/page.jsx | 22 + .../demo/veriforce/alerts/AlertsDashboard.jsx | 291 +++++++ src/app/demo/veriforce/alerts/page.jsx | 22 + src/app/demo/veriforce/auth/page.jsx | 13 + src/app/demo/veriforce/config.js | 98 +++ src/app/demo/veriforce/layout.jsx | 27 + .../management/ManagementDashboard.jsx | 203 +++++ src/app/demo/veriforce/management/page.jsx | 22 + src/app/demo/veriforce/page.jsx | 23 + .../procurement/ProcurementDashboard.jsx | 207 +++++ src/app/demo/veriforce/procurement/page.jsx | 22 + .../veriforce/reports/ReportsDashboard.jsx | 341 +++++++++ src/app/demo/veriforce/reports/page.jsx | 22 + .../demo/veriforce/safety/SafetyDashboard.jsx | 286 +++++++ src/app/demo/veriforce/safety/page.jsx | 22 + .../veriforce/settings/SettingsDashboard.jsx | 291 +++++++ src/app/demo/veriforce/settings/page.jsx | 22 + src/app/demo/veriforce/test/page.jsx | 82 ++ src/components/Demo/components/Auth/Auth.jsx | 4 +- src/components/Demo/components/Demo.jsx | 4 +- .../components/Navigation/Breadcrumbs.jsx | 18 +- .../Demo/components/Navigation/Navigation.jsx | 1 + .../components/Navigation/NavigationMd.jsx | 5 +- .../components/Navigation/NavigationSm.jsx | 7 +- .../Demo/components/Navigation/UserMenu.jsx | 44 +- src/components/Explore/Explore.jsx | 9 +- src/components/Gallery/galleryItems.js | 12 +- src/components/Hero/HeroDemo/UserMenu.jsx | 4 +- src/components/TableauEmbed/TableauAuth.jsx | 30 +- src/components/TableauEmbed/TableauEmbed.jsx | 4 +- .../TableauToolbar/TableauToolbar.jsx | 76 +- src/components/TableauEmbed/TableauViz.jsx | 142 +--- .../DynamicDashboardViewer.jsx | 205 +++++ .../TableauNavigation/TableauNavigation.jsx | 345 +++++++++ src/components/TableauNavigation/index.js | 1 + src/components/index.js | 3 + src/components/ui/Breadcrumb.jsx | 2 +- src/global.css | 35 + src/hooks/tableauHooks.js | 2 + src/hooks/useTableauDashboards.js | 183 +++++ src/hooks/useTableauSession.ts | 5 +- src/models/Users/userStore.ts | 37 + src/services/tableauApi.js | 270 +++++++ test_system.sh | 72 ++ 67 files changed, 5196 insertions(+), 454 deletions(-) create mode 100644 TABLEAU_CONFIG.md create mode 100755 dev.sh create mode 100644 public/img/demos/Veriforce_contractor_2.svg create mode 100644 public/img/themes/veriforce/veriforce-logo-big.png create mode 100644 public/img/themes/veriforce/veriforce-logo.jpeg create mode 100644 public/img/themes/veriforce/veriforce-logo.png create mode 100644 src/app/api/tableau/views/route.js create mode 100644 src/app/api/tableau/workbooks/route.js create mode 100644 src/app/api/test/session/route.js create mode 100644 src/app/demo/veriforce/Home.jsx create mode 100644 src/app/demo/veriforce/README.md create mode 100644 src/app/demo/veriforce/agent/AgentDashboard.jsx create mode 100644 src/app/demo/veriforce/agent/page.jsx create mode 100644 src/app/demo/veriforce/alerts/AlertsDashboard.jsx create mode 100644 src/app/demo/veriforce/alerts/page.jsx create mode 100644 src/app/demo/veriforce/auth/page.jsx create mode 100644 src/app/demo/veriforce/config.js create mode 100644 src/app/demo/veriforce/layout.jsx create mode 100644 src/app/demo/veriforce/management/ManagementDashboard.jsx create mode 100644 src/app/demo/veriforce/management/page.jsx create mode 100644 src/app/demo/veriforce/page.jsx create mode 100644 src/app/demo/veriforce/procurement/ProcurementDashboard.jsx create mode 100644 src/app/demo/veriforce/procurement/page.jsx create mode 100644 src/app/demo/veriforce/reports/ReportsDashboard.jsx create mode 100644 src/app/demo/veriforce/reports/page.jsx create mode 100644 src/app/demo/veriforce/safety/SafetyDashboard.jsx create mode 100644 src/app/demo/veriforce/safety/page.jsx create mode 100644 src/app/demo/veriforce/settings/SettingsDashboard.jsx create mode 100644 src/app/demo/veriforce/settings/page.jsx create mode 100644 src/app/demo/veriforce/test/page.jsx create mode 100644 src/components/TableauNavigation/DynamicDashboardViewer.jsx create mode 100644 src/components/TableauNavigation/TableauNavigation.jsx create mode 100644 src/components/TableauNavigation/index.js create mode 100644 src/hooks/tableauHooks.js create mode 100644 src/hooks/useTableauDashboards.js create mode 100644 src/services/tableauApi.js create mode 100755 test_system.sh diff --git a/.nvmrc b/.nvmrc index deed13c0..603606bc 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/jod +18.17.0 diff --git a/TABLEAU_CONFIG.md b/TABLEAU_CONFIG.md new file mode 100644 index 00000000..e59e6da6 --- /dev/null +++ b/TABLEAU_CONFIG.md @@ -0,0 +1,38 @@ +# Tableau Cloud Configuration + +## Environment Variables + +Create a `.env.local` file in the root directory with the following variables: + +```bash +# Tableau Cloud Configuration +NEXT_PUBLIC_TABLEAU_BASE_URL=https://prod-useast-b.online.tableau.com +NEXT_PUBLIC_TABLEAU_SITE_ID=embeddingplaybook +``` + +## Current Configuration + +The system is currently configured to use: +- **Base URL**: `https://prod-useast-b.online.tableau.com` +- **Site ID**: `embeddingplaybook` + +## Authentication + +The navigation system uses Tableau's REST API for authentication. Users will need to provide their Tableau Cloud credentials to access their dashboards. + +## Features + +- ✅ Real Tableau Cloud authentication +- ✅ Dynamic dashboard loading +- ✅ Folder-based organization +- ✅ Search functionality +- ✅ Responsive design +- ✅ Dark mode optimized + +## Testing + +To test the system: +1. Navigate to the Safety Dashboard page +2. Click "Sign In to Tableau" in the navigation panel +3. Enter your Tableau Cloud credentials +4. Browse and select dashboards from your site diff --git a/dev.sh b/dev.sh new file mode 100755 index 00000000..cad869ff --- /dev/null +++ b/dev.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Auto-switch to correct Node.js version and start dev server +echo "🚀 Starting Tableau Embedding Playbook Development Server" +echo "========================================================" + +# Check if nvm is available +if command -v nvm &> /dev/null; then + echo "📦 Switching to Node.js version specified in .nvmrc..." + nvm use +else + echo "⚠️ nvm not found. Please ensure you're using Node.js 18.17.0+" +fi + +echo "🔧 Starting development server on port 3001..." +npm run dev diff --git a/package.json b/package.json index f1f02069..38e0fdc7 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,14 @@ "version": "0.0.1", "description": "Tableau Embedded Playbook", "scripts": { - "dev": "next lint && next dev", + "dev": "next dev --port 3000", + "dev:fast": "next dev --port 3000 --turbo", + "dev:lint": "next lint && next dev --port 3000", "build": "next build", "export": "next export", "start": "next start", "lint": "next lint", + "lint:fix": "next lint --fix", "info": "next info", "demo": "next lint && next build && next start", "save": "git add pages public && git commit -m 'saving content (/public & /pages)'", diff --git a/public/img/demos/Veriforce_contractor_2.svg b/public/img/demos/Veriforce_contractor_2.svg new file mode 100644 index 00000000..2a6e88cd --- /dev/null +++ b/public/img/demos/Veriforce_contractor_2.svg @@ -0,0 +1,713 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KLUv/QBY9EEDGlsLqyiQRESYDwCAjVlRaVmzsFxZRSCd0ZlbiHru+FOVGZffnkkEAEAAAAAQNArH +CtQK8Wy/YJwRQCCPoeQ7z+FbNcutlACex5x4RsHyDQfwcA6r4Dx+5SccgI5VNb2IVTU9vwQQELGq +vlQsIRDOZVWr5QLijhYHuL33c+wl49od27Ctnai4zmOpJkkRmsBcp3B5hj/ZVpW1jbtw+KWCbTL0 +WFoeq12pmnbhmkq+WdqAFB3fsI3T8J1rpySNZ5QW5/Bc47JK87A8w5qc0zDJ0rwmIM5v7WvmF/9Y +z3VKU91wjcpvWDslaeyq7xzXYlqe81jdV3ElJbPxjII7rOSOc1qCxy/4MsOpupKq2RyOzDEHoQnA +952CYziOJF0DQL7ryM9xmWXRcgCx/DFFAMiceY7XMF/FlXAcyVVc8eZpjrlMe2J8tco5eYslq+JN +bA/o/DvnzHPMlaQuRC6nYBvTzTE2q6XpNV+S5Y/j+81XcUX0n/yxep3neJ1ZdBqnYDdfxZVRbLHk +OHKKiFaukkECatWu79X0fjigluhfaHqEaJa+LAfEFDyPcTyNZxSc37UtoAXzlMwiNIE4gJzDc7wZ +F1q3685Zsmi5hcs0/NHG0f+SNF91nteqF0XTmy/4ouUxHt8Zl0XLd86JVXVKpj2glii9YrIqtowQ +eczOY5geanQt5zAA5I3WwmOLdkpShNZH32LJG7McPx+KJh3nAtp5jtd8FVdMXJbTcp2Z4XgDauNH ++tDUwn+ap/j58SxVdJ2KOTl+y66AWEVL8JyWb/uOA9RaHOCO8fi2cbqWWfGtCYhn+1WnNFueU3Nd +q6CWouEZRmkBFnec4/Ct3fFK693HOZxzBuTyLKNkXP7cdT27JpV8v56WsGN2ndFiFhxj0XBF07yX +pAgtpyWg1gfcMF3LeUpmaKCJy+AcZk8V3ZrEL/4clytqJktlsgwAIVQmzvN4hjMquhXQVjELapkA +mKlCmtd8tglpPVNpvTTSLFr3WgCa1wq0rk20ng0IaV3rWRraaD2DQxitx7P9kjlg5mnxC76AWqKU +zzF4FbMjRMbQ6ABqmgJmeDwPyDn5narollUzXPU9w1UTl3FUE6f17rU+aqTmta/18Zve9/GXo+j7 ++XVN9F88/e8leYrd/P4kvQ9D8fRnSX5dOzt5nr38o//lKZZfz0bZrZo4Tcx69/33sIt97GQvu9nP +nna1r53tbXd71r3334de9KMnfelNf/rUq371rG+96/v3///wi3/85C+/+c+ffvWvn/3td38PffjD +MBTDMSTDMszM8AzTUA3XkA3b0A276MUvhqIojiIplqIpnmIqquIqsmIrumIf/fjHcBTHcSTHcjTH +c0xHdVxHdmxHd8w66clPhqRIjiRJlqRJnmRKquRKsmRLumQvffnLsBTLsSTLsjTLs0xLtVxLtmxL +t+ymN78ZmqI5mqRZmplpnmZqquZqsmZrumY//fnP8BTP8STP8jTP80xP9VxP9mxP9+ypT38apmI6 +pmRapmZ6pmmqpmvKpm3qpllXvfrVUBXVUSXVUjXVU01VVV1VVm1VV+2rX/8aruI6ruRaruZ6rumq +ruvKru3qrp317GdDVmRHlmRLNjPZk01ZlV1Zlm1Zl+2tb38btmI7tmRbtmZ7tmmrtmvLtm3rtt31 +7ndDV3RHl3RL13RPN3VVd3VZt3Vdl2zHVmzD1re9ddmWZdmVVdmUPVmTJdmRzUQ2ZD3b2XZl13VV +13Q913IlV3H9a19blVVV9VRLdVRD1atuyqZqeqZkKqY/7Wl7rud5lud4hmc/W3M1s9MszdEMzW62 +5VqeZVmOZVj60iVZUiVPsiRHMiQ96abiGLLiSopjG2ZsqJLh6P7W/OVvPZulZSfF8PvMTM3v3bNl +13L0Z2/LrC1D8rOiH13RHcVQzL7Yfvb8ps8+u6ZZmppkT90zc9lzPddTDU/TNE0zIz1bZqM7tiM7 +ruN6juZYhmMrsl/MUjIU/fpVU/zh/22mjm1nU1Lsoe8dzdyWXdVMTU+zVEmVHMXwe7WrXc266qZu +2qZtyqZsyqZruqZqqqZqmqZpmqZnaqZmWqZlWqZkSqZjOqZiKqZhGqY//alPfdpT92zP9mxP9szY +cz3VMz3T8zzN0zzLkzzHczzFMzz/+U9/9tM1XbM1WXM1VTM1U9M0S7M0SXM0R1M0Q/Ob3/RmN92y +LTO2XEu1TMuzNMuyJMuxFMuw/KUve9lLl2xJllxJlUzJkzTJkiRJkQzJT3qyk+3IjuqYjuZojuQ4 +juIYjtkf++iKrciKq6iKqWiKpUiKoyiKoejFLrphG7LhGqrhGZphGZLhGIphGP6wh+5vP/vVn/70 +n7/85B9/Jv7w+99//65n/epXr/rUn970pSe96EMf+u+9797tbV+72tN+9rObvexkH/vYxR72nzWy +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 +uR2LaoLI7ALwnBgbrD2dBQ== + + + diff --git a/public/img/themes/veriforce/veriforce-logo-big.png b/public/img/themes/veriforce/veriforce-logo-big.png new file mode 100644 index 0000000000000000000000000000000000000000..7b1abd4db847c3b1c20c6dcae2d517a2b69e8d38 GIT binary patch literal 28909 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 AL;wH) literal 0 HcmV?d00001 diff --git a/public/img/themes/veriforce/veriforce-logo.jpeg b/public/img/themes/veriforce/veriforce-logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..19ae43a8d866282f4d1a1a78c12301080061240e GIT binary patch literal 23943 zcmbq*1yo(j(&oVvT!Xv2ySux)ySqDq;KAL49^9Sa?iMt`Ex7xf-23kT{x@r8&6=4$ zuy;}2^;LE4wo|*C_aE=S0LU^D(h>kLFaQ7y^Z~qYgXc?&iW(}ZC`w4niT{-W{?*RZ z-W8Gw0I+v(b5W5LA=1*;A%fWjK!POD0BnGd#-^@L!b(bVfd6f}Tl#|z0L;_>!TR48 z`(J0knVGwqf(-ls%H}e4a&ZIELLgef)6MA*{S`!`n_3y0f#`1_n$87eAc&s%!#DYx z-u**c{!Kspp{M)Cl7Oa2pKvjm90RW(O-ruij z005XQ0N^#^{r$P{{r$BF0D$-b0DMpWx4dIA$em{({?~u=$Z`Mxq)-5$x%=NdlVSj% z1>|F_O($a)d~njM4uD!S~9YJzv&7@KgKe zRI}_W=2)7q=5=MJ{gzPsWAOsof0jlh*og@dxz}pDV)5WBS{`-7q1!9$9zweJD!zvI zKf#T2eZ{yY=w`F)AO8F{A9{Ux9z9o5+jBfbay)phze|lvxO(9Ew-I2&)48`Jy5u&=WnGD+z61$&H9}gY zwL_K~6*}2UJbC+6th$0Z4ssi;*Xdd$3kQX#`WFUozRfy^GCuyHk8{qZj;i+E);?kR z|6n2!Iuh~~_gnqWyn(}v4M@Wg4EGic?17ysSiIqGLr7Hs22zTg4ey^A)~lb+--?D4$n{O~{b zOLgyAqo(tmeF^I1cN=;elKbBQLc0dekDVsbibn%(vH1Bu|I&nvfxtcpFWaD8CK|h9 zGa|^)w7Tx!&hLY{K?1>HV2clzmB*Gjjh|$heV=3!v@uk1u_Gd?_t%bQf=DC_;CvJR8~FKhq-hHU(%?am9Xy9b_5N_!sEq5g)FM+l`ln5k?ZY#JukvyDJAl*vS}M*UV7hWL=pS=I$b=-j z)VckmvRBp9!L+!%%ciXes9l_@S>&FHDF!bulKZ1RT{2sX)LC@NNcNOAHJai+%l!U^ zrFlh|$A1mze_01JUCaIZ>#-W%`05^`0zIJ!OYA3`SjZ;?LB^Qe&Sdizp+tBKRkQKK zg-+=mysJrT;FIBAyg=%Yql`2j2CY&UgLi;%t8dpLjL_e;MU>15j5+78)6|@%S$bBt zzRv8b=4O3fbQFw}R|w70Q(~(=Fw<o*b9b=|fiGR4W`jdR-O6n5p6g%_qsVgw8J$0;Cx zzh|k>hMh|Onj1q!I|}M~<4yn6_kY0wP_FOaLP7kSQ}aRmE~#GNi{`I% ziPCNT$mB;$#2)%eUU5d<#a#@Sn#Z~M7}Sto%tB)PDAuC_z=Vfz75z9%vV|;-#o8!g zZF^rau%KKlfEMnXo5z8&HLuu&3~n6bh&|av0LHk~qN;_r2l${I3J&(jn;dw!N#M7t zvhQt{OM-IZ1ux`S2X#3*-uRSgyZ*4d!Kj~0e`S43Fb(?NWN)*gZvh2+NH?rQGBc#~ zae`z-;FI@Pm#2J6jae&SW=`&NzLDQDdY;;$Rc>AQh3NJczz5u=7N9m5S$!z&3vK?` zMRWV^@hZIrvewI#^58rgDq1p!u4#xf4z{+3&3$%cYKpP0hDEcie{G-65&QkdJK#&l z1T^N{M;;1#1WTF#OY#J_fr0AY-f?>kFAD4m_dYL65)*jT@Wm#ed6T@f`reNdkiu?% zQQ0;{+KW`+n2SNMs64&y@@eW<;kr!cn}LC_JtRgTS|8Q81_sz;w(W8Z3G_l!xp^~! zg8E|m$y^JNo1~E?WBEZ>I7p^Fh*4~yzm2MC>KDg{CVO3)dDs<3Ni zMiKFbj4ktbzt(OIvL}9+2<)-lW@m$uFFt%#ty134ZTe95}!V9#nfxo$j(W~#*N!-c0!8;FooN2 z#%k|t@XVtz)7aD#sLGq(Ury{*8_eSoyIRRzO1sG0+D=(!5g+AcX0kuam2~iJ64MT0 zLpX2l)MuHOQXNn9N{TFIS4`u(LKHouG%7IY-;U3tFm1~0?>A&XUNoJh2_6zy(6ic- zgvL$sjSd^GzUGDpHmCTri67Z)=Ghft+bj{CPEz#Ej~zD?XlkoUUNTKtNl)ZEz0j@n z_AVhE&E5cE+1Rk$ve_oB^_8PkmtWgi+p(f9FC&z(4QxFX&h{+YhP$i zXk9u?THuq(-2;bVtF7mslE2X{(OWBWIQqGR z(33RqZBmyHu|o;|tkdqQ@+|`*qzR4>=sXk>vZK~u%HFJ$tXSCU>Wn-dDE@r7asx`$ zx1dC6rmVPoYt=gWKd&|xy(JLIKJy?-630Df%hgk}zXRrkTg}_frDn;}+8pYKE#3jY zGVsNSqhRvYZKjLk*X)DwyFFF^MeHZGmDwr|--91P?@|^dBPW)ZPXr#H@VKHBJ$hS6hfvuR9~swVa4!)B=UYYzvO$1GuAH z7$BJB-T`d7SJ=noD#^yc?ijWMH(VEQI?nVR@1L_00Q1x5cR-7Yt(Bf%s;m8&fEUCw zd^T;o?0i)ATePEYL+A6PM*uABuwZ-0Vxa$i0P?i`n&o4HY8pVZD4wd6_i)s~Xx}IV z1wM&(EE}s3LH|a$l;>G~t(1E#uCUUjot_@np2o1U*9h+L$<;8OJDubgE9BHQ zbPsWMAv-lR4gZlQo?zKY&j;h~g@a#YE&XfM)YE6J!#WmA@)TLSe7{b{0kpBhDXO_Q z;yrkSe`)hOsu{H``-ayCxWg4{V14zet57)82lonP#7i!%)rC5KE#9wNIC&xS=-J=6r4ncwL(XW5&tFi4o=);RMhJ#x_}6c{y!Z$jHn3@ysq`o{Gv???G{2Q`-_D6gqhSC<`$XXQ9t zg!;A$9oTUcn!D`Xx?7*)m~ed@Zmly{eL_~l-@UTE)bzWelHJYk;HJK!x=P#Lysbyr z*Lgy9-K~u-7Y;M|I$86a>HDXm?!>xDu6b3hENwa%q){`HZ>zI99o_lf8V@gUmyb1Q>Bv9vaZR`|iWYtLL|_LOjK@CDD= z0a^L_MIf3jdC1lyU6^{-Y9lZU7^OF7UlVqJ}}iV;;5duMseUfKur*|N}@*rfI)(T);vu zNEBiyRAv$;Az^1U7UKeRQZgeEmAb!HN}wf`AlN%VIccl@^GKggZnD{&T}x?F1BZSf zL09-OZn*{*A+yM?exrma-^aV2;%uM7-_e7s6|W*c`)a0;1JF3s;8e~nS1Ad(>>LW* z3jO3^e~q3Wj^;-6Au%`!O6$@dEBBOpYH(5a8@n;cY)Bq4yeWC}G-s9g1ddVSP1m)X zXjN^%eGThArSR|=_d$i{z(q4R4ZVje(`ggG6!@7epiq^1#A>=n7>bQshT1oI#{Ui= zEH=zW;iIslEJJHi%HdMfwfj8kE1wJp?q72txaZF`?(DvUa}u@&LJI%L!P)1uQTXUE zg12tI#Y*Db#K5VPNBAMze@t;faIR`vMgXloda-pS^b|D``Y{Cm3gMECR+Q56nqouQ zLj|E-G5t} zLD?(h@g1B$*%HI&L$yCkCXk{xa2tF{LQubjP<0ly4tN3><3MnAH zN;kwl&hrs($SmxYm1$((lg}Z0ig)CQe?3<49m4pI$Llb{7hHrVEm=KrnS4`7ahz24 zGlp#<<7(ylqYd+E;~upke1YPo^%(L|ausrB)MxH-SA}0SSCIiOGb5jfW?As?SR4-Tmz%`fI$YT6LbgA%6TcC-R z)wgNdtP2OI`PoiMN(nN$Jnk{k=2!JfCL{T84msRU9>;8bpGNU9lf(E(%%e5lkh#hv zU01_Mp$BxQ(pDctcMj+imeqZJQl*XZ3yVa8s(e(X+2iJ*qB4^qk`j%Sk?*|y>8InN z*@Im<-W?%|$WGVDu?A3Mfwz2A0h6KdDGw7;Gm(jAGkr+_!jtI2-J5=e>mkp89} z=n+#iN+9?nx1EJI)$58Q+^4egVn!>z2q(UoCFT5(%PnNaZSE(be22co;K&f)x6&~b zAmrec-!pD{Mv4xXw*@T6Nlfbex^C|oPD(SW+BgPwdnlS=djB;xSa^v+eq-ET`ohm2 z>n%eo1}7|?&*cJHoXmcOdA80pSAe4duA5D6EoVE4*&rywOgPQEah0XVJ>fHU!DF8l z-x!)!r#{_n?{@%7IsqrW|3NU$grA|S_>=@+>=zASTDbuYb(*;k8_Y}?V?N2l)|y0p zw)6MASYO3ru|VjSma1tK+ri7x^4H#1$(zCn%btdXwuSgYc9}ptGG0N3PQk?q zj(J7pdbj|<%iaBP1U zkzZii*%ud!!`JNR2Kt{TIMhKHbRY}_5C+>U-K6PhSklia{NZs3S+3^8^s)}CN2yHh z*Im^6A+O z8Sy|mneQ1lkge(a*xq2D<>UzQnFDDe&t@RHMNy)wlQBdE`d0AU60VM2JkLHY9t&Oc zp!_?4CLMkXV$03(_f-zfQLpHL1uZI*@qlI?GW`^bu~ub#6JAw9C7Q&@I+nh!l$n_W z(GsqSJq*vT?MO0OmzyAU*Zk@}&f-N*;XAUB5x^OJq)t9nDbZN*4TfCwt$;iD%3`zn-2XnPUf|u&91(*;iT3K_Z)WH3YF< z^37CWaZHr~!cdU!H+*+s=#~S5Cf=Y)^hlu8c#}E73U`v=N#9}d9XBqsrcQF-x`eIp zj=MV#24{T2STv9tymk^>RVz7#)C*XQtNdfR{y{gT3HSvPr<=u8a1nKOv&iScm(l0K zZcv|yIq$ee<-?Iyan!+qUaXrIj9q?C*;mO*r zmbNG&be))-WNVEB?{bDK0VeIX0YZ*G{UxV9%`IoBCdeF(8rfoecHo!RsBTJHO&zqi zJ>d3NcH>-f=LKu=N>}3>!OhxLVzVdD{4ps~CUL0rj|3=NgwroD6m1R40u&w5#thi#2NDy+|h% z&Lr@}VRPEhd=JM7)vCN^Av}BspnqCH*lc{(pd#1AN6fvM8v0dut=UgxbB)Yy2=Q|k z2p^?(3tWuKC8(Iv=7*=g8!Gzap6LDQ2kpl!yHBeBVU} z$99uXY{irNsV2_VdR$h;iOAM1LrN4UJS{>YMqt&ay76exvc{*MPagZ2J^!+U6ah9X zR^JBPY^Mmeo!TKlV&r+?9kAnAY$8D;aba^bwCHsXOgIbBSgJOM=UKd{6aBtVyC&+1 zzlq(<{%g?{KjYSV>v#2NYt3qna+9G#S0`Wj2fPlDUZY$1L6g|U2vGwQa1h4t(-8> z9@9MF!hGAOoe+V+-^4^~G*tJMZ*V}++D{=hpmP9LdYHz)ir>N5zXKL6h1EWpH~<`h zd@gV~CQ3iMHZKG4vo3YI_;$D3?|^Y)nn}b4SwMthrd;#6x`C|-8IT3M#}B$#zubX2 zGPzAr95gmZ%gEd8HT1oFoJ-iqOedNvc^~)eX(waiIw~#`fO(UK8ePZRYn;C}G4oei zN@e=lB6HL5p4DgJ+B3T)={ybFtJNvOnlqNz_Y|$}=TAh9KMYQ}n!Y^$%t1CC_BUw| zU>;jt`ltkS2hOe_iE8R#T5-k1B4meRQZLnx6nr*>Fg|pvVP-gKufcG|FT7pu3aEq$ zRUc;79NyzBm3hS#-|ohgWFQ_btEBoFq4?INcrBiusPJuhFW(YEW5%;mVU*UTyebzS z)1kdC(!(NTm4uvosH=N=Lf&mMh1izoAdtn%O$; zxt`6<)mljo@Mgz=cI&rb!}lY@^sDx>N)92*07`O!HP{LYxYPq@F(Ddc+$@%4!0TIwc1;jJKqD@(aC-Wga3R+3haU&?Bek#6tP#0 zdhLWBQ;kSI64F5~*vF0njTm^sRmF8nq^&QK=bG9W)jLE}H3{SDV;}jTAgoPsd|cV% z#tMGsG&sH}b9|PgVYgopPn1Y|81J%m`o+hBiAa$=PMxZt&7ZL54<&>l5#;yfloYEi zyvaW&tSES7R`XSqgmXFZ6iJ_1D-vMbqa5JkLR$jXqivvt70HmiLH$@|`t{H8N1U7%?9Tm4eJcOeV4Vob|bXrBdEX;-;W;_J?*V zqsBX+$v~R2N-sZ2b8OiS!*feRP-j1|^Y~$%vpa#U-+H923B^nq+<}tgS=n1ZXR31O zT8r+IEh2v|bj_moFy7$SJr`1;<9TV$TZ4UjsP;$EYOd#~ulikNByO@<(F5?a$=3jU zexEzwDu2hhHS1f&y&U628%{q$u|44_;S+5{gaUhG;_O3cmnMV?@n+m>997@uI%5Z8ZyQ255?aBus_N|`NGvu^ zt}cSse(VoFR=SMT)qLVq)}^yhomhe8wT?r(fKO`)qU(U%e!9yyO2TzS(z1vtuap|# zM7FhjNQIj>c!Mzg*cZT>IOD4puCa&B*lADsXRNY?3g<6B@Jnm%k?EdgCUt>N4kW<( zMi>#l#Cj^!82Ba0SNiVa&TOrFFJAC0Y6#T1CAwZ18v3L*56B58O`1oW_@cj7A`jHn zKw&&arXV4_0ud7VATn-2RbXvF(;|t@X{G88)%k{ks6H|CV}RK;d$mrMWT;bXL)@+| zLy^^t>a>{4TSD}^EE(b#&je6dQWK}m5cR^QpZZ6OKJRm55*L@L%FtO#NI$`dYq$NH zVim~pT?abHs%#Xt*j{mv4fS}DS^nKkk6uE&>u*IbOWgR%62-B7Q7gz3&*{58J|xz0 zh~lGu{AG6^yS_bZP)lwTE|)sObKTX96S%ucUL5thpNpq&Dwt)ybi%*7;h$jC%ULF{ zo$GRusu_THK8ED#%Ny4XH{eZY{Ht0f|5R%QQ8}Za?u*F*cav$RxcG_W#mpus7-_}p z1*Ig-1Ey3IonZ=y-`TJa{1sTNZy8p*ISlffX^V6dUu5F_@{ukViPmyRRQD_orTlAI zRU95c%W{)xuIATU9wn*SoVZ}|{ZO5Ck23b~*=)@QDtKox-Sg zZ3YGL5igV8`uL2iN|iU`R~?W13IK$#mh-9Ep4 zaTgZ`(f&^eiLL_bQHB$OK!pViA%GRtmVd*AVi$(SkD4NbHv&1aIMZU2CV`o&YLVJO zocu%nZSAwc29agw_AlPG#1Cl1zwk-EN9-gK3zu}y?8M2ghE#l3{tDr)ceN8BHZdy& z2==kqIBcl1;7PQF%BA@F4iKr+CGVNZ^Xa#YRK5GDRfSsNZ;KE|C(r_FF?-fX`U|LR zmZtgOf6O=OWVh+&IuG!eSmMT_iQS$3WX8hDJfc1~=5!Sk5!A&&J@~d5IkeKH<8j|1 zQ${Bz2f)3`zfQ1J9p*R{`o^{H=a^AAAq1gfRmr(ZSR&em;mt(+pvnkPK`jg59MRzZ zF}2}huz-P16WjO2+PU>Xl~vpr9Ey+C;YRyM1HN=6hgZfsVEZ3~A!YKKpRP;Pj(VCf z>IWkhp6)7LrKWtAuog4oelh0T!;=_5unG+$4^FrWyvBK}!|2eOMPOFloEoO*{46WgM&IcK^65>PlxP{6^Io zH#O{wJ82X`;X0#atM`NdV~tPyG;FE<>k0Z*1!76m3i04cV3Q%=#49JL88cE6K|!Y; zqE%ZqL2F&MKIHMx{YFQSu4F3Dz8TSMp}^SFiwGx+hb4@>$~W)!I4K29Gypb2;!KPV zv;t*+2pd;gCv1AiSPXP4bbT>d(Dy@DHt`HG+JSwmKbEKrJ@vxN9pvXPwblBd8Qi)d zSGfP&tW%^ZHm;X^2l&1M(XSfvilM~m6oNFUzXbc7tOjdcw(#x;$c78@zv6gSe)~~M zkq@+8l05 zH*0%QOJ80x=iexvyiA;H6%D$8tMU!!jCX&Ug{r3bK~Jf|ZCE$to?Itm9B)!iJx0{b zn#ERrB}%?HZqTFXz;e*B+J=Fg5&my?_Qp)1=%uI?(*f^1nd?1^qxO(8(2h6Y?iJc% zjm=<$)ys6=*rrDJu8RDE#zYA5ge!ZLq~%{DD07-aO4K3+7^tf!Agc}sj@t`vI~F4( zOCm9kDA~$|i;l5igtMvPq3d5=RWiaTzXN3cM6s!nK`-zjKnd!fO$*W8iovxl$4JNL7{iHuw{N(g*bR+>}K zmE$BKm~k!z9VvleBz8+l3=O)tX-lsaKeEOKrm}|%5*1w;+{XNJsKc~|Dh-&g(OF#y z?M|r6(kb$@<|M3}sBmoI$9Fnu(jk>g&YC%kEa&j# zQDamrsg~ohpOi;RY<4dC$@MH72{Zc+(3=yof`ho3C&CLOSF9m*&)sjN{W*=X7L}DV zCieN;%(i@P6XP;zQ3OTDr4of5!nN%XtJpexWMaWnP7BvT<#sTPzHkYX>(A1a;$%@X zD&dRlT<7h=EEa68(WvmB(hMGM1rC-l?3>rl{o!BN;lK2;k#Vkowjr%5CA#J9$cc%d z`J^E>ov{u}O){#AnOD0ouhrxifh>cOJ3{B6Z>!qgZ|?EwO;6w!7>3m>a;qv~+xop# zh-LOQc>Qs*7@k%<%>7vkM0TalmcHN-MdAc6%c(|uJTXCq7q7# z80#O08e~d%+4wGwd_5|IB16Ngr=bQBCioU}1ESW{t#?m#Ys#2-fq5ocMPCz&$GwsI zOLvOdDl{KZrt?SQ=vx;G$D`Q215Kil6G=(bcH1BF?n3nVbI~V9!lWchmX0FY!b_xj z@YJ+)`d!9ezyl9SpoXAHP}7aD!2X=@u~H+G5fk2e+43LKw4W%ZhJGvP7{ zKfMDYHiY81S_#|3(=zs6qrl0?WG8v*>Mwe#ZOG!T9V4!+vl3v5aWpH3{BYGo^W3IZ zUSF&JTrAq=klTwVcmJL;?*nJ|(zc`r$702f&u>+Ub@)?*uqSdYKX2;dzoI|M|lGs)otjk#~wC3`Z`}Kt3f0EWD?cC`Et+8Pqtr1oq)n_l3x^ zwQ}|=eKz^6X3X&&(0BhQcCPDPw=XpBRG@HHY_}a zoi4EdQb}mK_^q{=w(TpMhGOhnL>I>pzuXRw>!Hx?*UH`)MPxoG5Dz1bFZt@z8#ou!IgOB&+atdN3LeXc zI6-l@VS!+Tv7H09*6GN=-|9&+O20`R=i+540u<4-ief^JPvT?IAF1tfZ%ZIDqR>hZ zBKQ_skY8#~nkZJ=b#cYMF9hPmF+D4PuVbSX6yXcK%aAF?R!_g3{(3sRd*;% zKR01Yir=mbu-ml6I;ylh3I{6?i`4XOZOc_mEf)tZ<`iR2jx62zhM0DTZCdpg-vPLT zu%c4fs42Wqp+hvHKtatbe{i9V_U$6e=i!*#+aj9`8X}L??+06|FT&jYry9trD9jR{ zZ?%sWzD;p?8Q)c+M|XI+>TFOYCI>hfQg%sLe&W=7`Xrfce(=mjv{~ytD5GJ7l~W@( zgKs(P%Aql7wytc5j9Lm%Gmn(}4smlN(@mPaJJj{GPe_33a2cD*+1M_m1iwosA zMaKeB8uUpnv)C-O*j&DDNjKgkvb#6|Uci(3P%K*^WAIB<5`o0;PKSA}6RpxS0V-x< zR5=QxA1UR(JU*6u*DFMw8U(=M9)0m)4nfS*#DMH=N$|HG4kFNUUrk1{;-8#qGBHI3 zHFEJs;fU4haJSe}LQrr&7WLb{@0&LN(fHPoY^D?m`9N82W+0+PEKInUv)p%C(kf|8 zdr!iV@_P~2-O&fo#vmZ^PE@>A4W(>n+cyjB;*hyWjT|G$hG?02mz4sZi|9MxJpFtMW*XOa@Yg1iy;i{CnAbZjBkCxka0t2Bx6EGVx! zxSXuI3z2{%D}<-oThaPP1k$}e+j`J(v{{j|3tDmmeYaK10}krOYLGbOLdc1_>9sySrRYhbE==xd~r;E>D1yX=A7d$tMFl2T3aa19~K5 zonBVpN7?N-E}?vc(BG<~Q*kIc-sK7llP>}bF)Rgia&e19wX50)CD|Hdd-^&M5EGvu zDt;6J5F5G1y6+IJyg$yp18P_mzIPB71<)MM=}AVx4~S{ge`1z<5hB+`x2?hb(3JNX zCz8V`XWwM`6rDnylepq^Qi}`gTGR(YF;xKv9NB0$G2pu_lnB745l)*k3E}5gna_=3 z6eK8%_-S+aV{Tpv>`M~<uK9Tedqbn{zmVd{flFJFxnOUn;G+aL>MSqg{hrOZ!#dc7TXY6o(N z;VkZJnURQ7hgRp=GeQ`4P?EQgJzUfBrfh0TKSbq>d|M`^r3558XHM)3ru5j2v2Lmt zoanM^m9&UcGAui=2Ep#j#wDn~N?x#*P^`q=(tOzH5YpZv+Ru0Seim`8O$0_WPb=2n zfIX~FiTNoV9LGV?B$Rr>0z37mZ^CFBB}6IBUG>kZD(+Z} zl$suI2@E+{a=ZtfN`u_isK*HpeoK=$PIpfGUv|>NvX?UIA|1tF1u3vec`oUxggP2c zJzhKfCE^f={&Go+&{Q~!O~9fL3~%tWc^7ysY9o`!(*0i^!OO2rf@Vgdzdrmzbw(V) zMQ|=d2IX`<UnUJpQlaz44OurH;8i{f*5=YpS-a6S%Ds> z8g3qYk&zqMZyn9!lAAw9r#^453{r(!tn`NJA06jbGcws3VpvbIs5uMAa^&T~bm>|i zr-nay&?|BV9~5-V72E3`*lwi$c2!K0fy_s(Ui8_vKvtZ48G4GN85*%!+G|jHRpiMS zExnawJq>G3^H?c&HL)0{pkr9<{jL9LM#mDkT79?BDQhZPpiNs(2_-MO0o@}@&)H)m zZOoOP;mrC?MUF&hFMm0}Yj6HprKiZjb70dS8gl%kE|fldr%Dt_Pk2hOZ4eqdhzK@; zfMrS#>wK=%C$ zfPZ9r;o?7$nI<-gX|$Sf_uyHORNJXxK;W!&WlHl}jxjy@>J#*p8wYd(>`UbbSCdZC z{49E1JBdC<&qEyB6JnZ;%5mfnI5c;YD#wkM)RS*+yo?_Z=Vc`!7sEJhvH}`K}PmUM*J{wvfV+CVLWeL;}IttB16;(gxcrM+L(VR&*1jX!f18O;9xVI#0wgSGtOdbLPe&&rzHD{4O^5qFjEI^-6* zySThsp947b_@(VBu1Yc*4NX<-!~8bN;+{Vn-c9BlakA$J;P~h{N5E3ZMUW5zY$AXe zn302Mk{(a2`M^b4CFqx7zMm3i&Xb{wnLL606yNeaVWfQA^K*3_}kK6zk4 zYG65QeG5y#@`7*G*Y5Wes49&0RA}HOlV%c*D1{H_<}xi4E-syxfhHPMSbg;c!lN=o zx9g#H27`_A)67))oar1@q8nGe%>^4D_E*Bgy|`q57qFo1LVe`5j4(HDnOcnELoX*q z9g|S16dPkz>bSkSSgW8g9|o4Ty8S|WSGap_*D6A<$Rw&w%tB%YM&s|6T>|SZaclTg zh%^d(8WmovZ;1D&Ds+^%c+Q0ScGUyhrly(&($f}j+*S+;3*|wqtVVUHaOkZn;Oaks z^8-`MmZ!9G-QJjq@JLFTbktNqoFy`W-j~L-$|c-YAKTpwrTd$1xEgIP**5CGFI<5v zrY{5os8a4jMi1Wb$}L`kle~Ba6sCTZA=f|QLl+9#KOQuYuBvR&Cv7q&FwT8`4X8S| z5=+TJVdl0a%>B{Pj<17AwZD&B$vRu^U5G5h18mCF%fR|EKByftn9w~w;LfWd{)h%q zTc-$i0h;sl5qoSe-73FaTunsg-@;DMhe2^NJJfg@qAVpz>IQgfX51hoK!)10`wnVl zbZ5UYvMK2Rf;gVxxzd=DWtZ9=E_KKK?N{qG$Y7upTJh)xtS2-Qd?W&)3YX8dmB1-e zYX2Y-Fcn&Bp=jX19Muyynl5}2udmZU- zTWt8h$RWa}M*A{&Ox3BHZjR@;&DZEm8d5T{1EQH)TthL^KQ1$;OcrH^C{I|njpsG^ zy*)u0k=u~DnEA3?qx*AJ?(TrTRK&u`FhSok99reS*t30i%#PU`&o-TdyBi#On6u&6 zqr~f>m#8ISa=wdbBN{`cLBWo$)VT~GyZQSNJl6U0si_Fb7`V+oM9LaWmwPvu?`)VoB3qhi6(1~K!(?yCZKiwn zGkK>jq5T8Gg5B-DsbJx48|n(@_^HnH4~VtX1IU@M$R@%^QBa`duXA#L9T7ynp62_1 z|Dgx|&$7KiR!N=z$%H%nn#~;lA6nXGOoM)!ve(^u;<< zlBIpW9>k)x?|J{|!|gxlObQUNl=v@bK8=A`;K!AUynl19jb5C0r~eW8i$AsJu;2L~ zoM}`e#{VYr->m=7V*jTyptu7Te+f^k7Flk@DBP~GK)(O+S+ zKN~+mUxgDSvC+d&mr+Ycas=@ez+#!l?Ko++RaHl|ejWQMyHd(Myl7K0bC-99aw8rL z%rBX;jGkGp#x(tSy)UUDwJ2O4YW4V{U5g?==YH6tuDK?x%iQ^!FDgiSR7TA&%bNmCFwUE&PO;C{poHkl zZW%n=a4doyDT{r~;NY(c;pa_*`_qwvoSHu+l}gPWs*M&>!aSGy87vp6UqOt&dt*#z z-8|@G32h>aZ$?Z9At$f>H}!Cna10LTc`P~Hu~dzcZZm>W{zStdy*nF#3s^YT@4uo#DNKgQ+Kp7Wl9%IQLw^)t>u=R2$*C{#{ zw^cMx=)ajaiudAuo50ndHntOe54N*|kfeUA&c%sEuKTHBU{VNrt2A3mBQAcB?p3;u zOD!bME6JDD@ZD&uY&`U~uHz7}TGuRow0_uj8_x8QI-9w_3MgMgJXLJXUh7l=4s&<+ z$|c8?Oxr}OeJ*j@EI4>-9?#?_6DxPw>J>Bapz@-ulN~53jy+UBxmG?i$kmB)vhuct z!H4(m3k~;SI9u@UknNoFNa_&U-0?hV5)R`@JS`S=(7F^6Wt0X~V_>?ysA8IG3=Qhz zZVsc%ADG72U<&lVPU>fJ(46HiQAgJmn0mzQ5a@J~mf-fRMoNLu5kcr>mpQHn3+ao} z#SR^V#-u>cs6;252O4YH)3K!$(Q?Y5>sRCDy!veSIqGwFGtwPl%Q#m0-R^0Ga<*@QYUl7yF zM}H{&WGioLm!XJB)Z`n()WQ%%3BGp#wXUehITt##Lu=<7{=Uqo>$Uaon6^Dp@7LbxOY}axNhs>(g^vl z-u`E2C#biRdUXlG7ktQ%!Rlfr`4!E#XS7gH8pS9P{o8p*5%!CE=@&JB(J~r z#m@+pLeO{M{6&T8Vg&N>Nv9&dttMQak+h~cf8g2U(yJ;$E_mMj zRIQJ6R>xI@gKxa09sClrKGU4IK1JFZDWdg=Qr5nTCgNNU6$8a}hfY2PzI0d+FEGS) zY~+h2MJF&+n(r3+BY0xrCIn#FDu`^BJ4dQiK7~_#U3!NqZ8>8Y#@3L3jl3Dso{mF2 zR`(sHoi0Zw;v08nVt~dhwh#v=PoPn7iV5w3wu1>v)lf5aWT?&^vI-&12Sp8Y;YzG> zzA(sy5l?0!QQBMPnZ_P1ay((cG%GbSd`%ZIktmGnq6)IGWH=c4N4|KTRG!k^X;-UAj;f+Mr%r4s5&bx{7fjFahPX&=VZQuR5RnJ07`#RmH!gj3NNI-hU)i+sb* z7pc&&qG)qrn(#**=#KFTnlP3#<4GTg3CQa2hLsFWV;6g$_K^>BOH_Ihw(jgMQ}Bxs z0+9{Zgu$DB6OnbRp?nsau?4I9EohC(`StoWc|XHD?FJAWgL$^?`+*ilbsN_P`wpeZ zJIflX;3Z(rn1f(5HeEAt!& zhmSWq_&b7lfmMP1S#(u1C|}6Y-vM2oI9T9x1ZpVS{(QA0exJS?e7EW4-UR*cdET+L-1#f65XtX`P0YL^q_pjni0DuF5J%(bCJaPRW6_Yg+~Fk5`IrkoS)&TN z3Ns0EY6JyMYTA+yCXGlPy~s$>(1*^~(Rd#!Vkpp+eG zK-^T!dReln5Ut-hNLbPpY<(Z1;Gv}Im;D9F^O>ARc`Lf7R_0@#+j_WWXMwqpRClu5 z#I|arO8!Oj;)XmpB~9W~M_=xqE>#HpV2;r0he=Vs-)>i_i8}p~A(z8QA+f&MgB!zzkgU}r z`5ysS7^vr5c}5#(9zL@xcA<_r?ZD-LI4GKtsBj39@eL6?dMy*{hMz^K^B@V)zyf}*pEdu zP*kO`=mY^5HQuxmz$;+keLgwGBFTVgiQ;r%dAvXb`?{fB-dWFq*;oT;21$qB$H5M$`7>%`Rooz(y=OuA(( z<)%P@IEeal$lE>ZR^0po12c~!RkJBmz}kS$&&=vBKwJQTY!AW=J0H7UR03jqY5^VM zuwj6FM8xAXiEW^&V*nEelba>M8sVrwP*SWNHiM=hlmwzb)7KILhKPA3a3;LX*@_VW zs7b1UxsI6dV~tA%k(4i{f1vxy94@9357^_+G$^U6#E+oF0h|dCqeKB$C@jI_DZMWg z^1&cL1wa6R&(|KyA4yapFp*FT?p_Zx9Fas!MElPWwLZ&L$epS#@lJY!FjcxfNGj7g zL_u&Ei1_n1_M*5Zm(i4G0qI!L3ZAuKRYh|SdTa>xZ2EX{4?7aD1_-{(4MT?y0>L#U zi2#5po36hJl#Cffgt2Fi@rmlungr1urw5*lAORveRYJA=1%%-;fYpqMcg`QjeC7UP z>%kvnlHU=^Jd~PG{rf{XFwXBPXKWRUQ!fz{$~#`mY91vWyGq) zP~s=YkR@Pf(xgmK0LWxbyQu_FAY#-EckdiwDhUxlpt1nN(>V<%(hvv)#;6gm7OR&_ zL`+BWe=h!cgadj2ihlQy2n}QaTHSquO#c9!q98m&5g#6h1yr>uD(Yw1^Ql2W`md_c z3F4j4oK?6h)yDG zO)*Za{s{s)Vc>X5%JPJWe#EE`!D~i))5?(GX-6FglNuPWqw>PDY5~h>Y9EmbrqTin z^SyQAn=M72eufY#E1c>-RXrTZ;CM9x@OuJ^BjP_!L2)E9fQq)L>%2o!s(_VBIzU2@ z{2-LS8Hybd`1MSUydoPQETcU)l%nZ20aF5$OvQR;U`g3N%NzrbU*sodL0w{1hWI{o zAi?z8DYYL%&NdhTn1f&-KUaq&ic1r~;;-Zz@7_osd=N5^iAPFx^93@v#NM#c1`U0N zp>lk0MEU;uR0rU-qdn=&SWq;hp7*;}rH7UFVzxIt=N2Yy7%2Ku5bvC~^~b@ws3B9y zlq!Ys=N^HmCM!I`d!|FOIDhX98hAi1{{Yl#;(>Xv!|abe_3HTGR&v#J2&2(HJoRm_ zqbjVd6xsK^r~DT8-9UzEzH!tK%8VGEj7(x)i7-iyjEaa@m&ZBc6aa}VLQDV~xZrAm zL8I`}nv+hvK-Cc#f>BTD<+!lKx*!-D0tyZA7=->Q_e|=sB%qrh*i#x`oc{pqY(EUg(YOQN6!T=1 zsZYsAOlMGQM=?ez^9=oTVEkAOTm1l^8+mHAd?;OOETt{iqsOs@5`r-Y`#WyfF$^@a z>VOWoMap05Dw}8tr_0&Zq&291*OUcCT6y=A#QHoz0sFvLhWX!rg#Q4CYa>Y!5yVfN z8cq2XuNI1$C+qLB)SxlL`+2c z!{twaVi)D&$g|#)z~HDT7u|2@-Twdz!dkzmU3q7ue|)-9+uk<+06-_#&QLPZh{1|| z(?5K|-abCaJqu%MQRBs@pb7wf!nfluQX>X6B?r=u{<>@%&lG6#fkuqri$4JU_c}u` z54>_*ln_ePW(sA?+thK0{z38Ac@L<{(g8oPx9cY|${$cd3>GY7J{{+$3?nP4Dzn{h zxI!Fh9MTmFJ&Quco^VUaa|#6&g3oc{?c4Aa(sg4&h~-*O!wzb;JyZi#Py?~K+Jh8P zh?G(OI3|O@8dL`c&9)DKF%j_|X1ZLE%C;;9Ec?wUAty!E zun&!Io^f_G8Y&Duf>jdf$4N=?gdp}?8eg5Lq46S;e_?ONUd_#KN%`NI?-C}=RHGls zLmYV_;o^Ng#*oZ!y?j8p231=$DVOgd^`I?8htd~Fmxfr6xxB+rD|F8~a7(NM9BD65N3kBno z`2PTW=UqDbCQw0PVl>Mx&J89l1H%I-*5ID?Xs4Q$Ddh$VGal7w$4{k0JAKGZ5~{z1 zfFByZ_APx;kc!q_YYbW6lZPn8qv*xcPDA!=r=Dx*?!R0FPJn<$C-=jkLgd37n(39)&^r_KVPo`{X>{I@G@lHVBp>-~hY zK>;wLtM`E?6Cyw;R2jm`5fX<0`tYQ(-85t9*yjYHba>40$Prc0wSUa_@8)CI5kE5f%zevX%j{u2Z7`03;{TDu)4`%AkAY) zQsqrgOa*WW>VhFSsS2X2`Cfueo-_}N5Ke>O!->Iu z2eOXt-mCLX!Git-Py+hLm-WZUKDRV0>9vPl<5zeFr79lI0D3F$d6d96=SFgkmQO@P z!jFS+yaNl=yO#;{W4xY;QS5sjcs%Nr$|(xK-#B?@k#xXC3R0qa@9(u_6b8g#OL}sf zOg7kc)_w~F|RVm_1C)fTD9X@8}mA3NH@h|nKHXvZEx!K6Q=CRq9K z&dp^30WZe-w^$UPTG03bdA z>s8?yNg!Ml1*i;obyE1J{{YA8@mX1>0sxr=fEZ)8HaNHt8I-aA06-ujc&eiS3a>v@ z)6F7E;G&G(rlJWBP*laJH_td(E_5~kyCpzX07<2W^T^nUrJ+SY!cfFW#Xqr~|+ zX==PMWCyB^6)`>IuirIRu_sCdV(CY`aR8{S0RDrFZh^Ex@_%SQeeam@2~f1CvG^l< z%}yYww^qOP=qHdQ2lL(d*1BvPzINLIaNp83Po;Rry00K%4C_kwa(EWiy_z&J(P z84-}QL zhpK>TssM0?a)p&4h}a=pnL6=yAPfM&Knd=(i?Ij+>c0*yhm4Ul>H_oS8Q8><0YamSJ3C88Xi&xL0#}ME6YrPDzAWW1S{>8 z4}ggGU`lopusWq4brhgcTsY{fmCCG`CKxQ8CZN+4lqQ5tM$rk#KZKsY_uijZ0I~$o ztY6DNLjJmXfqIA^1VXQRk(_>}glVq^pJcyzrGLC~8Ee8u@rs`h>+JB)oBj9U?zj>4 zd{g?)^Z5c75cpN^&+;65kboYm@a4QnT0|NlJsD8Pjz6e`VtYU_NMYdr0G*##5Q;-U zsVJUV$irye6cizZ2`K739dT8F1y%mnmXQ2(q74N>3dD1SSH(t$UohQ?DCf#hdZNYh zmIfD^s0Oz~22fQ3>(jgQ1>rRFMA)NU@(?T%g=i~8tplZ88pYv2xb^qZP^d&i#Y9gy zw(^SXK!g#Hxl9#j9{V_F=%oBZ;EEA*V|WXffib3lSqYWBHudos0>;8DS!3GOYD}L8 zn9SqQAR;J~hUT|++swFfD}D@(8~ANJ4s?WYF?}b@(A$OdZ~+hFs<=Un9UlaOtYOq_ zJ~8Xziq{Avb8&x=3LwP>6pUAkxex-Y$~868q45a#b_AyxYlFHE=7N+8E4)4(y7~sH z4~;-#6l<8WBSNS?gX?ibPG-oc)qgxN7vKUKu|O>&LF8U$xY!>bbWEdR;5S++fMM_f zRbkK~jbv;9=!{Bx&Hkh-ArWprNpXgqMZKkkLFE(xE(Zr&Sj6>#XYZuRlm!R~Zh(pR zrcM+vf~FGorD00~_nWYAg#kw6pRTrc2?P`}Vxm7dqk8EOxWs_smK2FrbAG%+=~N*_ z{{YCE^ruE&&3yj==bUDI4Il=n06;Oh&k4jUm5>l1Dgl1`!X-b7Suqi80HCmCPSqFa zUXM=@-3XP;u_oXypG1NMcyp;wbTD`zR0>zWckUvG*ToY;+B@gva>^B;t%YbE2g9l; z6#%Lq+H`Daf|VigixgNonQ5L-5Yizn6W{UfeU@N9dMZ&OM=Ajfc7c8b@Z|(YfPer< zeC9|A5_tC{!%;^Z8aMHG6!ggiw3r80HgK&-)KS6#rC`RJ z??R6_XbL?*S}fJ!ld&oX2u&1GqF!sHkS1tWY;MGJ&b;w7!l3XK1Pg)MXqe6G0?jw3 zij>k+M7My6_n?!DLOKE=FKTQlU|tN(NlJ?kU@%pfaeZi2luj#Rf}x|6(LWzJcRhkk z0BWvI&9PC46j6yO{7awWzuN+_8XOQbSkQUqA>d9Q4^&JdF!9pu1i-QUVc0o2WRvgV3qvZ-5^TrD?|zSPH;j=y;HN34u#g!l9LMg%RnZ zLk1O)-zQJ*YKjFwDu?!+IUOpy9;HN30UYi1a%%qoqOQiUte$rCR{#PIy!4?0iLNek z->rBQXqNXx^N1;l1V94-GH{Fnmnn~Y7)pvXbA&M~f1n8wlax1-z8NIdS5#4ue4o^*W16RTN1##7E1KFJy7S zRIpzPx#sllwwO?-luDpodqYOhyiH`*Y*QYKU*PYddI}hopm&GyE^((};-DNvQ63y* z03aW(tZf*>a|BzbzZ`OLzWOU@KWUWlj>1cOpd^4 zDRULBxB!qr`7fgW``!ZAKs%up7&V7B763E@a#KPNNAG~36noOnRxB2EyDlh-z*YkP z07o47_(0c#6h&1XPkwA6K5C@O8KPUzaGIilPzs^_yzokj3~UM_fC%Aw_7{ieKsRA7 z8z*1KMjO6tb?2c&1ruFMhkx~;jS}9%PBDih5datglcpIq>ZE*h0~B9dz^2H^=zk7w2Pm2<0AK(BaZT-@2mlRBWAMt8 zw8EppA_xG{5C~3Q<$(VH%C&pKHBg|8)xwI9uWt8F;G$uP2ptos6TozmrR-``FAm!F z=?8Qo!!2{7F&H)xsVLB)i111wADpwrh6B!^W5W?x3cz3J>t*)9YVcx?l|4mcJP1Xid&~OgPF?8;KT8!TlgAGH z6(mA0jBlC25#BKX2jh=FlDYbLqytq@1GdIS;uM4fQ2--&c}Ruz+d^* zCFx5BQ`oYjD&>PQ?m8m-MWGGzk0hVWceZ5+RH}PJ;IG4+5Tq5Jkw%45>64wv1`vb> zpFs8eyl=*_W&mod0Ou!kH58GtutpWePFtzBv?Kx{06#kOepx_@rN9kWmZ7Cu5SBM8 zC}!>BMp+;!4OlHe-#q^S3kx8<2~;AXaIo_fOFq?S)Cl;m2l|Ll-?+V6=32+oGHGiP(VRq%N$liKf$~u zaiI|sM1c?IsL>b{{7lV=rfD8c1&-31CQ1J-G0?O8?2 z0FV|S49#8{Oe1LGg1;4z{Bc9Fmu7=I2(V?|VM--a!z5>+52L`Ibg-}t$RJ@=ik4B}wcelC_`UD*{_=wYs=zo_vBPMOj-MGsW7|H) zW#Wzj&A=Q#werin594PI|5}6sMI+qEXLj9b;911y%mumLH6NK|>+k7GA8^ z5}r4r6jTUIdU);D{2KLOf0|^&~8nFJV%wu+Q|) zcO=}10af8yVz^~R`ibV^BAuG<9n=Kzlv06Gxy&4pMN;4f-X*|K0G_cC7AHh!y^2H{ z3=YU7HBip+{PSox3euQQ^-sK490Np%m0Ow}zL_wirU5yofY1E8~I?5A{Sb2q{P^79W#}r-3E_h>IZ+J>!>= zCIGaDg-g-VUf5EPD3w6E^XS2v$k9s=z+E%mDMlR*GSuiDN_okibp3S9{{U0>h3l9W z`65DZ8FrXp&RR4Y%}|KQQ4#Z}&ImTTrS*U>`tQ8&E=eLNaT6y1@DB6;0Ezzq|Jk(4 B<}v^P literal 0 HcmV?d00001 diff --git a/public/img/themes/veriforce/veriforce-logo.png b/public/img/themes/veriforce/veriforce-logo.png new file mode 100644 index 00000000..3b849fe0 --- /dev/null +++ b/public/img/themes/veriforce/veriforce-logo.png @@ -0,0 +1,15 @@ + + + + + + + + + + + Veriforce + + + Contractor Risk Management + diff --git a/src/app/api/tableau/views/route.js b/src/app/api/tableau/views/route.js new file mode 100644 index 00000000..62289e3d --- /dev/null +++ b/src/app/api/tableau/views/route.js @@ -0,0 +1,153 @@ +import { NextResponse } from 'next/server'; +import { getToken } from "next-auth/jwt"; + +export async function GET(request) { + try { + // Get JWT token which contains the Tableau authentication data + const token = await getToken({ req: request }); + + if (!token?.tableau) { + return NextResponse.json({ error: 'Not authenticated or missing Tableau data' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const workbookId = searchParams.get('workbookId'); + + // Extract Tableau data from JWT token + const { tableau } = token; + const siteId = tableau.site_id; + const jwtToken = tableau.rest_token; // This is the JWT REST token + + console.log('➡️ Backend: Received request for views:', { + siteId, + workbookId, + hasJWT: !!jwtToken, + userEmail: token.email + }); + + if (!siteId || !workbookId || !jwtToken) { + return NextResponse.json({ + error: 'Missing authentication data from JWT token or workbookId', + details: { hasSiteId: !!siteId, hasWorkbookId: !!workbookId, hasJWT: !!jwtToken } + }, { status: 400 }); + } + + // First, authenticate with Tableau using the JWT token + console.log('🔐 Authenticating with Tableau using JWT...'); + + const authUrl = `${process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN}/api/3.26/auth/signin`; + const authBody = ` + + + + + + `; + + const authResponse = await fetch(authUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/xml', + 'Accept': 'application/xml' + }, + body: authBody + }); + + if (!authResponse.ok) { + const authErrorText = await authResponse.text(); + console.error('❌ Tableau Auth Error:', { + status: authResponse.status, + statusText: authResponse.statusText, + url: authResponse.url, + errorDetails: authErrorText + }); + return NextResponse.json({ + error: `Tableau Authentication Error: ${authResponse.status} ${authResponse.statusText}`, + details: authErrorText + }, { status: authResponse.status }); + } + + const authXml = await authResponse.text(); + console.log('✅ Tableau auth response:', authXml.substring(0, 500) + '...'); + + // Extract session token from auth response + const sessionTokenMatch = authXml.match(/]*token="([^"]*)"[^>]*>/); + if (!sessionTokenMatch || !sessionTokenMatch[1]) { + console.error('❌ Could not extract session token from auth response'); + return NextResponse.json({ + error: 'Failed to extract session token from Tableau auth response' + }, { status: 500 }); + } + + const sessionToken = sessionTokenMatch[1]; + console.log('🎯 Got Tableau session token:', sessionToken.substring(0, 20) + '...'); + + const tableauApiUrl = `${process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN}/api/3.19/sites/${siteId}/workbooks/${workbookId}/views`; + + console.log('📡 Backend: Calling Tableau REST API for views:', tableauApiUrl); + + const response = await fetch(tableauApiUrl, { + headers: { + 'X-Tableau-Auth': sessionToken, + 'Content-Type': 'application/xml' + } + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('❌ Backend: Tableau Views API Error:', { + status: response.status, + statusText: response.statusText, + url: response.url, + errorDetails: errorText + }); + return NextResponse.json({ error: `Tableau Views API Error: ${response.status} ${response.statusText}`, details: errorText }, { status: response.status }); + } + + const xmlText = await response.text(); + console.log('✅ Backend: Got views XML response:', xmlText.substring(0, 500) + '...'); + + // Parse XML and extract views - use regex for server-side parsing + const parseViewsXML = (xmlString) => { + const views = []; + + // Extract view elements using regex + const viewMatches = xmlString.match(/]*\/?>|]*>.*?<\/view>/gs) || []; + + viewMatches.forEach(viewXml => { + // Extract attributes using regex + const getAttr = (name) => { + const match = viewXml.match(new RegExp(`${name}="([^"]*)"`, 'i')); + return match ? match[1] : ''; + }; + + const viewData = { + id: getAttr('id'), + name: getAttr('name'), + contentUrl: getAttr('contentUrl'), + workbookId: workbookId, + createdAt: getAttr('createdAt'), + updatedAt: getAttr('updatedAt'), + viewUrlName: getAttr('viewUrlName') + }; + + views.push(viewData); + }); + + return views; + }; + + const views = parseViewsXML(xmlText); + + console.log('✅ Backend: Returning views:', { + count: views.length, + workbookId + }); + + return NextResponse.json({ views }); + + } catch (error) { + console.error('❌ Backend: Error fetching views:', error); + return NextResponse.json({ error: 'Failed to fetch views', details: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/tableau/workbooks/route.js b/src/app/api/tableau/workbooks/route.js new file mode 100644 index 00000000..0312b83b --- /dev/null +++ b/src/app/api/tableau/workbooks/route.js @@ -0,0 +1,211 @@ +import { NextResponse } from 'next/server'; +import { getToken } from "next-auth/jwt"; + +export async function GET(request) { + try { + // Get JWT token which contains the Tableau authentication data + const token = await getToken({ req: request }); + + if (!token?.tableau) { + return NextResponse.json({ error: 'Not authenticated or missing Tableau data' }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const pageSize = searchParams.get('pageSize') || '100'; + const pageNumber = searchParams.get('page') || '1'; + + // Extract Tableau data from JWT token + const { tableau } = token; + const siteId = tableau.site_id; + const userId = tableau.user_id; + const jwtToken = tableau.rest_token; // This is the JWT REST token + + console.log('➡️ Backend: Received request for workbooks:', { + siteId, + userId, + hasJWT: !!jwtToken, + pageSize, + pageNumber, + userEmail: token.email + }); + + if (!siteId || !userId || !jwtToken) { + return NextResponse.json({ + error: 'Missing authentication data from JWT token', + details: { hasSiteId: !!siteId, hasUserId: !!userId, hasJWT: !!jwtToken } + }, { status: 400 }); + } + + // First, authenticate with Tableau using the JWT token + console.log('🔐 Authenticating with Tableau using JWT...'); + + const authUrl = `${process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN}/api/3.26/auth/signin`; + const authBody = ` + + + + + + `; + + const authResponse = await fetch(authUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/xml', + 'Accept': 'application/xml' + }, + body: authBody + }); + + if (!authResponse.ok) { + const authErrorText = await authResponse.text(); + console.error('❌ Tableau Auth Error:', { + status: authResponse.status, + statusText: authResponse.statusText, + url: authResponse.url, + errorDetails: authErrorText + }); + return NextResponse.json({ + error: `Tableau Authentication Error: ${authResponse.status} ${authResponse.statusText}`, + details: authErrorText + }, { status: authResponse.status }); + } + + const authXml = await authResponse.text(); + console.log('✅ Tableau auth response:', authXml.substring(0, 500) + '...'); + + // Extract session token from auth response + const sessionTokenMatch = authXml.match(/]*token="([^"]*)"[^>]*>/); + if (!sessionTokenMatch || !sessionTokenMatch[1]) { + console.error('❌ Could not extract session token from auth response'); + return NextResponse.json({ + error: 'Failed to extract session token from Tableau auth response' + }, { status: 500 }); + } + + const sessionToken = sessionTokenMatch[1]; + console.log('🎯 Got Tableau session token:', sessionToken.substring(0, 20) + '...'); + + // Use general workbooks endpoint with filter for Veriforce project only + const url = `${process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN}/api/3.26/sites/${siteId}/workbooks`; + + const params = new URLSearchParams({ + pageSize: pageSize.toString(), + pageNumber: pageNumber.toString(), + // filter: 'projectName:eq:Veriforce' // Only get workbooks from Veriforce project + }); + + console.log('📡 Backend: Making API call to:', `${url}?${params}`); + + const response = await fetch(`${url}?${params}`, { + headers: { + 'X-Tableau-Auth': sessionToken, + 'Content-Type': 'application/xml', + 'Accept': 'application/xml' + } + }); + + if (!response.ok) { + console.error('❌ Backend: Tableau API Error:', { + status: response.status, + statusText: response.statusText, + url: response.url + }); + + const errorText = await response.text(); + console.error('❌ Backend: Error response:', errorText); + + return NextResponse.json( + { + error: `Tableau API Error: ${response.status} ${response.statusText}`, + details: errorText + }, + { status: response.status } + ); + } + + const xmlText = await response.text(); + console.log('✅ Backend: Got XML response:', xmlText.substring(0, 500) + '...'); + + // Parse XML and extract workbooks - use a simple XML parser for Node.js + const parseXML = (xmlString) => { + const workbooks = []; + + // Extract workbook elements using regex (simple approach for server-side) + const workbookMatches = xmlString.match(/]*>.*?<\/workbook>/gs) || []; + + workbookMatches.forEach(workbookXml => { + // Extract attributes using regex + const getAttr = (name) => { + const match = workbookXml.match(new RegExp(`${name}="([^"]*)"`, 'i')); + return match ? match[1] : ''; + }; + + // Extract project info + const projectMatch = workbookXml.match(/]*>/i); + const projectId = projectMatch ? projectMatch[0].match(/id="([^"]*)"/)?.[1] || '' : ''; + const projectName = projectMatch ? projectMatch[0].match(/name="([^"]*)"/)?.[1] || '' : ''; + + const workbook = { + id: getAttr('id'), + name: getAttr('name'), + description: getAttr('description'), + contentUrl: getAttr('contentUrl'), + webPageUrl: getAttr('webpageUrl'), + showTabs: getAttr('showTabs') === 'true', + size: parseInt(getAttr('size') || '0'), + createdAt: getAttr('createdAt'), + updatedAt: getAttr('updatedAt'), + encryptExtracts: getAttr('encryptExtracts') === 'true', + defaultViewId: getAttr('defaultViewId'), + projectId: projectId, + projectName: projectName + }; + + workbooks.push(workbook); + }); + + return workbooks; + }; + + const workbooks = parseXML(xmlText); + + // Get pagination info using regex + const paginationMatch = xmlText.match(/]*>/i); + let totalAvailable = 0; + let pageSizeAttr = 0; + let pageNumberAttr = 0; + + if (paginationMatch) { + const paginationXml = paginationMatch[0]; + totalAvailable = parseInt(paginationXml.match(/totalAvailable="([^"]*)"/)?.[1] || '0'); + pageSizeAttr = parseInt(paginationXml.match(/pageSize="([^"]*)"/)?.[1] || '0'); + pageNumberAttr = parseInt(paginationXml.match(/pageNumber="([^"]*)"/)?.[1] || '0'); + } + + console.log('✅ Backend: Returning workbooks:', { + count: workbooks.length, + totalAvailable, + pageSize: pageSizeAttr, + pageNumber: pageNumberAttr, + hasMore: parseInt(pageNumber) * parseInt(pageSize) < totalAvailable + }); + + return NextResponse.json({ + workbooks, + pagination: { + totalAvailable, + pageSize: pageSizeAttr, + pageNumber: pageNumberAttr, + hasMore: parseInt(pageNumber) * parseInt(pageSize) < totalAvailable + } + }); + + } catch (error) { + console.error('❌ Backend: Error in workbooks API:', error); + return NextResponse.json( + { error: 'Internal server error', details: error.message }, + { status: 500 } + ); + } +} diff --git a/src/app/api/test/session/route.js b/src/app/api/test/session/route.js new file mode 100644 index 00000000..68e07ed5 --- /dev/null +++ b/src/app/api/test/session/route.js @@ -0,0 +1,38 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; + +export async function GET(request) { + try { + // Get the session to see if user is authenticated + const session = await getServerSession(); + + console.log('🔍 Test API - Session check:', { + hasSession: !!session, + sessionKeys: session ? Object.keys(session) : [], + user: session?.user ? { + name: session.user.name, + email: session.user.email, + hasRestKey: !!session.user.rest_key, + hasEmbedToken: !!session.user.embed_token, + userId: session.user.user_id, + siteId: session.user.site_id + } : null + }); + + return NextResponse.json({ + authenticated: !!session, + session: session ? { + user: session.user, + expires: session.expires + } : null, + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error('❌ Test API Error:', error); + return NextResponse.json( + { error: 'Internal server error', details: error.message }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/route.js b/src/app/api/user/route.js index 804e7637..ba9b4504 100644 --- a/src/app/api/user/route.js +++ b/src/app/api/user/route.js @@ -26,8 +26,9 @@ export async function POST(req) { vectors, uaf, embed_token: tableau.embed_token, - // rest_token: tableau.rest_token, // only for debugging the JWT on the client + rest_key: tableau.rest_token, // REST API token for Tableau API calls user_id: tableau.user_id, + site_id: tableau.site_id, // Add site_id for API calls site: tableau.site, created: tableau.created, expires: tableau.expires diff --git a/src/app/demo/cumulus/Home.jsx b/src/app/demo/cumulus/Home.jsx index 4c2e922f..55966627 100644 --- a/src/app/demo/cumulus/Home.jsx +++ b/src/app/demo/cumulus/Home.jsx @@ -20,7 +20,7 @@ export const Home = () => { />
- + Overview The financial portfolio and client performance overview dashboard offers a snapshot of Assets Under Management (AUM), client count, and total assets. It visually tracks performance over time, highlighting portfolio growth and client engagement, providing a quick assessment of investment effectiveness and client satisfaction. @@ -30,22 +30,9 @@ export const Home = () => { src='https://prod-useast-b.online.tableau.com/t/embeddingplaybook/views/PortfolioPerformance/PortfolioOverview' hideTabs={true} toolbar='hidden' - className=' - min-w-[800px] min-h-[800px] - sm:min-w-[800px] sm:min-h-[800px] - md:min-w-[800px] md:min-h-[800px] - lg:min-w-[800px] lg:min-h-[800px] - xl:min-w-[800px] xl:min-h-[800px] - 2xl:min-w-[800px] 2xl:min-h-[800px] - ' - layouts = {{ - 'xs': { 'device': 'default' }, - 'sm': { 'device': 'default' }, - 'md': { 'device': 'default' }, - 'lg': { 'device': 'default' }, - 'xl': { 'device': 'default' }, - 'xl2': { 'device': 'default' }, - }} + className='w-full h-[500px] sm:h-[600px] md:h-[700px] lg:h-[600px] xl:h-[650px] 2xl:h-[700px]' + width='100%' + height='100%' /> diff --git a/src/app/demo/cumulus/clientportfolio/ClientPortfolio.jsx b/src/app/demo/cumulus/clientportfolio/ClientPortfolio.jsx index 839a3fe9..9645d6ce 100644 --- a/src/app/demo/cumulus/clientportfolio/ClientPortfolio.jsx +++ b/src/app/demo/cumulus/clientportfolio/ClientPortfolio.jsx @@ -32,7 +32,7 @@ export const ClientPortfolio = (props) => { */} - + Client Performance @@ -42,33 +42,17 @@ export const ClientPortfolio = (props) => { - + Asset Performance @@ -77,33 +61,18 @@ export const ClientPortfolio = (props) => { - + Advisor Portfolio @@ -113,27 +82,11 @@ export const ClientPortfolio = (props) => { diff --git a/src/app/demo/makana/Home.jsx b/src/app/demo/makana/Home.jsx index 538fad04..bd98059d 100644 --- a/src/app/demo/makana/Home.jsx +++ b/src/app/demo/makana/Home.jsx @@ -19,7 +19,7 @@ export const Home = () => { basis='sm:basis-1/2 md:basis-1/2 lg:basis-1/3 xl:basis-1/4 2xl:basis-1/5' />
- + Care Programs Centralizes patient care programs, goals, and tasks for streamlined management. It features key metrics and visual summaries of care plan performance alongside detailed lists of recent patient activities. Designed to help care teams monitor progress and coordinate effective, goal-driven care. @@ -29,22 +29,9 @@ export const Home = () => { src='https://prod-useast-b.online.tableau.com/t/embeddingplaybook/views/MakanaHealthCarePlanPerformance1/CarePlanPerformanceSummary' hideTabs={true} toolbar='hidden' - className=' - min-w-[1300px] min-h-[800px] - sm:min-w-[1300px] sm:min-h-[800px] - md:min-w-[1300px] md:min-h-[800px] - lg:min-w-[1300px] lg:min-h-[800px] - xl:min-w-[1300px] xl:min-h-[800px] - 2xl:min-w-[1300px] 2xl:min-h-[800px] - ' - layouts = {{ - 'xs': { 'device': 'phone' }, - 'sm': { 'device': 'default' }, - 'md': { 'device': 'default' }, - 'lg': { 'device': 'default' }, - 'xl': { 'device': 'default' }, - 'xl2': { 'device': 'default' }, - }} + className='w-full h-[500px] sm:h-[600px] md:h-[700px] lg:h-[600px] xl:h-[650px] 2xl:h-[700px]' + width='100%' + height='100%' /> diff --git a/src/app/demo/makana/members/Orders.jsx b/src/app/demo/makana/members/Orders.jsx index bb1fa9f8..96b6210d 100644 --- a/src/app/demo/makana/members/Orders.jsx +++ b/src/app/demo/makana/members/Orders.jsx @@ -41,7 +41,7 @@ export const Orders = (props) => {
- + Shipping Summary @@ -53,26 +53,13 @@ export const Orders = (props) => { src='https://prod-useast-b.online.tableau.com/t/embeddingplaybook/views/superstore/ShipSummary' hideTabs={true} toolbar='hidden' - className=' - min-w-[309px] min-h-[240px] - sm:min-w-[486px] sm:min-h-[300px] - md:min-w-[600px] md:min-h-[400px] - lg:min-w-[240px] lg:min-h-[248px] - xl:min-w-[309px] xl:min-h-[226px] - 2xl:min-w-[400px] 2xl:min-h-[236px] - ' - layouts = {{ - 'xs': { 'device': 'default' }, - 'sm': { 'device': 'phone' }, - 'md': { 'device': 'default' }, - 'lg': { 'device': 'default' }, - 'xl': { 'device': 'tablet' }, - 'xl2': { 'device': 'desktop' } - }} + className='w-full h-[300px] sm:h-[400px] md:h-[500px] lg:h-[400px] xl:h-[450px] 2xl:h-[500px]' + width='100%' + height='100%' /> - + Shipping Trends @@ -84,22 +71,9 @@ export const Orders = (props) => { src='https://prod-useast-b.online.tableau.com/t/embeddingplaybook/views/superstore/ShippingTrend' hideTabs={true} toolbar='hidden' - className=' - min-w-[309px] min-h-[240px] - sm:min-w-[486px] sm:min-h-[300px] - md:min-w-[600px] md:min-h-[400px] - lg:min-w-[240px] lg:min-h-[248px] - xl:min-w-[309px] xl:min-h-[226px] - 2xl:min-w-[400px] 2xl:min-h-[236px] - ' - layouts = {{ - 'xs': { 'device': 'default' }, - 'sm': { 'device': 'phone' }, - 'md': { 'device': 'default' }, - 'lg': { 'device': 'default' }, - 'xl': { 'device': 'tablet' }, - 'xl2': { 'device': 'desktop' } - }} + className='w-full h-[300px] sm:h-[400px] md:h-[500px] lg:h-[400px] xl:h-[450px] 2xl:h-[500px]' + width='100%' + height='100%' /> diff --git a/src/app/demo/makana/mother/Products.jsx b/src/app/demo/makana/mother/Products.jsx index 2be4cd1c..ff907923 100644 --- a/src/app/demo/makana/mother/Products.jsx +++ b/src/app/demo/makana/mother/Products.jsx @@ -32,7 +32,7 @@ export const Products = (props) => { - + Segment Analysis @@ -42,33 +42,17 @@ export const Products = (props) => { - + Category Performance @@ -78,27 +62,11 @@ export const Products = (props) => { diff --git a/src/app/demo/superstore/Home.jsx b/src/app/demo/superstore/Home.jsx index 31f16962..3730c4d9 100644 --- a/src/app/demo/superstore/Home.jsx +++ b/src/app/demo/superstore/Home.jsx @@ -20,7 +20,7 @@ export const Home = () => { />
- + Overview Your personal digest of Superstore sales in North America @@ -30,22 +30,9 @@ export const Home = () => { src='https://prod-useast-b.online.tableau.com/t/embeddingplaybook/views/superstore/overview_800x800' hideTabs={true} toolbar='hidden' - className=' - min-w-[300px] min-h-[1430px] - sm:min-w-[510px] sm:min-h-[1430px] - md:min-w-[600px] md:min-h-[1080px] - lg:min-w-[400px] lg:min-h-[1440px] - xl:min-w-[720px] xl:min-h-[1180px] - 2xl:min-w-[860px] 2xl:min-h-[1180px] - ' - layouts = {{ - 'xs': { 'device': 'phone' }, - 'sm': { 'device': 'phone' }, - 'md': { 'device': 'default' }, - 'lg': { 'device': 'phone' }, - 'xl': { 'device': 'tablet' }, - 'xl2': { 'device': 'desktop' }, - }} + className='w-full h-[500px] sm:h-[600px] md:h-[700px] lg:h-[600px] xl:h-[650px] 2xl:h-[700px]' + width='100%' + height='100%' /> diff --git a/src/app/demo/superstore/orders/Orders.jsx b/src/app/demo/superstore/orders/Orders.jsx index bb1fa9f8..96b6210d 100644 --- a/src/app/demo/superstore/orders/Orders.jsx +++ b/src/app/demo/superstore/orders/Orders.jsx @@ -41,7 +41,7 @@ export const Orders = (props) => {
- + Shipping Summary @@ -53,26 +53,13 @@ export const Orders = (props) => { src='https://prod-useast-b.online.tableau.com/t/embeddingplaybook/views/superstore/ShipSummary' hideTabs={true} toolbar='hidden' - className=' - min-w-[309px] min-h-[240px] - sm:min-w-[486px] sm:min-h-[300px] - md:min-w-[600px] md:min-h-[400px] - lg:min-w-[240px] lg:min-h-[248px] - xl:min-w-[309px] xl:min-h-[226px] - 2xl:min-w-[400px] 2xl:min-h-[236px] - ' - layouts = {{ - 'xs': { 'device': 'default' }, - 'sm': { 'device': 'phone' }, - 'md': { 'device': 'default' }, - 'lg': { 'device': 'default' }, - 'xl': { 'device': 'tablet' }, - 'xl2': { 'device': 'desktop' } - }} + className='w-full h-[300px] sm:h-[400px] md:h-[500px] lg:h-[400px] xl:h-[450px] 2xl:h-[500px]' + width='100%' + height='100%' /> - + Shipping Trends @@ -84,22 +71,9 @@ export const Orders = (props) => { src='https://prod-useast-b.online.tableau.com/t/embeddingplaybook/views/superstore/ShippingTrend' hideTabs={true} toolbar='hidden' - className=' - min-w-[309px] min-h-[240px] - sm:min-w-[486px] sm:min-h-[300px] - md:min-w-[600px] md:min-h-[400px] - lg:min-w-[240px] lg:min-h-[248px] - xl:min-w-[309px] xl:min-h-[226px] - 2xl:min-w-[400px] 2xl:min-h-[236px] - ' - layouts = {{ - 'xs': { 'device': 'default' }, - 'sm': { 'device': 'phone' }, - 'md': { 'device': 'default' }, - 'lg': { 'device': 'default' }, - 'xl': { 'device': 'tablet' }, - 'xl2': { 'device': 'desktop' } - }} + className='w-full h-[300px] sm:h-[400px] md:h-[500px] lg:h-[400px] xl:h-[450px] 2xl:h-[500px]' + width='100%' + height='100%' /> diff --git a/src/app/demo/superstore/products/Products.jsx b/src/app/demo/superstore/products/Products.jsx index 2be4cd1c..ff907923 100644 --- a/src/app/demo/superstore/products/Products.jsx +++ b/src/app/demo/superstore/products/Products.jsx @@ -32,7 +32,7 @@ export const Products = (props) => { - + Segment Analysis @@ -42,33 +42,17 @@ export const Products = (props) => { - + Category Performance @@ -78,27 +62,11 @@ export const Products = (props) => { diff --git a/src/app/demo/veriforce/Home.jsx b/src/app/demo/veriforce/Home.jsx new file mode 100644 index 00000000..f85888bf --- /dev/null +++ b/src/app/demo/veriforce/Home.jsx @@ -0,0 +1,147 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui"; +import { Metrics, TableauEmbed } from '@/components'; +import Image from 'next/image'; +import { + Shield, + AlertTriangle, + TrendingUp, + XCircle, + Clock +} from 'lucide-react'; + +export const description = "Veriforce Contractor Risk Management - Comprehensive safety and compliance tracking dashboard with real-time alerts and self-service analytics"; + +export const Home = () => { + + return ( +
+
+ {/* Header Section */} +
+
+

+ Comprehensive safety and compliance tracking for your contractor network +

+
+
+
+
+ System Healthy +
+
+
+ + {/* Pulse Metrics */} + + + + {/* Main Dashboard */} +
+ {/* Compliance Overview */} +
+ + + + + Compliance Overview + + + Real-time compliance tracking across all contractors and safety metrics + + + + + + +
+ + {/* Quick Actions & Alerts */} +
+ {/* Critical Alerts */} + + + + + Critical Alerts + + + Issues requiring immediate attention + + + +
+ +
+

Safety Certification Expired

+

ABC Construction - 3 workers

+

2 days overdue

+
+
+
+ +
+

Training Due Soon

+

XYZ Electric - 12 workers

+

5 days remaining

+
+
+
+ +
+

Incident Report Pending

+

DEF Plumbing - Site A

+

1 day overdue

+
+
+
+
+ + {/* Quick Actions */} + + + + + Quick Actions + + + + + + + + + +
+
+
+
+ ); +}; diff --git a/src/app/demo/veriforce/README.md b/src/app/demo/veriforce/README.md new file mode 100644 index 00000000..bac86afc --- /dev/null +++ b/src/app/demo/veriforce/README.md @@ -0,0 +1,190 @@ +# Veriforce Contractor Risk Management Demo + +## Overview + +This demo showcases a comprehensive contractor risk management solution built with Tableau Embedded, designed to compete against PowerBI by highlighting superior self-service capabilities, intuitive navigation, and advanced analytics. + +## Key Differentiators vs PowerBI + +### 1. Superior Self-Service Capabilities +- **Templated Reports**: Pre-built report templates that users can customize +- **Version Control**: Track changes and maintain multiple versions of customized reports +- **File Library**: Central repository showing how to access and use different reports +- **Customization**: Users can modify templates to fit their specific needs + +### 2. Intuitive Navigation & Alerts +- **Direct Navigation**: Alerts link directly to relevant sections for issue resolution +- **Proactive Monitoring**: Surface problems before they escalate +- **Contextual Actions**: Quick actions based on current data and user role + +### 3. Advanced Visual Analytics +- **Interactive Dashboards**: Rich, interactive visualizations with drill-down capabilities +- **Real-time Updates**: Live data updates across all dashboards +- **Mobile Responsive**: Optimized for all device types + +## User Personas + +### 1. Safety Team Users +**Dashboard**: `/demo/veriforce/safety` +- View contractor safety compliance data +- Monitor safety metrics and incidents +- Track contractor certifications and training +- Generate safety reports and schedule training + +**Key Features**: +- Safety compliance overview with real-time tracking +- Incident tracking and trend analysis +- Certification management and renewal alerts +- Critical safety issues with direct action buttons + +### 2. Procurement Team Users +**Dashboard**: `/demo/veriforce/procurement` +- Assess contractor performance and risk +- View procurement-specific metrics +- Monitor vendor compliance status +- Analyze cost savings and vendor performance + +**Key Features**: +- Vendor performance overview with risk assessment +- Cost analysis and savings tracking +- Risk assessment matrix +- Procurement alerts and contract management + +### 3. Client Users (Management) +**Dashboard**: `/demo/veriforce/management` +- High-level overview of contractor data +- Consolidated compliance metrics across all contractors +- Actionable insights for decision-making +- Strategic planning and resource allocation + +**Key Features**: +- Executive dashboard with KPI metrics +- Portfolio overview and risk trends +- Financial impact analysis +- Strategic insights and recommendations + +## Core Functionality + +### Data Access & Exports +- **Client Access**: Full access to all contractor-supplied data +- **Compliance Tracking**: Identify contractors out of compliance +- **Export Capabilities**: Generate Excel and PDF exports +- **Real-time Data**: Live updates from connected data sources + +### Alerts & Navigation +- **Compliance Alerts**: Automated notifications for compliance issues +- **Direct Navigation**: Alerts link to specific dashboard sections +- **Proactive Monitoring**: Early warning system for potential issues +- **Alert Categories**: Safety, Compliance, and Procurement alerts + +### Self-Service Reports +- **Template Library**: Pre-built report templates for common use cases +- **Customization**: Modify templates to fit specific needs +- **Version Control**: Track changes and maintain report versions +- **Export Options**: Multiple format support (Excel, PDF, etc.) + +### Role-Based Access +- **Safety Team**: Access to safety dashboards and incident tracking +- **Procurement Team**: Vendor performance and cost analysis tools +- **Management**: Executive dashboards and strategic insights +- **Contractor Users**: Currently disabled (future feature showcase) + +## Technical Implementation + +### Dashboard Structure +``` +/demo/veriforce/ +├── config.js # Demo configuration and navigation +├── layout.jsx # Main layout wrapper +├── page.jsx # Home dashboard +├── Home.jsx # Home dashboard component +├── safety/ # Safety team dashboard +├── procurement/ # Procurement team dashboard +├── management/ # Executive dashboard +├── alerts/ # Compliance alerts system +├── reports/ # Self-service reports +├── agent/ # AI assistant +└── settings/ # System configuration +``` + +### Key Components +- **TableauEmbed**: Embedded Tableau visualizations +- **Metrics**: KPI cards with trend indicators +- **Alerts**: Interactive alert system with navigation +- **Reports**: Self-service report management +- **AI Assistant**: Natural language query interface + +### Branding +- **Primary Color**: #1E40AF (Veriforce Blue) +- **Secondary Color**: #3B82F6 +- **Accent Color**: #F59E0B +- **Success Color**: #10B981 +- **Warning Color**: #F59E0B +- **Danger Color**: #EF4444 + +## Features Showcase + +### 1. Compliance Tracking +- Real-time compliance monitoring across all contractors +- Visual indicators for compliance status +- Automated alerts for non-compliant contractors +- Direct navigation to resolution actions + +### 2. Risk Assessment +- Comprehensive risk scoring system +- Visual risk matrix for vendor evaluation +- Trend analysis and predictive insights +- Risk mitigation recommendations + +### 3. Self-Service Analytics +- Template-based report creation +- Custom dashboard building +- Interactive data exploration +- Export and sharing capabilities + +### 4. Mobile Responsiveness +- Optimized layouts for all device sizes +- Touch-friendly interface elements +- Responsive data visualizations +- Mobile-specific navigation patterns + +## Success Criteria Met + +✅ **Value for Safety Teams**: Clear safety compliance tracking and incident management +✅ **Value for Procurement Teams**: Comprehensive vendor performance and cost analysis +✅ **Actionable Insights**: Strategic recommendations and decision support +✅ **Self-Service Capabilities**: Exceeds PowerBI with templating and customization +✅ **Professional Branding**: Consistent Veriforce branding throughout +✅ **Intuitive UX**: User-friendly interface across all personas + +## Future Enhancements + +### Contractor Access (Planned) +- Limited dashboard access for contractor users +- Self-service compliance reporting +- Training and certification tracking +- Performance feedback system + +### Advanced Analytics +- Predictive risk modeling +- Machine learning insights +- Automated report generation +- Advanced data visualization + +### Integration Capabilities +- Third-party data source connections +- API integrations with existing systems +- Real-time data synchronization +- Custom workflow automation + +## Getting Started + +1. Navigate to `/demo/veriforce` to access the main dashboard +2. Use the navigation menu to explore different user personas +3. Try the AI assistant for natural language queries +4. Explore the self-service reports section +5. Configure alerts and settings as needed + +## Support + +For questions about this demo or Tableau Embedded capabilities, refer to the main documentation or contact the development team. diff --git a/src/app/demo/veriforce/agent/AgentDashboard.jsx b/src/app/demo/veriforce/agent/AgentDashboard.jsx new file mode 100644 index 00000000..d6091135 --- /dev/null +++ b/src/app/demo/veriforce/agent/AgentDashboard.jsx @@ -0,0 +1,267 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui"; +import { + BrainCircuit, + MessageSquare, + Lightbulb, + TrendingUp, + Shield, + Users, + AlertTriangle, + BarChart3, + FileText, + Download +} from 'lucide-react'; + +export const AgentDashboard = () => { + const sampleQuestions = [ + "Show me contractors with safety compliance issues", + "What's our overall risk score across all contractors?", + "Which contractors need certification renewals?", + "List all procurement metrics for this quarter", + "Show me the compliance dashboard for safety team", + "Generate a report on high-risk vendors", + "What are the trends in safety incidents?", + "Which contractors have the best performance scores?" + ]; + + const recentInsights = [ + { + id: 1, + type: "Safety", + title: "Safety Incident Trend Analysis", + description: "Incidents have decreased by 23% this quarter compared to last quarter", + confidence: "High", + action: "Continue current safety protocols" + }, + { + id: 2, + type: "Compliance", + title: "Certification Renewal Alert", + description: "47 contractors have certifications expiring in the next 30 days", + confidence: "High", + action: "Schedule renewal reminders" + }, + { + id: 3, + type: "Procurement", + title: "Vendor Performance Insight", + description: "ABC Construction shows 15% improvement in performance metrics", + confidence: "Medium", + action: "Consider expanding contract scope" + } + ]; + + return ( +
+
+ {/* Header Section */} +
+
+

+ + AI Assistant +

+

+ Get instant insights and answers about your contractor risk management data +

+
+
+
+
+ AI Active +
+
+
+ + {/* Chat Interface */} +
+ {/* Chat Area */} +
+ + + + + Chat with AI Assistant + + + Ask questions about your contractor data and get intelligent insights + + + + {/* Chat Messages */} +
+
+
+ +
+
+
+

+ Hello! I'm your AI assistant for contractor risk management. I can help you analyze compliance data, identify trends, and answer questions about your contractor network. What would you like to know? +

+
+
+
+ +
+
+
+

+ Show me contractors with safety compliance issues +

+
+
+
+ +
+
+ +
+
+ +
+
+
+

+ I found 23 contractors with safety compliance issues. Here's a breakdown: +

+
    +
  • • ABC Construction: 3 workers with expired certifications
  • +
  • • XYZ Electric: 12 workers need training renewal
  • +
  • • DEF Plumbing: 1 overdue incident report
  • +
+

+ Would you like me to generate a detailed report or show you the safety dashboard? +

+
+
+
+
+ + {/* Chat Input */} +
+ + +
+
+
+
+ + {/* Sidebar */} +
+ {/* Sample Questions */} + + + + + Sample Questions + + + Try asking these questions to get started + + + + {sampleQuestions.slice(0, 4).map((question, index) => ( + + ))} + + + + {/* Recent Insights */} + + + + + Recent Insights + + + AI-generated insights from your data + + + + {recentInsights.map((insight) => ( +
+
+ {insight.title} + + {insight.confidence} + +
+

{insight.description}

+

{insight.action}

+
+ ))} +
+
+
+
+ + {/* Quick Actions */} + + + + + Quick Actions + + + Common tasks and data exploration options + + + +
+ + + + +
+
+
+
+
+ ); +}; diff --git a/src/app/demo/veriforce/agent/page.jsx b/src/app/demo/veriforce/agent/page.jsx new file mode 100644 index 00000000..0089dbeb --- /dev/null +++ b/src/app/demo/veriforce/agent/page.jsx @@ -0,0 +1,22 @@ +import { Demo, FloatingAssistant } from '@/components'; + +import { AgentDashboard } from './AgentDashboard'; +import { settings } from '../config'; + +const Page = () => { + const pageName = 'Agent'; + + return ( + + + + + ) +} + +export default Page; diff --git a/src/app/demo/veriforce/alerts/AlertsDashboard.jsx b/src/app/demo/veriforce/alerts/AlertsDashboard.jsx new file mode 100644 index 00000000..98df473f --- /dev/null +++ b/src/app/demo/veriforce/alerts/AlertsDashboard.jsx @@ -0,0 +1,291 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui"; +import { TableauEmbed } from '@/components'; +import { + AlertTriangle, + CheckCircle, + XCircle, + Clock, + Filter, + Search, + Download, + Bell, + Shield, + Users, + DollarSign +} from 'lucide-react'; + +export const AlertsDashboard = () => { + const alertTypes = [ + { + title: "Critical Alerts", + value: "23", + color: "red", + icon: , + description: "Require immediate attention" + }, + { + title: "Warning Alerts", + value: "47", + color: "yellow", + icon: , + description: "Need attention soon" + }, + { + title: "Info Alerts", + value: "89", + color: "blue", + icon: , + description: "For your information" + }, + { + title: "Resolved Today", + value: "12", + color: "green", + icon: , + description: "Successfully addressed" + } + ]; + + const criticalAlerts = [ + { + id: 1, + type: "Safety", + title: "Safety Certification Expired", + contractor: "ABC Construction", + severity: "Critical", + time: "2 days overdue", + description: "3 workers have expired safety certifications", + action: "Renew certifications immediately" + }, + { + id: 2, + type: "Compliance", + title: "Incident Report Overdue", + contractor: "DEF Plumbing", + severity: "Critical", + time: "1 day overdue", + description: "Safety incident report not submitted", + action: "Submit incident report" + }, + { + id: 3, + type: "Procurement", + title: "Contract Expiring", + contractor: "XYZ Electric", + severity: "Critical", + time: "5 days remaining", + description: "$2.3M contract expires soon", + action: "Initiate renewal process" + } + ]; + + return ( +
+
+ {/* Header Section */} +
+
+

+ + Compliance Alerts +

+

+ Monitor and manage compliance alerts with direct navigation to resolution +

+
+
+ + + +
+
+ + {/* Alert Summary Metrics */} +
+ {alertTypes.map((alert, index) => ( + + +
+
+

{alert.title}

+

{alert.value}

+

{alert.description}

+
+
+ {alert.icon} +
+
+
+
+ ))} +
+ + {/* Main Alerts Dashboard */} +
+ {/* Alert Analytics */} + + + + + Alert Analytics & Trends + + + Visual analysis of alert patterns and resolution trends + + + + + + + + {/* Critical Alerts List */} + + + + + Critical Alerts + + + Issues requiring immediate attention with direct navigation + + + + {criticalAlerts.map((alert) => ( +
+
+
+
+ {alert.title} + + {alert.severity} + +
+

{alert.contractor}

+

{alert.description}

+

{alert.action}

+
+
+

{alert.time}

+ +
+
+
+ ))} +
+
+
+ + {/* Alert Categories */} +
+ + + + + Safety Alerts + + + Safety compliance and certification issues + + + +
+ Certification Expired + 5 +
+
+ Training Overdue + 12 +
+
+ Incident Reports + 3 +
+
+
+ + + + + + Compliance Alerts + + + General compliance and regulatory issues + + + +
+ Documentation Missing + 8 +
+
+ Audit Findings + 6 +
+
+ Policy Updates + 4 +
+
+
+ + + + + + Procurement Alerts + + + Contract and vendor management issues + + + +
+ Contract Expiring + 7 +
+
+ Performance Review + 9 +
+
+ Risk Assessment + 5 +
+
+
+
+
+
+ ); +}; diff --git a/src/app/demo/veriforce/alerts/page.jsx b/src/app/demo/veriforce/alerts/page.jsx new file mode 100644 index 00000000..90087b93 --- /dev/null +++ b/src/app/demo/veriforce/alerts/page.jsx @@ -0,0 +1,22 @@ +import { Demo, FloatingAssistant } from '@/components'; + +import { AlertsDashboard } from './AlertsDashboard'; +import { settings } from '../config'; + +const Page = () => { + const pageName = 'Alerts'; + + return ( + + + + + ) +} + +export default Page; diff --git a/src/app/demo/veriforce/auth/page.jsx b/src/app/demo/veriforce/auth/page.jsx new file mode 100644 index 00000000..285dab0e --- /dev/null +++ b/src/app/demo/veriforce/auth/page.jsx @@ -0,0 +1,13 @@ +import { Auth } from '@/components'; + +import { settings } from '../config'; + +const AuthPage = () => { + return ( + + ) +} + +export default AuthPage; diff --git a/src/app/demo/veriforce/config.js b/src/app/demo/veriforce/config.js new file mode 100644 index 00000000..9e97ce46 --- /dev/null +++ b/src/app/demo/veriforce/config.js @@ -0,0 +1,98 @@ +import { + Home, + Shield, + ShoppingCart, + Users2, + BrainCircuit, + AlertTriangle, + FileText, + Settings, + BarChart3, + TrendingUp +} from "lucide-react"; + +export const settings = { + app_id: 'veriforce', + app_name: 'Veriforce Contractor Risk Management', + app_logo: '/img/demos/Veriforce_contractor_2.svg', + auth_logo: '/img/themes/veriforce/veriforce-logo.jpeg', + base_path: '/demo/veriforce', + auth_hero: 'https://images.unsplash.com/photo-1581092160562-40aa08e78837?q=80&w=2948&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', + ai_chat: true, + ai_avatar: '/img/themes/veriforce/veriforce-logo.jpeg', + sample_questions: [ + "Show me contractors with safety compliance issues", + "What's our overall risk score across all contractors?", + "Which contractors need certification renewals?", + "List all procurement metrics for this quarter", + "Show me the compliance dashboard for safety team" + ], + sections: [ + { + name: 'Home', + icon: , + path: '', + min_role: 0, + description: 'Contractor risk management overview' + }, + // { + // name: 'Agent', + // icon: , + // path: '/agent', + // min_role: 0, + // description: 'AI assistant for contractor risk insights' + // }, + { + name: 'Safety Team', + icon: , + path: '/safety', + min_role: 0, + description: 'Safety compliance tracking and incident monitoring' + }, + { + name: 'Procurement', + icon: , + path: '/procurement', + min_role: 1, + description: 'Contractor performance and vendor compliance assessment' + }, + { + name: 'Management', + icon: , + path: '/management', + min_role: 2, + description: 'Executive dashboard with consolidated contractor insights' + }, + { + name: 'Alerts', + icon: , + path: '/alerts', + min_role: 0, + description: 'Compliance alerts and issue tracking' + }, + { + name: 'Reports', + icon: , + path: '/reports', + min_role: 0, + description: 'Self-service reports and templates' + }, + { + name: 'Settings', + icon: , + path: '/settings', + min_role: 2, + description: 'System configuration and user management' + } + ], + branding: { + primary_color: '#1E40AF', // Veriforce blue + secondary_color: '#3B82F6', + accent_color: '#F59E0B', + success_color: '#10B981', + warning_color: '#F59E0B', + danger_color: '#EF4444', + background_color: '#F8FAFC', + text_color: '#1F2937' + } +} diff --git a/src/app/demo/veriforce/layout.jsx b/src/app/demo/veriforce/layout.jsx new file mode 100644 index 00000000..909433d9 --- /dev/null +++ b/src/app/demo/veriforce/layout.jsx @@ -0,0 +1,27 @@ +import { ThemeProvider } from 'next-themes'; + +import { AuthGuard, LanggraphAgentRuntimeProvider } from '@/components'; +import { settings } from './config'; + +export const metadata = { + title: settings.app_name, + description: 'Veriforce Contractor Risk Management - Comprehensive safety and compliance tracking for contractors', +}; + +export default function VeriforceLayout({ children }) { + return ( + + + + {children} + + + ); +} diff --git a/src/app/demo/veriforce/management/ManagementDashboard.jsx b/src/app/demo/veriforce/management/ManagementDashboard.jsx new file mode 100644 index 00000000..fd7bfe65 --- /dev/null +++ b/src/app/demo/veriforce/management/ManagementDashboard.jsx @@ -0,0 +1,203 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui"; +import { TableauEmbed } from '@/components'; +import { + Users2, + TrendingUp, + DollarSign, + Shield, + CheckCircle, + XCircle, + Clock, + FileText, + Download, + Filter, + Search, + BarChart3, + Target, + AlertTriangle, + PieChart +} from 'lucide-react'; + +export const ManagementDashboard = () => { + + return ( +
+
+ {/* Header Section */} +
+
+

+ + Executive Dashboard +

+

+ High-level overview of contractor risk management performance and strategic insights +

+
+
+ + + +
+
+ + + {/* Strategic Overview */} +
+ {/* Contractor Portfolio Overview */} + + + + + Contractor Portfolio Overview + + + Strategic view of contractor distribution, performance, and risk levels + + + + + + + + {/* Risk & Compliance Trends */} + + + + + Risk & Compliance Trends + + + Historical trends and predictive insights for risk management + + + + + + +
+ + {/* Financial Impact Analysis */} + + + + + Financial Impact Analysis + + + Cost savings, risk mitigation value, and ROI from contractor risk management initiatives + + + + + + + + {/* Strategic Insights & Actions */} +
+ + + + + Strategic Insights + + + Key insights and recommendations for executive decision-making + + + +
+

Top Performing Contractors

+

ABC Construction, XYZ Electric, and DEF Plumbing show consistently high compliance rates and low risk scores.

+

Recommendation: Consider expanding contracts with these vendors.

+
+
+

Cost Savings Opportunity

+

Consolidating similar contractor categories could reduce costs by an estimated $180K annually.

+

Recommendation: Review contractor portfolio for consolidation opportunities.

+
+
+

Risk Mitigation

+

23 contractors require immediate attention due to compliance issues or high risk scores.

+

Recommendation: Implement targeted improvement plans for at-risk contractors.

+
+
+
+ + + + + + Executive Actions + + + + + + + + + + +
+
+
+ ); +}; diff --git a/src/app/demo/veriforce/management/page.jsx b/src/app/demo/veriforce/management/page.jsx new file mode 100644 index 00000000..33e83904 --- /dev/null +++ b/src/app/demo/veriforce/management/page.jsx @@ -0,0 +1,22 @@ +import { Demo, FloatingAssistant } from '@/components'; + +import { ManagementDashboard } from './ManagementDashboard'; +import { settings } from '../config'; + +const Page = () => { + const pageName = 'Management'; + + return ( + + + + + ) +} + +export default Page; diff --git a/src/app/demo/veriforce/page.jsx b/src/app/demo/veriforce/page.jsx new file mode 100644 index 00000000..7b11dcb9 --- /dev/null +++ b/src/app/demo/veriforce/page.jsx @@ -0,0 +1,23 @@ +import { Demo, FloatingAssistant } from '@/components'; + +import { Home } from './Home'; +import { settings } from './config'; + +const Page = () => { + // for the most part, only the pageName and child components for should be modified to make new pages + const pageName = ''; + + return ( + + + + + ) +} + +export default Page; diff --git a/src/app/demo/veriforce/procurement/ProcurementDashboard.jsx b/src/app/demo/veriforce/procurement/ProcurementDashboard.jsx new file mode 100644 index 00000000..7831bcc9 --- /dev/null +++ b/src/app/demo/veriforce/procurement/ProcurementDashboard.jsx @@ -0,0 +1,207 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui"; +import { TableauEmbed } from '@/components'; +import { + ShoppingCart, + TrendingUp, + DollarSign, + Users, + CheckCircle, + XCircle, + Clock, + FileText, + Download, + Filter, + Search, + BarChart3, + Target, + AlertTriangle +} from 'lucide-react'; + +export const ProcurementDashboard = () => { + + return ( +
+
+ {/* Header Section */} +
+
+

+ + Procurement Dashboard +

+

+ Assess contractor performance, manage vendor relationships, and optimize procurement decisions +

+
+
+ + + +
+
+ + + {/* Main Dashboard Grid */} +
+ {/* Vendor Performance Overview */} + + + + + Vendor Performance Overview + + + Comprehensive vendor performance metrics and risk assessment + + + + + + + + {/* Cost Analysis & Savings */} + + + + + Cost Analysis & Savings + + + Track procurement costs, savings opportunities, and budget performance + + + + + + +
+ + {/* Risk Assessment Matrix */} + + + + + Vendor Risk Assessment Matrix + + + Comprehensive risk evaluation across all vendor categories and performance indicators + + + + + + + + {/* Action Items & Quick Actions */} +
+ + + + + Procurement Alerts + + + Critical issues requiring procurement team attention + + + +
+ +
+

Contract Expiring Soon

+

ABC Construction - $2.3M contract

+

15 days remaining - Renewal needed

+
+
+
+ +
+

Performance Review Due

+

XYZ Electric - Q4 review

+

7 days remaining

+
+
+
+ +
+

High Risk Vendor

+

DEF Plumbing - Risk score 4.2

+

Review required

+
+
+
+
+ + + + + + Quick Actions + + + + + + + + + +
+
+
+ ); +}; diff --git a/src/app/demo/veriforce/procurement/page.jsx b/src/app/demo/veriforce/procurement/page.jsx new file mode 100644 index 00000000..a261eac9 --- /dev/null +++ b/src/app/demo/veriforce/procurement/page.jsx @@ -0,0 +1,22 @@ +import { Demo, FloatingAssistant } from '@/components'; + +import { ProcurementDashboard } from './ProcurementDashboard'; +import { settings } from '../config'; + +const Page = () => { + const pageName = 'Procurement'; + + return ( + + + + + ) +} + +export default Page; diff --git a/src/app/demo/veriforce/reports/ReportsDashboard.jsx b/src/app/demo/veriforce/reports/ReportsDashboard.jsx new file mode 100644 index 00000000..a20efc12 --- /dev/null +++ b/src/app/demo/veriforce/reports/ReportsDashboard.jsx @@ -0,0 +1,341 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui"; +import { TableauEmbed } from '@/components'; +import { + FileText, + Download, + Plus, + Edit, + Copy, + Trash2, + Eye, + Star, + Clock, + Users, + BarChart3, + PieChart, + TrendingUp, + Filter, + Search +} from 'lucide-react'; + +export const ReportsDashboard = () => { + const reportTemplates = [ + { + id: 1, + name: "Compliance Summary Report", + description: "High-level compliance overview across all contractors", + type: "Executive", + lastModified: "2 hours ago", + createdBy: "Sarah Johnson", + isTemplate: true, + isStarred: true + }, + { + id: 2, + name: "Safety Incident Analysis", + description: "Detailed analysis of safety incidents and trends", + type: "Safety", + lastModified: "1 day ago", + createdBy: "Mike Chen", + isTemplate: true, + isStarred: false + }, + { + id: 3, + name: "Vendor Performance Review", + description: "Comprehensive vendor performance metrics and scoring", + type: "Procurement", + lastModified: "3 days ago", + createdBy: "Lisa Rodriguez", + isTemplate: true, + isStarred: true + }, + { + id: 4, + name: "Q4 Risk Assessment - Custom", + description: "Customized risk assessment for Q4 contractor review", + type: "Management", + lastModified: "1 week ago", + createdBy: "You", + isTemplate: false, + isStarred: false + }, + { + id: 5, + name: "Training Completion Report", + description: "Worker training completion status and certification tracking", + type: "Safety", + lastModified: "2 weeks ago", + createdBy: "David Kim", + isTemplate: true, + isStarred: false + } + ]; + + const recentReports = [ + { + id: 1, + name: "Monthly Compliance Dashboard", + type: "Executive", + generated: "Today", + status: "Ready" + }, + { + id: 2, + name: "Safety Metrics Q3", + type: "Safety", + generated: "Yesterday", + status: "Ready" + }, + { + id: 3, + name: "Vendor Risk Analysis", + type: "Procurement", + generated: "2 days ago", + status: "Processing" + } + ]; + + return ( +
+
+ {/* Header Section */} +
+
+

+ + Self-Service Reports +

+

+ Create, customize, and manage reports with powerful self-service capabilities +

+
+
+ + + +
+
+ + {/* Quick Stats */} +
+ + +
+
+

Total Reports

+

47

+
+ +
+
+
+ + +
+
+

Templates

+

12

+
+ +
+
+
+ + +
+
+

Custom Reports

+

35

+
+ +
+
+
+ + +
+
+

Generated Today

+

8

+
+ +
+
+
+
+ + {/* Report Templates */} + + + + + Report Templates + + + Pre-built report templates to get you started quickly + + + +
+ {reportTemplates.map((template) => ( +
+
+
+

{template.name}

+ {template.isStarred && } +
+
+ + +
+
+

{template.description}

+
+ {template.type} + {template.lastModified} +
+
+ + +
+
+ ))} +
+
+
+ + {/* Recent Reports */} +
+ + + + + Recent Reports + + + Your recently generated and scheduled reports + + + + {recentReports.map((report) => ( +
+
+
+ +
+
+

{report.name}

+

{report.type} • {report.generated}

+
+
+
+ + {report.status} + + +
+
+ ))} +
+
+ + + + + + Report Analytics + + + Usage patterns and popular report types + + + + + + +
+ + {/* Self-Service Features */} + + + + + Self-Service Capabilities + + + Powerful features that differentiate from PowerBI + + + +
+
+
+ +
+

Custom Templates

+

Create and customize report templates to fit your specific needs

+
+
+
+ +
+

Version Control

+

Track changes and maintain multiple versions of your reports

+
+
+
+ +
+

Interactive Analytics

+

Drill down into data with interactive visualizations and filters

+
+
+
+ +
+

Export Options

+

Export to Excel, PDF, and other formats with one click

+
+
+
+
+
+
+ ); +}; diff --git a/src/app/demo/veriforce/reports/page.jsx b/src/app/demo/veriforce/reports/page.jsx new file mode 100644 index 00000000..203d11a3 --- /dev/null +++ b/src/app/demo/veriforce/reports/page.jsx @@ -0,0 +1,22 @@ +import { Demo, FloatingAssistant } from '@/components'; + +import { ReportsDashboard } from './ReportsDashboard'; +import { settings } from '../config'; + +const Page = () => { + const pageName = 'Reports'; + + return ( + + + + + ) +} + +export default Page; diff --git a/src/app/demo/veriforce/safety/SafetyDashboard.jsx b/src/app/demo/veriforce/safety/SafetyDashboard.jsx new file mode 100644 index 00000000..0ef0528b --- /dev/null +++ b/src/app/demo/veriforce/safety/SafetyDashboard.jsx @@ -0,0 +1,286 @@ +"use client"; + +import { useState, useCallback, useEffect } from 'react'; +import { useTableauSession } from '../../../../hooks/useTableauSession'; +import { useSearchParams } from 'next/navigation'; +import { TableauNavigation } from '../../../../components/TableauNavigation/TableauNavigation'; +import { TableauEmbed } from '../../../../components/TableauEmbed'; +import { DynamicDashboardViewer } from '../../../../components/TableauNavigation/DynamicDashboardViewer'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '../../../../components/ui/Card'; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '../../../../components/ui/Tabs'; +import { Button } from '../../../../components/ui/Button'; +import { Avatar, AvatarFallback, AvatarImage } from '../../../../components/ui/Avatar'; +import { Badge } from '../../../../components/ui/Badge'; +import { + Search, + Menu, + X +} from 'lucide-react'; + +export const SafetyDashboard = () => { + const { + status: sessionStatus, + data: user, + isSuccess: isSessionSuccess, + isLoading: isSessionLoading + } = useTableauSession(); + + const searchParams = useSearchParams(); + const [selectedDashboard, setSelectedDashboard] = useState(null); + const [showNavigation, setShowNavigation] = useState(true); // Show navigation by default + + // Get Tableau session data - these will be available after SSO + const restToken = user?.rest_key; + const embedToken = user?.embed_token; + const siteId = user?.site_id; + + // Load dashboard from URL parameters if available + useEffect(() => { + if (searchParams) { + const dashboardId = searchParams.get('dashboardId'); + const dashboardName = searchParams.get('dashboardName'); + const workbookName = searchParams.get('workbookName'); + const contentUrl = searchParams.get('contentUrl'); + + if (dashboardId && dashboardName && workbookName && contentUrl) { + setSelectedDashboard({ + id: dashboardId, + name: dashboardName, + workbookName: workbookName, + contentUrl: decodeURIComponent(contentUrl) + }); + } + } + }, [searchParams]); + + const handleDashboardSelect = useCallback((dashboard) => { + setSelectedDashboard(dashboard); + }, []); + + return ( +
+ {/* Navigation Sidebar - ALWAYS rendered, isolated from main content */} + {showNavigation && ( +
+ +
+ )} + + {/* Main Content - Isolated container */} + +
+ ); +}; + +// Separate component to isolate Tableau embeds from navigation +const MainContent = ({ selectedDashboard, embedToken, siteId, showNavigation, setShowNavigation }) => { + return ( +
+ {/* Header with Navigation Toggle */} +
+
+
+ +
+

Safety Dashboard

+

Monitor safety compliance and incidents

+
+
+
+ + Safety Department + +
+
+
+ + {/* Main Dashboard Area */} +
+ {selectedDashboard ? ( + + ) : ( +
+ {/* Main Visualizations - 2/3 width on large screens */} +
+ {/* Safety Overview */} + + +
+
+ Safety Overview + Regional safety metrics and compliance status +
+ +
+
+ + + +
+ + {/* Incident Tracking */} + {/* + +
+
+ Incident Tracking + Monthly incident reports and trends +
+ + + All + Critical + Resolved + + +
+
+ + + +
*/} +
+ + {/* Sidebar - 1/3 width on large screens */} +
+ {/* Safety Score Card */} + + + Safety Score + Current compliance rating + + +
+
+ + + + +
+ 87% + Good +
+
+
+
+
+ Training Compliance + 92% +
+
+
+
+
+ Equipment Inspections + 78% +
+
+
+
+
+ Incident Resolution + 91% +
+
+
+
+
+
+
+ + {/* Quick Actions */} + + + Quick Actions + Common safety tasks + + + + + + + + +
+
+ )} +
+
+ ); +}; diff --git a/src/app/demo/veriforce/safety/page.jsx b/src/app/demo/veriforce/safety/page.jsx new file mode 100644 index 00000000..c7dae9a5 --- /dev/null +++ b/src/app/demo/veriforce/safety/page.jsx @@ -0,0 +1,22 @@ +import { Demo, FloatingAssistant } from '@/components'; + +import { SafetyDashboard } from './SafetyDashboard'; +import { settings } from '../config'; + +const Page = () => { + const pageName = 'Safety Team'; + + return ( + + + + + ) +} + +export default Page; diff --git a/src/app/demo/veriforce/settings/SettingsDashboard.jsx b/src/app/demo/veriforce/settings/SettingsDashboard.jsx new file mode 100644 index 00000000..b30c2a18 --- /dev/null +++ b/src/app/demo/veriforce/settings/SettingsDashboard.jsx @@ -0,0 +1,291 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui"; +import { + Settings, + Users, + Bell, + Shield, + Database, + Palette, + Download, + Upload, + Save, + RefreshCw +} from 'lucide-react'; + +export const SettingsDashboard = () => { + return ( +
+
+ {/* Header Section */} +
+
+

+ + System Settings +

+

+ Configure system settings, user management, and integration preferences +

+
+
+ + {/* Settings Categories */} +
+ {/* User Management */} + + + + + User Management + + + Manage users, roles, and permissions + + + +
+
+

Active Users

+

247 users across all teams

+
+ +
+
+
+

Role Assignments

+

Configure team-specific access levels

+
+ +
+
+
+

Contractor Access

+

Currently disabled - future feature

+
+ +
+
+
+ + {/* Alert Configuration */} + + + + + Alert Configuration + + + Configure compliance alerts and notifications + + + +
+
+

Critical Alerts

+

Immediate notification for urgent issues

+
+ +
+
+
+

Email Notifications

+

Set up automated email alerts

+
+ +
+
+
+

Alert Thresholds

+

Customize alert trigger conditions

+
+ +
+
+
+ + {/* Security Settings */} + + + + + Security Settings + + + Configure security policies and access controls + + + +
+
+

Data Encryption

+

AES-256 encryption enabled

+
+ + Active + +
+
+
+

Session Timeout

+

8 hours of inactivity

+
+ +
+
+
+

Audit Logging

+

Comprehensive activity tracking

+
+ +
+
+
+ + {/* Data Management */} + + + + + Data Management + + + Manage data sources and integration settings + + + +
+
+

Data Sources

+

12 connected data sources

+
+ +
+
+
+

Data Refresh

+

Every 4 hours

+
+ +
+
+
+

Backup Status

+

Last backup: 2 hours ago

+
+ +
+
+
+
+ + {/* Branding & Customization */} + + + + + Branding & Customization + + + Customize the appearance and branding of your Veriforce instance + + + +
+
+
+
+

Company Logo

+

Upload your company logo

+
+ +
+
+
+

Color Scheme

+

Customize primary and accent colors

+
+ +
+
+
+
+
+

Dashboard Layout

+

Configure default dashboard layout

+
+ +
+
+
+

Export Settings

+

Default export formats and options

+
+ +
+
+
+
+
+ + {/* System Actions */} +
+
+

System Actions

+

Perform system-wide operations and maintenance

+
+
+ + + +
+
+
+
+ ); +}; diff --git a/src/app/demo/veriforce/settings/page.jsx b/src/app/demo/veriforce/settings/page.jsx new file mode 100644 index 00000000..2eed524b --- /dev/null +++ b/src/app/demo/veriforce/settings/page.jsx @@ -0,0 +1,22 @@ +import { Demo, FloatingAssistant } from '@/components'; + +import { SettingsDashboard } from './SettingsDashboard'; +import { settings } from '../config'; + +const Page = () => { + const pageName = 'Settings'; + + return ( + + + + + ) +} + +export default Page; diff --git a/src/app/demo/veriforce/test/page.jsx b/src/app/demo/veriforce/test/page.jsx new file mode 100644 index 00000000..2511cad4 --- /dev/null +++ b/src/app/demo/veriforce/test/page.jsx @@ -0,0 +1,82 @@ +"use client"; +import { useTableauSession } from '@/hooks'; + +export default function TestPage() { + const { + status: sessionStatus, + data: user, + error: sessionError, + isSuccess: isSessionSuccess, + isError: isSessionError, + isLoading: isSessionLoading + } = useTableauSession(); + + return ( +
+
+

JWT Token Test

+ +
+

Session Status

+
+

Status: {sessionStatus}

+

Loading: {isSessionLoading ? 'Yes' : 'No'}

+

Success: {isSessionSuccess ? 'Yes' : 'No'}

+

Error: {isSessionError ? 'Yes' : 'No'}

+
+
+ + {user && ( +
+

User Data

+
+

Name: {user.name}

+

Email: {user.email}

+

Demo: {user.demo}

+

Has REST Key: {user.rest_key ? 'Yes' : 'No'}

+

Has Embed Token: {user.embed_token ? 'Yes' : 'No'}

+

Site ID: {user.site_id || 'Not available'}

+

Site: {user.site || 'Not available'}

+

User ID: {user.user_id || 'Not available'}

+
+
+ )} + + {user?.rest_key && ( +
+

REST Token (First 50 chars)

+

+ {user.rest_key.substring(0, 50)}... +

+
+ )} + + {user?.embed_token && ( +
+

Embed Token (First 50 chars)

+

+ {user.embed_token.substring(0, 50)}... +

+
+ )} + + {sessionError && ( +
+

Error

+

{sessionError.message}

+
+ )} + +
+

Instructions

+
+

1. Make sure you're signed in via the auth page

+

2. Check if the JWT tokens are present above

+

3. If tokens are missing, the authentication flow needs to be fixed

+

4. If tokens are present, we can use them to fetch dashboards

+
+
+
+
+ ); +} diff --git a/src/components/Demo/components/Auth/Auth.jsx b/src/components/Demo/components/Auth/Auth.jsx index d35ba262..ec3fa79e 100644 --- a/src/components/Demo/components/Auth/Auth.jsx +++ b/src/components/Demo/components/Auth/Auth.jsx @@ -14,7 +14,7 @@ export const description = "A login page with a full-screen background image and export const Auth = (props) => { const { settings } = props; - const { app_id, base_path, app_name, app_logo, auth_hero } = settings; + const { app_id, base_path, app_name, app_logo, auth_logo, auth_hero } = settings; const demoManager = new UserModel(); const users = demoManager.getUsersForDemo(app_id); @@ -40,7 +40,7 @@ export const Auth = (props) => {
- + APP
diff --git a/src/components/Demo/components/Demo.jsx b/src/components/Demo/components/Demo.jsx index 71f0833b..c97fe2e2 100644 --- a/src/components/Demo/components/Demo.jsx +++ b/src/components/Demo/components/Demo.jsx @@ -7,6 +7,7 @@ export const Demo = (props) => { base_path, app_name, app_logo, + auth_logo, ai_chat, ai_avatar, sections, @@ -25,12 +26,13 @@ export const Demo = (props) => { } return ( -
+
{ const { base_path, - crumbs + crumbs, + app_logo } = props; const breadcrumbItems = generateBreadcrumbs(base_path, crumbs); @@ -25,8 +27,18 @@ export const Breadcrumbs = (props) => { - - {item.title} + + {index === 0 && app_logo ? ( + {item.title} + ) : ( + item.title + )} diff --git a/src/components/Demo/components/Navigation/Navigation.jsx b/src/components/Demo/components/Navigation/Navigation.jsx index 60c2f99e..5042de31 100644 --- a/src/components/Demo/components/Navigation/Navigation.jsx +++ b/src/components/Demo/components/Navigation/Navigation.jsx @@ -25,6 +25,7 @@ export const Navigation = (props) => {
{ crumbs, app_name, app_logo, + auth_logo, ai_chat, ai_avatar, sections, @@ -35,7 +36,7 @@ export const NavigationMd = (props) => { className="group flex h-9 w-9 shrink-0 items-center justify-center gap-2 rounded-full bg-logoBackground text-lg font-semibold text-primary-foreground md:h-8 md:w-8 md:text-base" > - + APP {app_name} @@ -74,7 +75,7 @@ const NavBarItem = (props) => { {icon} {name} diff --git a/src/components/Demo/components/Navigation/NavigationSm.jsx b/src/components/Demo/components/Navigation/NavigationSm.jsx index 6926640a..a9a60aec 100644 --- a/src/components/Demo/components/Navigation/NavigationSm.jsx +++ b/src/components/Demo/components/Navigation/NavigationSm.jsx @@ -20,6 +20,7 @@ export const NavigationSm = (props) => { crumbs, app_name, app_logo, + auth_logo, ai_chat, ai_avatar, sections, @@ -39,11 +40,11 @@ export const NavigationSm = (props) => { href='/demos' className="flex items-center gap-4 px-2.5 hover:text-foreground" > - - + + APP - {app_name} + {app_name} {sections ? sections.map((section) => { diff --git a/src/components/Demo/components/Navigation/UserMenu.jsx b/src/components/Demo/components/Navigation/UserMenu.jsx index e8fd8ef9..e89a2188 100644 --- a/src/components/Demo/components/Navigation/UserMenu.jsx +++ b/src/components/Demo/components/Navigation/UserMenu.jsx @@ -40,28 +40,25 @@ export function UserMenu(props) { console.debug('Session Error:', sessionError); } + // Debug logging + console.debug('UserMenu Debug:', { + sessionStatus, + isSessionSuccess, + isSessionError, + isSessionLoading, + user: user ? { name: user.name, email: user.email, picture: user.picture } : null + }); + return (
- {isSessionError || isSessionLoading ? - - - - - - : null} - {isSessionSuccess ? - - - - - - : null} + + + + +
) } @@ -70,11 +67,14 @@ export function UserMenu(props) { const Logout = (props) => { const { status } = props; - if (status === 'success') { + // Show logout for any non-error status + if (status === 'success' || status === 'pending' || status === 'idle') { return ( await signOut('credentials', { redirect: false })} + onClick={async () => { + await signOut({ redirect: true, callbackUrl: '/demo/veriforce/auth' }); + }} > + + + + + +
+
+
+ + {/* Dashboard Content */} +
+
+ +
+
+ + {/* Share Modal */} + + + + Share Dashboard + + Share a direct link to this dashboard with others. + + +
+
+ + +
+
+ + + +
+
+
+ ); +}; diff --git a/src/components/TableauNavigation/TableauNavigation.jsx b/src/components/TableauNavigation/TableauNavigation.jsx new file mode 100644 index 00000000..63e4034d --- /dev/null +++ b/src/components/TableauNavigation/TableauNavigation.jsx @@ -0,0 +1,345 @@ +"use client"; +import { useState, useRef, useEffect, useCallback, memo } from 'react'; +import { + Folder, + FileText, + ChevronRight, + ChevronDown, + Search, + Grid, + List, + RefreshCw +} from 'lucide-react'; +import { useTableauSession } from '../../hooks'; +import { useTableauDashboards } from '../../hooks/tableauHooks'; + +const TableauNavigationComponent = ({ onDashboardSelect, selectedDashboard }) => { + const { + status: sessionStatus, + data: user, + error: sessionError, + isSuccess: isSessionSuccess, + isError: isSessionError, + isLoading: isSessionLoading + } = useTableauSession(); + + // Get Tableau session data + const restToken = user?.rest_key; + const embedToken = user?.embed_token; + const siteId = user?.site_id; + const isAuthenticated = isSessionSuccess && embedToken && siteId; + + const { + dashboardsByFolder, + allViews, + isLoading, + refreshDashboards, + restApiData, + restApiLoading, + restApiError + } = useTableauDashboards(); + + const [searchTerm, setSearchTerm] = useState(''); + const [expandedFolders, setExpandedFolders] = useState({}); + const [viewMode, setViewMode] = useState('grid'); // 'grid' or 'list' + const [isSearchFocused, setIsSearchFocused] = useState(false); + const searchInputRef = useRef(null); + const preventBlurRef = useRef(false); + + // Memoized event handlers to prevent re-renders + const handleInputFocus = useCallback((e) => { + console.log('INPUT FOCUSED'); + e.stopPropagation(); + setIsSearchFocused(true); + }, []); + + const handleInputBlur = useCallback((e) => { + console.log('INPUT BLURRED'); + if (preventBlurRef.current) { + e.preventDefault(); + e.stopPropagation(); + return; + } + setIsSearchFocused(false); + }, []); + + const handleInputClick = useCallback((e) => { + console.log('INPUT CLICKED'); + e.stopPropagation(); + preventBlurRef.current = true; + setIsSearchFocused(true); + }, []); + + const handleInputMouseDown = useCallback((e) => { + e.stopPropagation(); + // Don't prevent default - allow focus + }, []); + + // Maintain focus when search is focused + useEffect(() => { + if (isSearchFocused && searchInputRef.current) { + // Use a timeout to ensure focus happens after any re-renders + const timeoutId = setTimeout(() => { + if (searchInputRef.current) { + searchInputRef.current.focus(); + } + }, 0); + return () => clearTimeout(timeoutId); + } + }, [isSearchFocused]); + + // Use real Tableau data + const dashboards = dashboardsByFolder; + const views = allViews; + + // Debug logging removed to prevent re-renders + + const toggleFolder = (folderName) => { + setExpandedFolders(prev => ({ + ...prev, + [folderName]: !prev[folderName] + })); + }; + + const filteredDashboards = Object.entries(dashboards).filter(([folderName, workbooks]) => { + if (!searchTerm) return true; + return workbooks.some(workbook => + workbook.name.toLowerCase().includes(searchTerm.toLowerCase()) || + workbook.views.some(view => + view.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + ); + }); + + const filteredViews = views.filter(view => + view.name.toLowerCase().includes(searchTerm.toLowerCase()) || + view.workbookName.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + // Show loading state while session is being established or tokens are loading + if (isSessionLoading || (isSessionSuccess && !restToken)) { + return ( +
+
+
+

Loading Dashboards...

+

Preparing your Tableau content

+
+
+ ); + } + + // Show error state if there's a session error + if (isSessionError) { + return ( +
+
+

Unable to Load Dashboards

+

There was an error connecting to Tableau Cloud

+
+ Please refresh the page or contact support if the issue persists +
+
+
+ ); + } + + // If not authenticated, show a simple message (user should be authenticated via SSO) + if (!isAuthenticated) { + return ( +
+
+

No Dashboards Available

+

No Tableau dashboards found for your account

+
+ Contact your administrator if you believe this is an error +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+

My Dashboards

+

{user?.name || user?.email}

+
+
+ +
+
+ + {/* Search */} +
e.preventDefault()} onClick={(e) => e.stopPropagation()}> + + setSearchTerm(e.target.value)} + onFocus={handleInputFocus} + onBlur={handleInputBlur} + onClick={handleInputClick} + onMouseDown={handleInputMouseDown} + className="w-full pl-10 pr-4 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ + {/* View Mode Toggle */} +
+ + +
+
+ + {/* Content */} +
+ {isLoading ? ( +
+ + Loading dashboards... +
+ ) : searchTerm ? ( + // Search Results from REST API data +
+ {restApiData?.workbooks ? ( + <> +
+ {restApiData.workbooks.filter(dashboard => + dashboard.dashboard_name.toLowerCase().includes(searchTerm.toLowerCase()) || + dashboard.workbook_name.toLowerCase().includes(searchTerm.toLowerCase()) + ).length} result{restApiData.workbooks.filter(dashboard => + dashboard.dashboard_name.toLowerCase().includes(searchTerm.toLowerCase()) || + dashboard.workbook_name.toLowerCase().includes(searchTerm.toLowerCase()) + ).length !== 1 ? 's' : ''} +
+
+ {restApiData.workbooks + .filter(dashboard => + dashboard.dashboard_name.toLowerCase().includes(searchTerm.toLowerCase()) || + dashboard.workbook_name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .map((dashboard) => ( +
onDashboardSelect({ + id: dashboard.dashboard_id, + name: dashboard.dashboard_name, + workbookName: dashboard.workbook_name, + contentUrl: dashboard.content_url + })} + className={`p-3 rounded-lg cursor-pointer transition-colors mb-1 ${ + selectedDashboard?.id === dashboard.dashboard_id + ? 'bg-blue-600 text-white' + : 'hover:bg-slate-700 text-slate-300' + }`} + > +
+ +
+
{dashboard.dashboard_name}
+
{dashboard.workbook_name}
+
+
+
+ ))} +
+ + ) : ( +
+

No search results available

+
+ )} +
+ ) : ( + // Folder View +
+ {/* Show REST API dashboards as the primary content */} + {restApiData?.workbooks && restApiData.workbooks.length > 0 ? ( +
+
+ Available Dashboards ({restApiData.workbooks.filter(dashboard => + dashboard.dashboard_name.toLowerCase().includes(searchTerm.toLowerCase()) || + dashboard.workbook_name.toLowerCase().includes(searchTerm.toLowerCase()) + ).length}) +
+
+ {restApiData.workbooks + .filter(dashboard => + dashboard.dashboard_name.toLowerCase().includes(searchTerm.toLowerCase()) || + dashboard.workbook_name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .map((dashboard) => ( +
onDashboardSelect({ + id: dashboard.dashboard_id, + name: dashboard.dashboard_name, + workbookName: dashboard.workbook_name, + contentUrl: dashboard.content_url + })} + className={`p-3 rounded-lg cursor-pointer transition-colors mb-1 ${ + selectedDashboard?.id === dashboard.dashboard_id + ? 'bg-blue-600 text-white' + : 'hover:bg-slate-700 text-slate-300' + }`} + > +
+ +
+
{dashboard.dashboard_name}
+
{dashboard.workbook_name}
+
+
+
+ ))} +
+
+ ) : ( +
+ {restApiLoading ? ( +
+ +

Loading dashboards...

+
+ ) : restApiError ? ( +
+

Error loading dashboards:

+

{restApiError}

+
+ ) : ( +

No dashboards available

+ )} +
+ )} +
+ )} +
+
+ ); +}; + +export const TableauNavigation = memo(TableauNavigationComponent); diff --git a/src/components/TableauNavigation/index.js b/src/components/TableauNavigation/index.js new file mode 100644 index 00000000..3f14d707 --- /dev/null +++ b/src/components/TableauNavigation/index.js @@ -0,0 +1 @@ +export { TableauNavigation } from './TableauNavigation'; diff --git a/src/components/index.js b/src/components/index.js index 23132107..7dc2afd6 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -51,3 +51,6 @@ export { SessionProvider, VercelAgentRuntimeProvider, LanggraphAgentRuntimeProvi export { FloatingAssistant, Thread, Agent } from './Agent'; export { AuthGuard } from './AuthGuard'; + +export { TableauNavigation } from './TableauNavigation'; +export { DynamicDashboardViewer } from './TableauNavigation/DynamicDashboardViewer'; diff --git a/src/components/ui/Breadcrumb.jsx b/src/components/ui/Breadcrumb.jsx index 8cf0c74d..c441ba25 100644 --- a/src/components/ui/Breadcrumb.jsx +++ b/src/components/ui/Breadcrumb.jsx @@ -36,7 +36,7 @@ const BreadcrumbLink = React.forwardRef(({ asChild, className, ...props }, ref) () diff --git a/src/global.css b/src/global.css index 4825f36b..a553128b 100644 --- a/src/global.css +++ b/src/global.css @@ -193,6 +193,41 @@ --metrics-neutral: 280 70% 75%; --metrics-negative: 280 60% 33% ; } + + /* Define variables for the 'veriforce' theme (Blue) */ + [data-theme="veriforce"] { + --background: 222 47% 11%; /* Dark Blue */ + --foreground: 210 40% 98%; /* Light Blue */ + --demo-background: 222 47% 11%; /* Dark background for demos */ + --nav-background: 222 47% 11%; /* Dark navigation */ + --logo-background: 210 40% 98%; /* sometimes logo files are transparent */ + --icon-background: 210 40% 95%; /* same with icon having transparent backgrounds */ + --login-card-background: 0 0% 100%; /* for the Auth component on login screens */ + --nav-icons: 222 47% 30%; /* Darker icons on dark nav */ + --ai-icons: 210 40% 95%; + --breadcrumbs: 210 40% 98%; /* Light breadcrumbs on dark nav */ + --card: 0 0% 100%; /* White */ + --card-foreground: 222 47% 11%; + --popover: 0 0% 100%; + --popover-foreground: 222 47% 11%; + --primary: 217 91% 60%; /* Veriforce Blue */ + --primary-foreground: 0 0% 100%; /* White */ + --secondary: 210 40% 96%; /* Light Blue */ + --secondary-foreground: 222 47% 11%; /* Dark Blue */ + --muted: 210 40% 96%; /* Very Light Blue */ + --muted-foreground: 215 16% 47%; /* Mid Blue-Gray */ + --accent: 217 91% 60%; /* Veriforce Blue */ + --accent-foreground: 0 0% 100%; /* White */ + --destructive: 0 84% 60%; /* Red */ + --destructive-foreground: 0 0% 100%; + --border: 214 32% 91%; /* Soft Blue Border */ + --input: 214 32% 91%; /* Very Soft Blue Input */ + --ring: 217 91% 60%; /* Veriforce Blue Ring */ + + --metrics-positive: 142 76% 36%; /* Green */ + --metrics-neutral: 39 7% 54%; /* Yellow */ + --metrics-negative: 0 84% 60%; /* Red */ + } } /* End of @layer base containing themes */ @layer base { diff --git a/src/hooks/tableauHooks.js b/src/hooks/tableauHooks.js new file mode 100644 index 00000000..ddee623b --- /dev/null +++ b/src/hooks/tableauHooks.js @@ -0,0 +1,2 @@ +"use client"; +export { useTableauDashboards } from './useTableauDashboards'; diff --git a/src/hooks/useTableauDashboards.js b/src/hooks/useTableauDashboards.js new file mode 100644 index 00000000..be6ef61d --- /dev/null +++ b/src/hooks/useTableauDashboards.js @@ -0,0 +1,183 @@ +"use client"; + +import { useState, useEffect, useCallback } from 'react'; +import { useTableauSession } from '@/hooks'; +import { useMetadata } from '@/hooks'; + +// Custom hook for managing Tableau dashboards +export const useTableauDashboards = () => { + const { + status: sessionStatus, + data: user, + isSuccess: isSessionSuccess, + isError: isSessionError, + isLoading: isSessionLoading + } = useTableauSession(); + + // REST API state + const [restApiData, setRestApiData] = useState(null); + const [restApiLoading, setRestApiLoading] = useState(false); + const [restApiError, setRestApiError] = useState(null); + + // Function to fetch workbooks using our session-based backend API + const fetchWorkbooksViaREST = useCallback(async () => { + if (!user?.user_id || !user?.site_id || !user?.rest_key) { + return; + } + + setRestApiLoading(true); + setRestApiError(null); + + try { + // Just call our backend API - it will handle authentication via session + const response = await fetch('/api/tableau/workbooks?pageSize=100&page=1'); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + console.error('❌ Backend API Error:', { + status: response.status, + statusText: response.statusText, + errorData + }); + throw new Error(`Backend API Error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + console.log('✅ Backend API Response:', { + workboosk: data.workbooks, + workbooksCount: data.workbooks?.length || 0, + totalAvailable: data.pagination?.totalAvailable || 0 + }); + + // Transform workbooks into dashboard format + const allDashboards = []; + + if (data.workbooks && data.workbooks.length > 0) { + // For each workbook, we need to get its views/dashboards + for (const workbook of data.workbooks) { + console.log(`🔍 Getting views for workbook: ${workbook.name}`); + + try { + const viewsResponse = await fetch(`/api/tableau/views?workbookId=${workbook.id}`); + console.log("viewsResponse", viewsResponse) + if (viewsResponse.ok) { + const viewsData = await viewsResponse.json(); + console.log("viewsData", viewsData) + + if (viewsData.views) { + for (const view of viewsData.views) { + allDashboards.push({ + dashboard_name: view.name, + dashboard_id: view.id, + workbook_name: workbook.name, + content_url: view.contentUrl, + workbook_id: workbook.id + }); + } + } + } + } catch (viewError) { + console.error(`❌ Error getting views for workbook ${workbook.name}:`, viewError); + } + } + } + + console.log('✅ All dashboards processed:', { + totalDashboards: allDashboards.length + }); + + // Log dashboard names + if (allDashboards.length > 0) { + console.log('📋 Dashboard Names:'); + allDashboards.forEach((dash, index) => { + console.log(` ${index + 1}. ${dash.dashboard_name} (Workbook: ${dash.workbook_name})`); + }); + } + + setRestApiData({ + workbooks: allDashboards, + totalCount: allDashboards.length + }); + + } catch (error) { + console.error('❌ REST API Error:', error); + setRestApiError(error.message); + } finally { + setRestApiLoading(false); + } + }, [user?.user_id, user?.site_id, user?.rest_key]); + + // Fetch workbooks when user is authenticated + useEffect(() => { + if (user?.user_id && user?.site_id && user?.rest_key) { + console.log('🎯 User authenticated, fetching workbooks...'); + fetchWorkbooksViaREST(); + } else { + console.log('⏸️ User not authenticated yet, waiting...'); + } + }, [fetchWorkbooksViaREST]); + + // Use the existing metadata hook to get workbooks and dashboards + const { + data: metadata, + isLoading, + isError, + error + } = useMetadata(); + + + // Process + let dashboardsByFolder = {}; + let allViews = []; + + if (metadata && Array.isArray(metadata)) { + + metadata.forEach(item => { + if (item.workbooks && Array.isArray(item.workbooks)) { + item.workbooks.forEach(workbook => { + if (workbook.views && Array.isArray(workbook.views)) { + const folderName = item.name || 'Default'; + + if (!dashboardsByFolder[folderName]) { + dashboardsByFolder[folderName] = []; + } + + dashboardsByFolder[folderName].push({ + ...workbook, + views: workbook.views + }); + + // Add all views to the flat array + allViews.push(...workbook.views); + } + }); + } + }); + } else { + } + + + return { + // Session data + user, + sessionStatus, + isSessionSuccess, + isSessionError, + isSessionLoading, + + // Metadata-based data (GraphQL) + dashboardsByFolder, + allViews, + metadata, + isLoading, + isError, + error, + + // REST API data + restApiData, + restApiLoading, + restApiError, + // Helper function to refresh data + refreshData: fetchWorkbooksViaREST + }; +}; diff --git a/src/hooks/useTableauSession.ts b/src/hooks/useTableauSession.ts index ccfc0583..3d5eb3cb 100644 --- a/src/hooks/useTableauSession.ts +++ b/src/hooks/useTableauSession.ts @@ -26,9 +26,10 @@ export const useTableauSession = () => { return useQuery({ // eslint-disable-next-line @tanstack/query/exhaustive-deps queryKey: queryKey, - queryFn: () => { + queryFn: async () => { if (session_data?.user?.email) { - return getClientSession(session_data.user.email); + const result = await getClientSession(session_data.user.email); + return result; } else { throw new Error("useTableauSession Error: Session data not available"); } diff --git a/src/models/Users/userStore.ts b/src/models/Users/userStore.ts index ee421882..28af280b 100644 --- a/src/models/Users/userStore.ts +++ b/src/models/Users/userStore.ts @@ -148,5 +148,42 @@ export const Users = [ uaf: {} }, ] + }, + { + demo: 'veriforce', + roles: { + 0: { title: 'Safety Team', description: 'Safety compliance tracking and incident monitoring'}, + 1: { title: 'Procurement Team', description: 'Vendor performance and contract management'}, + 2: { title: 'Management', description: 'Executive oversight and strategic insights'}, + }, + users: [ + { + id: 'a', + name: "Sarah Johnson", + email: "sjohnson@veriforce.com", + picture: "/img/users/sofia_lopez.png", + role: 0, + vector_store: 'veriforce_sjohnson', + uaf: {"Department": ["Safety"], "Region": ["Central"]} + }, + { + id: 'b', + name: "Mike Chen", + email: "mchen@veriforce.com", + picture: "/img/users/justin_chen.png", + role: 1, + vector_store: 'veriforce_mchen', + uaf: {"Department": ["Procurement"], "Region": ["West"]} + }, + { + id: 'c', + name: "Lisa Rodriguez", + email: "lrodriguez@veriforce.com", + picture: "/img/users/rachel_morris.png", + role: 2, + vector_store: 'veriforce_lrodriguez', + uaf: {"Department": ["Management"], "Region": ["South"]} + }, + ] } ] diff --git a/src/services/tableauApi.js b/src/services/tableauApi.js new file mode 100644 index 00000000..28f255fa --- /dev/null +++ b/src/services/tableauApi.js @@ -0,0 +1,270 @@ +/** + * Tableau REST API Service + * Handles authentication and data fetching from Tableau Server/Online + */ + +const TABLEAU_BASE_URL = process.env.NEXT_PUBLIC_TABLEAU_BASE_URL || 'https://prod-useast-b.online.tableau.com'; + +class TableauApiService { + constructor() { + this.baseUrl = TABLEAU_BASE_URL; + this.siteId = process.env.NEXT_PUBLIC_TABLEAU_SITE_ID || 'embeddingplaybook'; + } + + /** + * Authenticate with Tableau Server using JWT token + * This method is used when you already have a JWT token from your existing auth system + */ + async authenticateWithJWT(jwtToken) { + try { + const response = await fetch(`${this.baseUrl}/api/3.19/auth/signin`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + credentials: { + jwt: jwtToken, + site: { + contentUrl: this.siteId + } + } + }) + }); + + if (!response.ok) { + throw new Error(`JWT authentication failed: ${response.statusText}`); + } + + const data = await response.json(); + return { + token: data.credentials.token, + siteId: data.credentials.site.id, + userId: data.credentials.user.id + }; + } catch (error) { + console.error('Tableau JWT authentication error:', error); + throw error; + } + } + + /** + * Get user's accessible workbooks and dashboards + */ + async getUserDashboards(token, siteId) { + try { + console.log('🔍 Fetching workbooks from Tableau API:', { + url: `${this.baseUrl}/api/3.19/sites/${siteId}/workbooks`, + siteId, + hasToken: !!token + }); + + const response = await fetch(`${this.baseUrl}/api/3.19/sites/${siteId}/workbooks`, { + headers: { + 'X-Tableau-Auth': token, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + console.error('❌ Tableau API Error:', { + status: response.status, + statusText: response.statusText, + url: response.url + }); + throw new Error(`Failed to fetch workbooks: ${response.statusText}`); + } + + const data = await response.json(); + const parsedWorkbooks = this.parseWorkbooks(data.workbooks); + + console.log('✅ Workbooks fetched successfully:', { + rawCount: data.workbooks?.length || 0, + parsedCount: parsedWorkbooks.length, + workbooks: parsedWorkbooks.map(wb => ({ + name: wb.name, + project: wb.projectName, + id: wb.id + })) + }); + + return parsedWorkbooks; + } catch (error) { + console.error('❌ Error fetching user dashboards:', error); + throw error; + } + } + + /** + * Get folders for organizing dashboards + */ + async getFolders(token, siteId) { + try { + console.log('📁 Fetching folders from Tableau API:', { + url: `${this.baseUrl}/api/3.19/sites/${siteId}/projects`, + siteId, + hasToken: !!token + }); + + const response = await fetch(`${this.baseUrl}/api/3.19/sites/${siteId}/projects`, { + headers: { + 'X-Tableau-Auth': token, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + console.error('❌ Tableau Folders API Error:', { + status: response.status, + statusText: response.statusText, + url: response.url + }); + throw new Error(`Failed to fetch folders: ${response.statusText}`); + } + + const data = await response.json(); + const parsedProjects = this.parseProjects(data.projects); + + console.log('✅ Folders fetched successfully:', { + rawCount: data.projects?.length || 0, + parsedCount: parsedProjects.length, + folders: parsedProjects.map(project => ({ + name: project.name, + id: project.id, + description: project.description + })) + }); + + return parsedProjects; + } catch (error) { + console.error('❌ Error fetching folders:', error); + throw error; + } + } + + /** + * Get specific workbook details including views + */ + async getWorkbookViews(token, siteId, workbookId) { + try { + console.log('👁️ Fetching views for workbook:', { + workbookId, + url: `${this.baseUrl}/api/3.19/sites/${siteId}/workbooks/${workbookId}`, + hasToken: !!token + }); + + const response = await fetch(`${this.baseUrl}/api/3.19/sites/${siteId}/workbooks/${workbookId}`, { + headers: { + 'X-Tableau-Auth': token, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + console.error('❌ Tableau Views API Error:', { + status: response.status, + statusText: response.statusText, + workbookId, + url: response.url + }); + throw new Error(`Failed to fetch workbook views: ${response.statusText}`); + } + + const data = await response.json(); + const parsedViews = this.parseViews(data.workbook.views); + + console.log('✅ Views fetched successfully for workbook:', { + workbookId, + rawCount: data.workbook.views?.length || 0, + parsedCount: parsedViews.length, + views: parsedViews.map(view => ({ + name: view.name, + id: view.id, + viewUrlName: view.viewUrlName + })) + }); + + return parsedViews; + } catch (error) { + console.error('❌ Error fetching workbook views:', error); + throw error; + } + } + + /** + * Parse workbooks data into a more usable format + */ + parseWorkbooks(workbooks) { + return workbooks.map(workbook => ({ + id: workbook.id, + name: workbook.name, + description: workbook.description || '', + projectId: workbook.project.id, + projectName: workbook.project.name, + owner: workbook.owner?.name || 'Unknown', + createdAt: workbook.createdAt, + updatedAt: workbook.updatedAt, + webPageUrl: workbook.webPageUrl, + size: workbook.size + })); + } + + /** + * Parse projects (folders) data + */ + parseProjects(projects) { + return projects.map(project => ({ + id: project.id, + name: project.name, + description: project.description || '', + parentProjectId: project.parentProjectId, + contentPermissions: project.contentPermissions + })); + } + + /** + * Parse views (dashboards) data + */ + parseViews(views) { + return views.map(view => ({ + id: view.id, + name: view.name, + contentUrl: view.contentUrl, + workbookId: view.workbook?.id, + workbookName: view.workbook?.name, + projectId: view.project?.id, + projectName: view.project?.name, + createdAt: view.createdAt, + updatedAt: view.updatedAt, + webPageUrl: view.webPageUrl, + viewUrlName: view.viewUrlName + })); + } + + /** + * Get embed URL for a specific view + */ + getEmbedUrl(siteId, workbookId, viewId) { + return `${this.baseUrl}/t/${this.siteId}/views/${workbookId}/${viewId}`; + } + + /** + * Sign out from Tableau + */ + async signOut(token) { + try { + await fetch(`${this.baseUrl}/api/3.19/auth/signout`, { + method: 'POST', + headers: { + 'X-Tableau-Auth': token, + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('Error signing out:', error); + } + } +} + +const tableauApiService = new TableauApiService(); +export default tableauApiService; diff --git a/test_system.sh b/test_system.sh new file mode 100755 index 00000000..fa879cca --- /dev/null +++ b/test_system.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +echo "🧪 Testing Tableau Dashboard System" +echo "==================================" +echo "" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test 1: Check if server is running +echo -e "${BLUE}Test 1: Server Status${NC}" +if curl -s "http://localhost:3001" > /dev/null; then + echo -e "${GREEN}✅ Server is running on http://localhost:3001${NC}" +else + echo -e "${RED}❌ Server is not running${NC}" + exit 1 +fi +echo "" + +# Test 2: Check auth page +echo -e "${BLUE}Test 2: Authentication Page${NC}" +if curl -s "http://localhost:3001/demo/veriforce/auth" | grep -q "Sarah Johnson"; then + echo -e "${GREEN}✅ Auth page is working - Sarah Johnson found${NC}" +else + echo -e "${RED}❌ Auth page issue - Sarah Johnson not found${NC}" +fi +echo "" + +# Test 3: Check current session status +echo -e "${BLUE}Test 3: Current Session Status${NC}" +SESSION_RESPONSE=$(curl -s "http://localhost:3001/api/test/session") +if echo "$SESSION_RESPONSE" | grep -q '"authenticated":false'; then + echo -e "${YELLOW}⚠️ No user authenticated (expected before login)${NC}" + echo " To authenticate: Go to http://localhost:3001/demo/veriforce/auth and click 'Sarah Johnson'" +else + echo -e "${GREEN}✅ User is authenticated${NC}" + echo "Session: $SESSION_RESPONSE" +fi +echo "" + +# Test 4: Check backend API with dummy data +echo -e "${BLUE}Test 4: Backend API Endpoint${NC}" +API_RESPONSE=$(curl -s "http://localhost:3001/api/tableau/workbooks?siteId=test&userId=test&restToken=test&page=1&pageSize=5") +if echo "$API_RESPONSE" | grep -q "401"; then + echo -e "${GREEN}✅ Backend API is working (401 expected with dummy token)${NC}" +else + echo -e "${RED}❌ Backend API issue${NC}" + echo "Response: $API_RESPONSE" +fi +echo "" + +# Test 5: Check safety dashboard +echo -e "${BLUE}Test 5: Safety Dashboard Page${NC}" +if curl -s "http://localhost:3001/demo/veriforce/safety" | grep -q "No Dashboards Available"; then + echo -e "${YELLOW}⚠️ Safety dashboard shows 'No Dashboards Available' (expected before authentication)${NC}" +else + echo -e "${GREEN}✅ Safety dashboard is loading${NC}" +fi +echo "" + +echo "🎯 NEXT STEPS:" +echo "==============" +echo "1. 🔑 Go to: http://localhost:3001/demo/veriforce/auth" +echo "2. 👤 Click on 'Sarah Johnson' to authenticate" +echo "3. 📊 Navigate to: http://localhost:3001/demo/veriforce/safety" +echo "4. ✨ You should see workbooks in the navigation sidebar" +echo "" +echo "If you see workbooks after authentication, the system is working! 🎉" From 053947d614edf54743036248951686ace5f21922 Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Wed, 8 Oct 2025 14:15:45 -0600 Subject: [PATCH 02/21] veriforce demo --- src/app/api/tableau/views/route.js | 2 ++ src/app/api/tableau/workbooks/route.js | 2 ++ src/app/api/test/session/route.js | 5 ++++- src/app/demo/veriforce/safety/page.jsx | 5 ++++- src/components/TableauEmbed/TableauAuth.jsx | 7 +++++-- src/components/TableauEmbed/TableauViz.jsx | 10 +--------- src/hooks/useTableauDashboards.js | 2 +- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/app/api/tableau/views/route.js b/src/app/api/tableau/views/route.js index 62289e3d..8c975395 100644 --- a/src/app/api/tableau/views/route.js +++ b/src/app/api/tableau/views/route.js @@ -1,6 +1,8 @@ import { NextResponse } from 'next/server'; import { getToken } from "next-auth/jwt"; +export const dynamic = 'force-dynamic'; + export async function GET(request) { try { // Get JWT token which contains the Tableau authentication data diff --git a/src/app/api/tableau/workbooks/route.js b/src/app/api/tableau/workbooks/route.js index 0312b83b..6ac39c08 100644 --- a/src/app/api/tableau/workbooks/route.js +++ b/src/app/api/tableau/workbooks/route.js @@ -1,6 +1,8 @@ import { NextResponse } from 'next/server'; import { getToken } from "next-auth/jwt"; +export const dynamic = 'force-dynamic'; + export async function GET(request) { try { // Get JWT token which contains the Tableau authentication data diff --git a/src/app/api/test/session/route.js b/src/app/api/test/session/route.js index 68e07ed5..8dd772d0 100644 --- a/src/app/api/test/session/route.js +++ b/src/app/api/test/session/route.js @@ -1,10 +1,13 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; +import { authOptions } from '../auth/[...nextauth]/options'; + +export const dynamic = 'force-dynamic'; export async function GET(request) { try { // Get the session to see if user is authenticated - const session = await getServerSession(); + const session = await getServerSession(authOptions); console.log('🔍 Test API - Session check:', { hasSession: !!session, diff --git a/src/app/demo/veriforce/safety/page.jsx b/src/app/demo/veriforce/safety/page.jsx index c7dae9a5..93dbcf61 100644 --- a/src/app/demo/veriforce/safety/page.jsx +++ b/src/app/demo/veriforce/safety/page.jsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react'; import { Demo, FloatingAssistant } from '@/components'; import { SafetyDashboard } from './SafetyDashboard'; @@ -11,7 +12,9 @@ const Page = () => { settings={settings} pageName={pageName} > - + Loading...
}> + + diff --git a/src/components/TableauEmbed/TableauAuth.jsx b/src/components/TableauEmbed/TableauAuth.jsx index 7de4abcc..257e5b92 100644 --- a/src/components/TableauEmbed/TableauAuth.jsx +++ b/src/components/TableauEmbed/TableauAuth.jsx @@ -42,6 +42,9 @@ export const TableauAuth = forwardRef(function AuthLayer(props, ref) { embed_token = user.embed_token; } + // Check if Mike is logged in + const isMikeLoggedIn = isSessionSuccess && user?.email === 'mchen@veriforce.com'; + // For public URLs, render immediately without authentication if (isPublicTableauUrl) { return ( @@ -54,7 +57,7 @@ export const TableauAuth = forwardRef(function AuthLayer(props, ref) { hide-tabs={hideTabs ? true : false} toolbar={toolbar} isPublic={isPublic} - customToolbar={customToolbar} + customToolbar={customToolbar && !isMikeLoggedIn} height={height} width={width} /> @@ -75,7 +78,7 @@ export const TableauAuth = forwardRef(function AuthLayer(props, ref) { hide-tabs={hideTabs ? true : false} toolbar={toolbar} isPublic={isPublic} - customToolbar={customToolbar} + customToolbar={customToolbar && !isMikeLoggedIn} height={height} width={width} /> : diff --git a/src/components/TableauEmbed/TableauViz.jsx b/src/components/TableauEmbed/TableauViz.jsx index b248a9bd..86fefe51 100644 --- a/src/components/TableauEmbed/TableauViz.jsx +++ b/src/components/TableauEmbed/TableauViz.jsx @@ -5,7 +5,6 @@ import { useEffect, useState, useRef, forwardRef, useId } from 'react'; import { tab_embed } from 'libs'; import { TableauToolbar } from 'components'; -import { useTableauSession } from 'hooks'; // handles post authentication logic requiring an initialized object to operate @@ -22,13 +21,6 @@ export const TableauViz = forwardRef(function Viz(props, ref) { height, width } = props; - - // Get user session data to check if Mike is logged in - const { data: user, isSuccess: isSessionSuccess } = useTableauSession(); - - // Check if Mike is logged in - const isMikeLoggedIn = isSessionSuccess && user?.email === 'mchen@veriforce.com'; - // creates a unique identifier for the embed const id = `id-${useId()}`; // to be used if parent did not forward a ref @@ -74,7 +66,7 @@ export const TableauViz = forwardRef(function Viz(props, ref) { return (
- {customToolbar && !isMikeLoggedIn ? : null} + {customToolbar ? : null} { } else { console.log('⏸️ User not authenticated yet, waiting...'); } - }, [fetchWorkbooksViaREST]); + }, [fetchWorkbooksViaREST, user?.user_id, user?.site_id, user?.rest_key]); // Use the existing metadata hook to get workbooks and dashboards const { From 806d92be1d29beb16ddadbd2934e51d89c851e3a Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Wed, 8 Oct 2025 14:22:11 -0600 Subject: [PATCH 03/21] veriforce wip --- src/app/api/test/session/route.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/test/session/route.js b/src/app/api/test/session/route.js index 8dd772d0..6e55b3b1 100644 --- a/src/app/api/test/session/route.js +++ b/src/app/api/test/session/route.js @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; -import { authOptions } from '../auth/[...nextauth]/options'; +import { authOptions } from '../../auth/[...nextauth]/options'; export const dynamic = 'force-dynamic'; From e616e9ed5c7824a62220020a0c11a8cbdbf1a190 Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Mon, 13 Oct 2025 15:17:27 -0500 Subject: [PATCH 04/21] customized dashboards --- src/app/demo/veriforce/Home.jsx | 50 ++++-- .../demo/veriforce/safety/SafetyDashboard.jsx | 152 +++++++++++------- src/components/Explore/Explore.jsx | 5 - 3 files changed, 127 insertions(+), 80 deletions(-) diff --git a/src/app/demo/veriforce/Home.jsx b/src/app/demo/veriforce/Home.jsx index f85888bf..417320c5 100644 --- a/src/app/demo/veriforce/Home.jsx +++ b/src/app/demo/veriforce/Home.jsx @@ -44,14 +44,14 @@ export const Home = () => { {/* Main Dashboard */} -
- {/* Compliance Overview */} -
+
+ {/* Executive Summary */} +
- Compliance Overview + Compliance and Safety Executive Summary Real-time compliance tracking across all contractors and safety metrics @@ -59,11 +59,35 @@ export const Home = () => { + + +
+ {/* {Compliance Center} */} +
+ + + + + Compliance Center + + + Contractor Compliance and Certification Status Overview + + + @@ -72,9 +96,9 @@ export const Home = () => {
{/* Quick Actions & Alerts */} -
+ {/*
*/} {/* Critical Alerts */} - + {/* @@ -110,10 +134,10 @@ export const Home = () => {
- + */} {/* Quick Actions */} - + {/* @@ -138,8 +162,8 @@ export const Home = () => {

Use self-service tools

-
-
+ */} + {/*
*/}
diff --git a/src/app/demo/veriforce/safety/SafetyDashboard.jsx b/src/app/demo/veriforce/safety/SafetyDashboard.jsx index 0ef0528b..785eaa46 100644 --- a/src/app/demo/veriforce/safety/SafetyDashboard.jsx +++ b/src/app/demo/veriforce/safety/SafetyDashboard.jsx @@ -97,7 +97,7 @@ const MainContent = ({ selectedDashboard, embedToken, siteId, showNavigation, se return (
{/* Header with Navigation Toggle */} -
+
- -
- - - - - + + + + + - {/* Incident Tracking */} - {/* - -
-
- Incident Tracking - Monthly incident reports and trends + {/* Incident Tracking */} + {/* + +
+
+ Incident Tracking + Monthly incident reports and trends +
+ + + All + Critical + Resolved + +
- - - All - Critical - Resolved - - -
- - - - - */} -
+
+ + + +
*/} +
- {/* Sidebar - 1/3 width on large screens */} -
+ {/* Sidebar - 1/3 width on large screens */} +
{/* Safety Score Card */} - + Safety Score Current compliance rating @@ -253,7 +254,7 @@ const MainContent = ({ selectedDashboard, embedToken, siteId, showNavigation, se {/* Quick Actions */} - + Quick Actions Common safety tasks @@ -277,7 +278,34 @@ const MainContent = ({ selectedDashboard, embedToken, siteId, showNavigation, se +
+ + {/* Full Width Dashboard - Lost Time Injuries */} + + +
+
+ Lost Time Injuries Analysis + Comprehensive injury tracking and trends +
+ +
+
+ + + +
)}
diff --git a/src/components/Explore/Explore.jsx b/src/components/Explore/Explore.jsx index eb985757..03af7134 100644 --- a/src/components/Explore/Explore.jsx +++ b/src/components/Explore/Explore.jsx @@ -4,11 +4,6 @@ import { TableauEmbed } from 'components'; export const Explore = (props) => { const { title, description, src } = props; - const isMikeUser = typeof window !== 'undefined' && - (window.location.href.includes('mchen@veriforce.com') || - window.location.search.includes('u=mchen')); - - console.log("isMikeUser", isMikeUser); return ( From ba22dd1821869983f70d6f6b3e5e4ca91b505382 Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Mon, 13 Oct 2025 16:34:35 -0500 Subject: [PATCH 05/21] filter and actionable insights --- src/app/demo/veriforce/Home.jsx | 420 ++++++++++++++++++- src/components/TableauEmbed/TableauAuth.jsx | 5 +- src/components/TableauEmbed/TableauEmbed.jsx | 4 +- src/components/TableauEmbed/TableauViz.jsx | 8 +- 4 files changed, 431 insertions(+), 6 deletions(-) diff --git a/src/app/demo/veriforce/Home.jsx b/src/app/demo/veriforce/Home.jsx index 417320c5..e8b2407d 100644 --- a/src/app/demo/veriforce/Home.jsx +++ b/src/app/demo/veriforce/Home.jsx @@ -1,3 +1,6 @@ +"use client"; + +import { useState, useRef, useEffect } from 'react'; import { Card, CardContent, @@ -12,12 +15,244 @@ import { AlertTriangle, TrendingUp, XCircle, - Clock + Clock, + Filter, + X } from 'lucide-react'; export const description = "Veriforce Contractor Risk Management - Comprehensive safety and compliance tracking dashboard with real-time alerts and self-service analytics"; export const Home = () => { + const [showFilterPopup, setShowFilterPopup] = useState(false); + const [insuranceStatus, setInsuranceStatus] = useState('all'); + const [selectedMarks, setSelectedMarks] = useState([]); + const [showEmailModal, setShowEmailModal] = useState(false); + const [emailPreviews, setEmailPreviews] = useState([]); + const [currentEmailIndex, setCurrentEmailIndex] = useState(0); + + // Apply filter to Tableau dashboards when insurance status changes + useEffect(() => { + const applyFilter = async () => { + console.log('Insurance status changed to:', insuranceStatus); + const fieldName = 'Insurance Status'; // Update this to match your actual field name in Tableau + const filterValue = insuranceStatus === 'all' ? [] : [insuranceStatus]; + + const applyFilterToViz = async (vizId) => { + // Get the actual tableau-viz web component by ID + const viz = document.getElementById(vizId); + + if (!viz) { + console.log(`Viz with id ${vizId} not found`); + return; + } + + // Wait for workbook to be available + if (!viz.workbook) { + console.log(`Workbook not ready for ${vizId}, waiting...`); + // Try again after a short delay + setTimeout(() => applyFilterToViz(vizId), 500); + return; + } + + try { + const activeSheet = viz.workbook.activeSheet; + console.log(`Applying filter to ${vizId} - Sheet:`, activeSheet.name, 'Type:', activeSheet.sheetType); + + // Check if the active sheet is a dashboard + if (activeSheet.sheetType === 'dashboard') { + // Apply filter to all worksheets in the dashboard + const worksheets = activeSheet.worksheets; + console.log('Found worksheets in dashboard:', worksheets.length); + + for (const worksheet of worksheets) { + if (insuranceStatus === 'all') { + await worksheet.clearFilterAsync(fieldName); + console.log(`Cleared filter on worksheet: ${worksheet.name}`); + } else { + await worksheet.applyFilterAsync(fieldName, filterValue, 'replace'); + console.log(`Applied filter ${filterValue} to worksheet: ${worksheet.name}`); + } + } + } else { + // If it's a single worksheet, apply directly + if (insuranceStatus === 'all') { + await activeSheet.clearFilterAsync(fieldName); + console.log('Cleared filter on worksheet'); + } else { + await activeSheet.applyFilterAsync(fieldName, filterValue, 'replace'); + console.log('Applied filter to worksheet:', filterValue); + } + } + } catch (error) { + console.error(`Error applying filter to ${vizId}:`, error); + console.error('Error details:', error.message); + } + }; + + // Apply filter to both dashboards using their IDs + await applyFilterToViz('executiveSummaryViz'); + await applyFilterToViz('complianceCenterViz'); + }; + + applyFilter(); + }, [insuranceStatus]); + + // Listen for mark selection events - attach INSIDE firstinteractive + useEffect(() => { + const handleMarkSelectionChanged = (markSelectionChangedEvent) => { + console.log('=== MARK SELECTION CHANGED ==='); + console.log('Event detail:', markSelectionChangedEvent.detail); + + // Use the pattern from the working Angular example + markSelectionChangedEvent.detail.getMarksAsync().then((marks) => { + console.log('Selected marks data:', marks); + console.log('Number of data tables:', marks.data.length); + + // Process marks data like the Angular example + const marksData = []; + + for (let markIndex = 0; markIndex < marks.data[0].data.length; markIndex++) { + const columns = marks.data[0].columns; + const obj = {}; + + for (let colIndex = 0; colIndex < columns.length; colIndex++) { + obj[columns[colIndex].fieldName] = marks.data[0].data[markIndex][colIndex].formattedValue; + } + + marksData.push(obj); + } + + console.log('Processed marks data:', marksData); + + // Store selected marks for email functionality + setSelectedMarks(marksData); + + // Log column names + if (marks.data[0].columns) { + const columnNames = marks.data[0].columns.map(col => col.fieldName); + console.log('Column names:', columnNames); + } + + }).catch((error) => { + console.error('Error getting selected marks:', error); + }); + }; + + const setupListeners = () => { + const executiveSummaryViz = document.getElementById('executiveSummaryViz'); + const complianceCenterViz = document.getElementById('complianceCenterViz'); + + console.log('Setting up listeners...'); + console.log('Executive Summary Viz found:', !!executiveSummaryViz); + console.log('Compliance Center Viz found:', !!complianceCenterViz); + + if (executiveSummaryViz) { + console.log('Adding firstinteractive listener to Executive Summary'); + executiveSummaryViz.addEventListener('firstinteractive', (event) => { + console.log('Executive Summary is now interactive!'); + // Add mark selection listener INSIDE firstinteractive + executiveSummaryViz.addEventListener('markselectionchanged', handleMarkSelectionChanged); + console.log('Mark selection listener attached to Executive Summary'); + }); + } + + if (complianceCenterViz) { + console.log('Adding firstinteractive listener to Compliance Center'); + complianceCenterViz.addEventListener('firstinteractive', (event) => { + console.log('Compliance Center is now interactive!'); + // Add mark selection listener INSIDE firstinteractive + complianceCenterViz.addEventListener('markselectionchanged', handleMarkSelectionChanged); + console.log('Mark selection listener attached to Compliance Center'); + }); + } + + return { executiveSummaryViz, complianceCenterViz }; + }; + + // Delay setup to ensure DOM elements are available + const timer = setTimeout(() => { + const { executiveSummaryViz, complianceCenterViz } = setupListeners(); + + // Store refs for cleanup + window._vizRefs = { executiveSummaryViz, complianceCenterViz, handleMarkSelectionChanged }; + }, 1000); + + // Cleanup + return () => { + clearTimeout(timer); + if (window._vizRefs) { + const { executiveSummaryViz, complianceCenterViz, handleMarkSelectionChanged } = window._vizRefs; + if (executiveSummaryViz) { + executiveSummaryViz.removeEventListener('markselectionchanged', handleMarkSelectionChanged); + } + if (complianceCenterViz) { + complianceCenterViz.removeEventListener('markselectionchanged', handleMarkSelectionChanged); + } + delete window._vizRefs; + } + }; + }, []); + + // Generate emails from selected marks (handles multiple selections) + const generateEmail = () => { + if (selectedMarks.length === 0) { + alert('Please select data points from the dashboard first!'); + return; + } + + // Generate an email for each selected mark + const emails = selectedMarks.map((mark, index) => { + // Try to find email field (common field names) + const emailField = Object.keys(mark).find(key => + key.toLowerCase().includes('email') || + key.toLowerCase().includes('e-mail') || + key.toLowerCase().includes('contact') + ); + + // Try to find contractor/company name + const nameField = Object.keys(mark).find(key => + key.toLowerCase().includes('contractor') || + key.toLowerCase().includes('company') || + key.toLowerCase().includes('name') + ); + + const emailAddress = emailField ? mark[emailField] : `contractor${index + 1}@example.com`; + const contractorName = nameField ? mark[nameField] : `Contractor ${index + 1}`; + + // Generate email content + return { + to: emailAddress, + subject: 'URGENT: Insurance Status Expired - Action Required', + body: `Dear ${contractorName}, + +This is an urgent notification regarding your insurance status. + +Our records indicate that your insurance coverage has EXPIRED. This requires immediate attention to maintain compliance with our contractor requirements. + +Selected Record Details: +${Object.entries(mark).map(([key, value]) => ` • ${key}: ${value}`).join('\n')} + +Please take the following actions immediately: +1. Review your current insurance policy +2. Renew or update your insurance coverage +3. Submit updated documentation to our compliance team + +Failure to address this issue may result in suspension of contractor privileges. + +If you have any questions or need assistance, please contact our compliance department. + +Best regards, +Veriforce Compliance Team + +--- +This is a demo email generated from Tableau mark selection.` + }; + }); + + setEmailPreviews(emails); + setCurrentEmailIndex(0); + setShowEmailModal(true); + }; return (
@@ -59,6 +294,7 @@ export const Home = () => { {
+ + {/* Insurance Status Filter Widget */} +
+ + + {/* Email Button - Shows when marks are selected */} + {selectedMarks.length > 0 && ( + + )} +
+ {/* {Compliance Center} */}
@@ -83,6 +342,7 @@ export const Home = () => { { {/*
*/}
+ + {/* Filter Popup Modal */} + {showFilterPopup && ( +
setShowFilterPopup(false)}> +
e.stopPropagation()}> +
+

+ + Filter by Insurance Status +

+ +
+ +
+ {['All', 'Active', 'Expired', 'Pending'].map((status) => ( + + ))} +
+
+
+ )} + + {/* Email Preview Modal - Multiple Recipients */} + {showEmailModal && emailPreviews.length > 0 && ( +
setShowEmailModal(false)}> +
e.stopPropagation()}> +
+

+ + Insurance Expiration Notices +

+ +
+ + {/* Email Counter and Navigation */} + {emailPreviews.length > 1 && ( +
+ + + Email {currentEmailIndex + 1} of {emailPreviews.length} + + +
+ )} + +
+ {/* Email Header */} +
+
+ +

{emailPreviews[currentEmailIndex].to}

+
+
+ +

{emailPreviews[currentEmailIndex].subject}

+
+
+ + {/* Email Body */} +
+ +
+                  {emailPreviews[currentEmailIndex].body}
+                
+
+ + {/* Action Buttons */} +
+ +
+ + +
+
+
+
+
+ )}
); }; diff --git a/src/components/TableauEmbed/TableauAuth.jsx b/src/components/TableauEmbed/TableauAuth.jsx index 257e5b92..6f9d2f2a 100644 --- a/src/components/TableauEmbed/TableauAuth.jsx +++ b/src/components/TableauEmbed/TableauAuth.jsx @@ -16,7 +16,8 @@ export const TableauAuth = forwardRef(function AuthLayer(props, ref) { toolbar, isPublic, WebEdit, - customToolbar + customToolbar, + id } = props; let embed_token; @@ -60,6 +61,7 @@ export const TableauAuth = forwardRef(function AuthLayer(props, ref) { customToolbar={customToolbar && !isMikeLoggedIn} height={height} width={width} + id={id} />
); @@ -81,6 +83,7 @@ export const TableauAuth = forwardRef(function AuthLayer(props, ref) { customToolbar={customToolbar && !isMikeLoggedIn} height={height} width={width} + id={id} /> : ) diff --git a/src/components/TableauEmbed/TableauViz.jsx b/src/components/TableauEmbed/TableauViz.jsx index 86fefe51..d2c087cf 100644 --- a/src/components/TableauEmbed/TableauViz.jsx +++ b/src/components/TableauEmbed/TableauViz.jsx @@ -19,10 +19,12 @@ export const TableauViz = forwardRef(function Viz(props, ref) { customToolbar, layouts, height, - width + width, + id: customId } = props; // creates a unique identifier for the embed - const id = `id-${useId()}`; + const generatedId = `id-${useId()}`; + const id = customId || generatedId; // to be used if parent did not forward a ref const localRef = useRef(null); // Use the forwarded ref if provided, otherwise use the local ref @@ -69,7 +71,7 @@ export const TableauViz = forwardRef(function Viz(props, ref) { {customToolbar ? : null} Date: Thu, 16 Oct 2025 08:57:48 -0500 Subject: [PATCH 06/21] translations --- LANGUAGE_SELECTOR_GUIDE.md | 123 +++++++ TABLEAU_TRANSLATION_API.md | 342 ++++++++++++++++++ src/app/demo/veriforce/Home.jsx | 194 ++++++---- .../demo/veriforce/safety/SafetyDashboard.jsx | 3 + src/app/layout.tsx | 7 +- .../LanguageSelector/LanguageSelector.jsx | 73 ++++ src/components/LanguageSelector/index.js | 1 + src/components/Metrics/Metric.jsx | 19 +- src/components/Metrics/Metrics.jsx | 1 + src/components/index.js | 2 + src/contexts/LanguageContext.jsx | 282 +++++++++++++++ src/hooks/index.js | 1 + src/hooks/useTableauTranslation.js | 123 +++++++ src/services/tableauTranslationService.js | 172 +++++++++ src/utils/metricTranslations.js | 76 ++++ 15 files changed, 1345 insertions(+), 74 deletions(-) create mode 100644 LANGUAGE_SELECTOR_GUIDE.md create mode 100644 TABLEAU_TRANSLATION_API.md create mode 100644 src/components/LanguageSelector/LanguageSelector.jsx create mode 100644 src/components/LanguageSelector/index.js create mode 100644 src/contexts/LanguageContext.jsx create mode 100644 src/hooks/useTableauTranslation.js create mode 100644 src/services/tableauTranslationService.js create mode 100644 src/utils/metricTranslations.js diff --git a/LANGUAGE_SELECTOR_GUIDE.md b/LANGUAGE_SELECTOR_GUIDE.md new file mode 100644 index 00000000..0cf88edb --- /dev/null +++ b/LANGUAGE_SELECTOR_GUIDE.md @@ -0,0 +1,123 @@ +# Language Selector Implementation Guide + +The language selector is now available on every page through a global context. Here's how to use it: + +## 🚀 Quick Setup + +The `LanguageProvider` is already wrapped around the entire app in `src/app/layout.tsx`, so the language selector is available everywhere. + +## 📝 Adding Language Selector to Any Page + +### 1. Import the Components + +```jsx +import { LanguageSelector } from '@/components/LanguageSelector'; +import { useLanguage } from '@/contexts/LanguageContext'; +``` + +### 2. Use the Language Context + +```jsx +export const YourComponent = () => { + const { t } = useLanguage(); + + return ( +
+

{t.title}

+

{t.subtitle}

+ {/* Your content here */} +
+ ); +}; +``` + +### 3. Add the Language Selector to Your Header + +```jsx +
+ + {/* Other header elements */} +
+``` + +## 🌍 Available Languages + +- **🇺🇸 English** (default) +- **🇪🇸 Español** (Spanish) +- **🇫🇷 Français** (French) + +## 📚 Available Translations + +The `t` object contains all translated strings. Common keys include: + +- `t.title` - Page title +- `t.subtitle` - Page subtitle +- `t.close` - Close button +- `t.previous` - Previous button +- `t.next` - Next button +- `t.email` - Email label +- `t.subject` - Subject label +- `t.message` - Message label + +## 🔧 Example Implementation + +Here's a complete example of adding the language selector to a page: + +```jsx +"use client"; + +import { LanguageSelector } from '@/components/LanguageSelector'; +import { useLanguage } from '@/contexts/LanguageContext'; + +export const MyPage = () => { + const { t } = useLanguage(); + + return ( +
+ {/* Header */} +
+
+
+

{t.title}

+

{t.subtitle}

+
+
+ + +
+
+
+ + {/* Page Content */} +
+

{t.message}

+
+
+ ); +}; +``` + +## ✨ Features + +- **Global State**: Language selection persists across all pages +- **Automatic Translation**: All text updates instantly when language changes +- **Click Outside to Close**: Dropdown closes when clicking outside +- **Flag Icons**: Visual language indicators with country flags +- **Responsive Design**: Works on all screen sizes + +## 🎯 Current Pages with Language Selector + +- ✅ Home page (`/demo/veriforce`) +- ✅ Safety Dashboard (`/demo/veriforce/safety`) + +## 📝 Adding to New Pages + +To add the language selector to any new page, simply: + +1. Import `LanguageSelector` and `useLanguage` +2. Add `` to your header +3. Use `t.keyName` for any text that should be translated + +The language context is already available everywhere, so no additional setup is needed! diff --git a/TABLEAU_TRANSLATION_API.md b/TABLEAU_TRANSLATION_API.md new file mode 100644 index 00000000..9773e127 --- /dev/null +++ b/TABLEAU_TRANSLATION_API.md @@ -0,0 +1,342 @@ +# Tableau Data Translation API + +This API provides comprehensive translation capabilities for all Tableau data including metrics, dashboards, worksheets, filters, and mark selections. + +## 🚀 Quick Start + +```jsx +import { useTableauTranslation } from '@/hooks/useTableauTranslation'; + +const MyComponent = () => { + const { translateMetric, translateDashboard, translateMarks } = useTableauTranslation(); + + // Translate any Tableau data + const translatedMetric = translateMetric(metricData); + const translatedDashboard = translateDashboard(dashboardData); + const translatedMarks = translateMarks(markData); + + return
{translatedMetric.name}
; +}; +``` + +## 📚 Available Translation Functions + +### Core Translation Functions + +| Function | Description | Input | Output | +|----------|-------------|-------|--------| +| `translateData(data, dataType)` | Translate any Tableau data | `object`, `string` | `object` | +| `translateMetric(metricData)` | Translate metric data | `object` | `object` | +| `translateDashboard(dashboardData)` | Translate dashboard data | `object` | `object` | +| `translateWorksheet(worksheetData)` | Translate worksheet data | `object` | `object` | +| `translateFilter(filterData)` | Translate filter data | `object` | `object` | +| `translateMarks(markData)` | Translate mark selection data | `object` | `object` | +| `translateDataArray(dataArray, dataType)` | Translate array of data | `Array`, `string` | `Array` | + +### Utility Functions + +| Function | Description | Returns | +|----------|-------------|---------| +| `getCurrentLanguage()` | Get current language code | `string` | +| `hasTranslation(text)` | Check if text has translation | `boolean` | +| `translations` | Direct access to translations | `object` | +| `language` | Current language code | `string` | + +## 🌍 Supported Languages + +- **🇺🇸 English** (default) +- **🇪🇸 Spanish** (Español) +- **🇫🇷 French** (Français) + +## 📊 Supported Data Types + +### 1. Metrics +```jsx +const { translateMetric } = useTableauTranslation(); + +const metricData = { + name: "Total Contractors", + displayName: "Total Contractors", + description: "Number of active contractors", + value: 150, + units: "contractors" +}; + +const translatedMetric = translateMetric(metricData); +// Result: { name: "Total de Contratistas", displayName: "Total de Contratistas", ... } +``` + +### 2. Dashboards +```jsx +const { translateDashboard } = useTableauTranslation(); + +const dashboardData = { + title: "Safety Overview", + description: "Monitor safety compliance", + worksheets: [...] +}; + +const translatedDashboard = translateDashboard(dashboardData); +``` + +### 3. Worksheets +```jsx +const { translateWorksheet } = useTableauTranslation(); + +const worksheetData = { + name: "Safety Incidents", + title: "Safety Incidents Analysis", + columns: [ + { fieldName: "Incident Type", displayName: "Incident Type" }, + { fieldName: "Severity", displayName: "Severity" } + ] +}; + +const translatedWorksheet = translateWorksheet(worksheetData); +``` + +### 4. Filters +```jsx +const { translateFilter } = useTableauTranslation(); + +const filterData = { + fieldName: "Insurance Status", + displayName: "Insurance Status", + values: ["Active", "Expired", "Pending"], + options: ["All", "Active", "Expired", "Pending"] +}; + +const translatedFilter = translateFilter(filterData); +``` + +### 5. Mark Selections +```jsx +const { translateMarks } = useTableauTranslation(); + +const markData = { + columns: [ + { fieldName: "Contractor Name", displayName: "Contractor Name" }, + { fieldName: "Risk Level", displayName: "Risk Level" } + ], + data: [ + ["ABC Corp", "High Risk"], + ["XYZ Ltd", "Low Risk"] + ] +}; + +const translatedMarks = translateMarks(markData); +``` + +## 🔧 Advanced Usage + +### Batch Translation +```jsx +const { translateDataArray } = useTableauTranslation(); + +const metrics = [ + { name: "Total Contractors", value: 150 }, + { name: "Safety Incidents", value: 5 }, + { name: "Compliance Rate", value: 95 } +]; + +const translatedMetrics = translateDataArray(metrics, 'metric'); +``` + +### Conditional Translation +```jsx +const { hasTranslation, translateMetric } = useTableauTranslation(); + +const processMetric = (metric) => { + if (hasTranslation(metric.name)) { + return translateMetric(metric); + } + return metric; // Use original if no translation +}; +``` + +### Language-Specific Logic +```jsx +const { getCurrentLanguage, translateMetric } = useTableauTranslation(); + +const processData = (data) => { + const language = getCurrentLanguage(); + + if (language === 'es') { + // Spanish-specific processing + return translateMetric(data); + } else if (language === 'fr') { + // French-specific processing + return translateMetric(data); + } + + return data; +}; +``` + +## 📝 Adding New Translations + +### 1. Update Language Context +Add new metric names to `src/contexts/LanguageContext.jsx`: + +```jsx +metrics: { + "New Metric Name": "New Metric Name", + "Another Metric": "Another Metric", + // ... existing translations +} +``` + +### 2. Add Translations for All Languages +```jsx +// English +"New Metric Name": "New Metric Name", + +// Spanish +"New Metric Name": "Nuevo Nombre de Métrica", + +// French +"New Metric Name": "Nouveau Nom de Métrique", +``` + +### 3. Use in Components +```jsx +const { translateMetric } = useTableauTranslation(); +const translatedMetric = translateMetric(metricData); +``` + +## 🎯 Common Use Cases + +### 1. Tableau Dashboard Integration +```jsx +const Dashboard = () => { + const { translateDashboard } = useTableauTranslation(); + const [dashboardData, setDashboardData] = useState(null); + + useEffect(() => { + // Fetch dashboard data from Tableau + fetchDashboardData().then(data => { + const translated = translateDashboard(data); + setDashboardData(translated); + }); + }, []); + + return
{dashboardData?.title}
; +}; +``` + +### 2. Dynamic Filter Translation +```jsx +const FilterComponent = ({ filterData }) => { + const { translateFilter } = useTableauTranslation(); + const translatedFilter = translateFilter(filterData); + + return ( + + ); +}; +``` + +### 3. Mark Selection Translation +```jsx +const MarkSelectionHandler = ({ marksData }) => { + const { translateMarks } = useTableauTranslation(); + const translatedMarks = translateMarks(marksData); + + // Process translated mark data + translatedMarks.data.forEach(row => { + console.log('Translated row:', row); + }); +}; +``` + +## 🔍 Debugging + +### Check Available Translations +```jsx +const { translations, hasTranslation } = useTableauTranslation(); + +console.log('Available metrics:', translations.metrics); +console.log('Has translation for "Total Contractors":', hasTranslation("Total Contractors")); +``` + +### Verify Translation Results +```jsx +const { translateMetric, getCurrentLanguage } = useTableauTranslation(); + +const originalMetric = { name: "Total Contractors" }; +const translatedMetric = translateMetric(originalMetric); + +console.log('Language:', getCurrentLanguage()); +console.log('Original:', originalMetric.name); +console.log('Translated:', translatedMetric.name); +``` + +## ⚡ Performance Tips + +1. **Memoize translations** for large datasets +2. **Use batch translation** for arrays +3. **Check for translations** before processing +4. **Cache translated data** when possible + +```jsx +const { translateDataArray } = useTableauTranslation(); + +// Good: Batch translate +const translatedData = useMemo(() => + translateDataArray(largeDataset, 'metric'), + [largeDataset] +); + +// Avoid: Individual translations in loops +largeDataset.forEach(item => translateMetric(item)); // ❌ +``` + +## 🚀 Integration Examples + +### With Tableau Embedding API +```jsx +const TableauViz = () => { + const { translateMarks } = useTableauTranslation(); + + useEffect(() => { + const viz = document.getElementById('tableau-viz'); + + viz.addEventListener('markselectionchanged', (event) => { + event.detail.getMarksAsync().then(marks => { + const translatedMarks = translateMarks(marks); + console.log('Translated marks:', translatedMarks); + }); + }); + }, []); + + return
; +}; +``` + +### With React State +```jsx +const MetricsDashboard = () => { + const { translateMetric } = useTableauTranslation(); + const [metrics, setMetrics] = useState([]); + + const loadMetrics = async () => { + const data = await fetchMetrics(); + const translated = data.map(metric => translateMetric(metric)); + setMetrics(translated); + }; + + return ( +
+ {metrics.map(metric => ( +
{metric.name}
+ ))} +
+ ); +}; +``` + +This API provides a complete solution for translating all Tableau data in your application! 🎯 diff --git a/src/app/demo/veriforce/Home.jsx b/src/app/demo/veriforce/Home.jsx index e8b2407d..08e9ba8a 100644 --- a/src/app/demo/veriforce/Home.jsx +++ b/src/app/demo/veriforce/Home.jsx @@ -8,7 +8,8 @@ import { CardHeader, CardTitle, } from "@/components/ui"; -import { Metrics, TableauEmbed } from '@/components'; +import { Metrics, TableauEmbed, LanguageSelector } from '@/components'; +import { useLanguage } from '@/contexts/LanguageContext'; import Image from 'next/image'; import { Shield, @@ -30,6 +31,33 @@ export const Home = () => { const [emailPreviews, setEmailPreviews] = useState([]); const [currentEmailIndex, setCurrentEmailIndex] = useState(0); + // Get language context + const { t } = useLanguage(); + + + + // Prevent page jumping by maintaining scroll position + useEffect(() => { + const initialScrollY = window.scrollY; + + const preventScroll = () => { + window.scrollTo(0, initialScrollY); + }; + + // Prevent any scrolling during dashboard load + window.addEventListener('scroll', preventScroll, { passive: false }); + + // Remove after 10 seconds to allow normal scrolling + const timer = setTimeout(() => { + window.removeEventListener('scroll', preventScroll); + }, 10000); + + return () => { + clearTimeout(timer); + window.removeEventListener('scroll', preventScroll); + }; + }, []); + // Apply filter to Tableau dashboards when insurance status changes useEffect(() => { const applyFilter = async () => { @@ -222,30 +250,30 @@ export const Home = () => { // Generate email content return { to: emailAddress, - subject: 'URGENT: Insurance Status Expired - Action Required', - body: `Dear ${contractorName}, + subject: t.urgentInsuranceStatusExpired, + body: `${t.dearContractor} ${contractorName}, -This is an urgent notification regarding your insurance status. +${t.urgentNotification} -Our records indicate that your insurance coverage has EXPIRED. This requires immediate attention to maintain compliance with our contractor requirements. +${t.recordsIndicate} -Selected Record Details: +${t.selectedRecordDetails}: ${Object.entries(mark).map(([key, value]) => ` • ${key}: ${value}`).join('\n')} -Please take the following actions immediately: -1. Review your current insurance policy -2. Renew or update your insurance coverage -3. Submit updated documentation to our compliance team +${t.pleaseTakeActions} +${t.reviewPolicy} +${t.renewCoverage} +${t.submitDocumentation} -Failure to address this issue may result in suspension of contractor privileges. +${t.failureWarning} -If you have any questions or need assistance, please contact our compliance department. +${t.questionsContact} -Best regards, -Veriforce Compliance Team +${t.bestRegards} +${t.complianceTeam} --- -This is a demo email generated from Tableau mark selection.` +${t.demoEmailGenerated}` }; }); @@ -255,19 +283,39 @@ This is a demo email generated from Tableau mark selection.` }; return ( -
-
+ <> + +
+
{/* Header Section */}
+

+ {t.title} +

- Comprehensive safety and compliance tracking for your contractor network + {t.subtitle}

-
-
-
- System Healthy +
+ + +
+
+
+ System Healthy +
@@ -286,23 +334,25 @@ This is a demo email generated from Tableau mark selection.` - Compliance and Safety Executive Summary + {t.executiveSummary} - Real-time compliance tracking across all contractors and safety metrics + {t.executiveSummaryDesc} - +
+ +
@@ -314,7 +364,7 @@ This is a demo email generated from Tableau mark selection.` className="flex items-center gap-2 px-6 py-3 bg-[#CEAB73] hover:bg-[#B89558] text-white rounded-lg transition-colors shadow-lg" > - Insurance Status: {insuranceStatus.charAt(0).toUpperCase() + insuranceStatus.slice(1)} + {t.insuranceStatus}: {t[insuranceStatus]} {/* Email Button - Shows when marks are selected */} @@ -324,7 +374,7 @@ This is a demo email generated from Tableau mark selection.` className="flex items-center gap-2 px-6 py-3 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors shadow-lg animate-pulse" > - Send Expiration Notice ({selectedMarks.length}) + {t.sendExpirationNotice} ({selectedMarks.length}) )}
@@ -335,22 +385,25 @@ This is a demo email generated from Tableau mark selection.` - Compliance Center + {t.complianceCenter} - Contractor Compliance and Certification Status Overview + {t.complianceCenterDesc} + - +
+ +
@@ -431,11 +484,11 @@ This is a demo email generated from Tableau mark selection.` {showFilterPopup && (
setShowFilterPopup(false)}>
e.stopPropagation()}> -
-

- - Filter by Insurance Status -

+
+

+ + {t.filterByInsuranceStatus} +

- {['All', 'Active', 'Expired', 'Pending'].map((status) => ( + {['all', 'active', 'expired', 'pending'].map((status) => ( ))} @@ -484,7 +537,7 @@ This is a demo email generated from Tableau mark selection.`

- Insurance Expiration Notices + {t.insuranceExpirationNotices}

- Email {currentEmailIndex + 1} of {emailPreviews.length} + {t.email} {currentEmailIndex + 1} {t.of} {emailPreviews.length}
)} @@ -521,18 +574,18 @@ This is a demo email generated from Tableau mark selection.` {/* Email Header */}
- +

{emailPreviews[currentEmailIndex].to}

- +

{emailPreviews[currentEmailIndex].subject}

{/* Email Body */}
- +
                   {emailPreviews[currentEmailIndex].body}
                 
@@ -547,7 +600,7 @@ This is a demo email generated from Tableau mark selection.` }} className="px-4 py-2 bg-slate-600 hover:bg-slate-500 text-white rounded-lg transition-colors" > - Close + {t.close}
@@ -584,6 +637,7 @@ This is a demo email generated from Tableau mark selection.`
)} -
+
+ ); }; diff --git a/src/app/demo/veriforce/safety/SafetyDashboard.jsx b/src/app/demo/veriforce/safety/SafetyDashboard.jsx index 785eaa46..d70fb8c2 100644 --- a/src/app/demo/veriforce/safety/SafetyDashboard.jsx +++ b/src/app/demo/veriforce/safety/SafetyDashboard.jsx @@ -6,6 +6,8 @@ import { useSearchParams } from 'next/navigation'; import { TableauNavigation } from '../../../../components/TableauNavigation/TableauNavigation'; import { TableauEmbed } from '../../../../components/TableauEmbed'; import { DynamicDashboardViewer } from '../../../../components/TableauNavigation/DynamicDashboardViewer'; +import { LanguageSelector } from '../../../../components/LanguageSelector'; +import { useLanguage } from '../../../../contexts/LanguageContext'; import { Card, CardContent, @@ -112,6 +114,7 @@ const MainContent = ({ selectedDashboard, embedToken, siteId, showNavigation, se
+ Safety Department diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9cc1b70c..3ed0ae23 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { SessionProvider } from 'next-auth/react'; +import { LanguageProvider } from '@/contexts/LanguageContext'; import '../global.css'; @@ -25,8 +26,10 @@ export default function RootLayout({ - - {children} + + + {children} + diff --git a/src/components/LanguageSelector/LanguageSelector.jsx b/src/components/LanguageSelector/LanguageSelector.jsx new file mode 100644 index 00000000..5886cc8a --- /dev/null +++ b/src/components/LanguageSelector/LanguageSelector.jsx @@ -0,0 +1,73 @@ +"use client"; + +import { Globe, ChevronDown } from 'lucide-react'; +import { useLanguage } from '@/contexts/LanguageContext'; + +export const LanguageSelector = () => { + const { + language, + setLanguage, + showLanguageDropdown, + setShowLanguageDropdown + } = useLanguage(); + + return ( +
+ + + {showLanguageDropdown && ( +
+
+ + + +
+
+ )} +
+ ); +}; diff --git a/src/components/LanguageSelector/index.js b/src/components/LanguageSelector/index.js new file mode 100644 index 00000000..493cac82 --- /dev/null +++ b/src/components/LanguageSelector/index.js @@ -0,0 +1 @@ +export { LanguageSelector } from './LanguageSelector'; diff --git a/src/components/Metrics/Metric.jsx b/src/components/Metrics/Metric.jsx index ce1d82a2..9a78b3fa 100644 --- a/src/components/Metrics/Metric.jsx +++ b/src/components/Metrics/Metric.jsx @@ -7,13 +7,16 @@ import { Skeleton } from "components/ui"; import { Badge } from "components/ui"; import { Dialog, DialogTrigger } from "components/ui"; -import { useInsights } from "hooks"; +import { useInsights, useTableauTranslation } from "hooks"; import { parseInsights } from "utils"; import { InsightsModal } from "components"; export const Metric = (props) => { const { metric } = props; + // Get translation utilities + const { translateMetric } = useTableauTranslation(); + // distinct count of insights const [bundleCount, setBundleCount] = useState(null); let result; // contains question, markup and facts @@ -113,12 +116,24 @@ export const Metric = (props) => { const Stats = (props) => { const { isSuccess, stats, bundleCount, metric } = props; + // Get translation utilities + const { translateMetric } = useTableauTranslation(); if (isSuccess) { + // Debug: Log the original metric name + console.log('🔍 Original metric name:', metric.name); + console.log('🔍 Full metric object:', metric); + + // Translate the entire metric object + const translatedMetric = translateMetric(metric); + + // Debug: Log the translated metric name + console.log('🌍 Translated metric name:', translatedMetric.name); + return (

- {metric.name} + {translatedMetric.name}

diff --git a/src/components/Metrics/Metrics.jsx b/src/components/Metrics/Metrics.jsx index 9a5c8ff5..07e04873 100644 --- a/src/components/Metrics/Metrics.jsx +++ b/src/components/Metrics/Metrics.jsx @@ -13,6 +13,7 @@ import { import { useMetrics } from 'hooks'; import { Metric } from "components"; import { sortPayloadByIds } from './utils'; +import { useLanguage } from '@/contexts/LanguageContext'; export const Metrics = (props) => { const { basis, sortOrder } = props; diff --git a/src/components/index.js b/src/components/index.js index 7dc2afd6..b807c1d0 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -54,3 +54,5 @@ export { AuthGuard } from './AuthGuard'; export { TableauNavigation } from './TableauNavigation'; export { DynamicDashboardViewer } from './TableauNavigation/DynamicDashboardViewer'; + +export { LanguageSelector } from './LanguageSelector'; diff --git a/src/contexts/LanguageContext.jsx b/src/contexts/LanguageContext.jsx new file mode 100644 index 00000000..3e831322 --- /dev/null +++ b/src/contexts/LanguageContext.jsx @@ -0,0 +1,282 @@ +"use client"; + +import { createContext, useContext, useState, useEffect } from 'react'; + +const LanguageContext = createContext(); + +export const useLanguage = () => { + const context = useContext(LanguageContext); + if (!context) { + throw new Error('useLanguage must be used within a LanguageProvider'); + } + return context; +}; + +export const LanguageProvider = ({ children }) => { + const [language, setLanguage] = useState('en'); + const [showLanguageDropdown, setShowLanguageDropdown] = useState(false); + + // Language translations + const translations = { + en: { + title: "Veriforce Contractor Risk Management", + subtitle: "Comprehensive safety and compliance tracking dashboard with real-time alerts and self-service analytics", + executiveSummary: "Executive Summary", + executiveSummaryDesc: "Real-time compliance tracking across all contractors and safety metrics", + complianceCenter: "Compliance Center", + complianceCenterDesc: "Contractor Compliance and Certification Status Overview", + insuranceStatus: "Insurance Status", + all: "All", + active: "Active", + expired: "Expired", + pending: "Pending", + sendExpirationNotice: "Send Expiration Notice", + filterByInsuranceStatus: "Filter by Insurance Status", + showAllInsuranceStatuses: "Show all insurance statuses", + currentlyActiveInsurance: "Currently active insurance", + expiredInsurancePolicies: "Expired insurance policies", + pendingInsuranceVerification: "Pending insurance verification", + insuranceExpirationNotices: "Insurance Expiration Notices", + previous: "Previous", + next: "Next", + email: "Email", + of: "of", + to: "To", + subject: "Subject", + message: "Message", + close: "Close", + sendThisEmail: "Send This Email", + sendAll: "Send All", + demoMode: "Demo Mode", + noActualEmails: "No actual emails will be sent. You have", + notificationsReady: "notification(s) ready to send.", + urgentInsuranceStatusExpired: "URGENT: Insurance Status Expired - Action Required", + dearContractor: "Dear", + urgentNotification: "This is an urgent notification regarding your insurance status.", + recordsIndicate: "Our records indicate that your insurance coverage has EXPIRED. This requires immediate attention to maintain compliance with our contractor requirements.", + selectedRecordDetails: "Selected Record Details", + pleaseTakeActions: "Please take the following actions immediately:", + reviewPolicy: "1. Review your current insurance policy", + renewCoverage: "2. Renew or update your insurance coverage", + submitDocumentation: "3. Submit updated documentation to our compliance team", + failureWarning: "Failure to address this issue may result in suspension of contractor privileges.", + questionsContact: "If you have any questions or need assistance, please contact our compliance department.", + bestRegards: "Best regards,", + complianceTeam: "Veriforce Compliance Team", + demoEmailGenerated: "This is a demo email generated from Tableau mark selection.", + // Metrics translations + metrics: { + // Common metric names that might come from Tableau + "Total Contractors": "Total Contractors", + "Active Projects": "Active Projects", + "Safety Incidents": "Safety Incidents", + "Compliance Rate": "Compliance Rate", + "Insurance Expired": "Insurance Expired", + "Training Completed": "Training Completed", + "Risk Score": "Risk Score", + "Certifications": "Certifications", + "Violations": "Violations", + "Inspections": "Inspections", + "Renewals": "Renewals", + "Approvals": "Approvals", + "Rejections": "Rejections", + "Pending": "Pending", + "Overdue": "Overdue", + "Completed": "Completed", + "In Progress": "In Progress", + "Not Started": "Not Started", + "High Risk": "High Risk", + "Medium Risk": "Medium Risk", + "Low Risk": "Low Risk", + "Critical": "Critical", + "Warning": "Warning", + "Info": "Info", + "Success": "Success", + "Error": "Error" + } + }, + es: { + title: "Gestión de Riesgos de Contratistas Veriforce", + subtitle: "Panel de seguimiento integral de seguridad y cumplimiento con alertas en tiempo real y análisis de autoservicio", + executiveSummary: "Resumen Ejecutivo", + executiveSummaryDesc: "Seguimiento de cumplimiento en tiempo real en todos los contratistas y métricas de seguridad", + complianceCenter: "Centro de Cumplimiento", + complianceCenterDesc: "Resumen del Estado de Cumplimiento y Certificación de Contratistas", + insuranceStatus: "Estado del Seguro", + all: "Todos", + active: "Activo", + expired: "Vencido", + pending: "Pendiente", + sendExpirationNotice: "Enviar Aviso de Vencimiento", + filterByInsuranceStatus: "Filtrar por Estado del Seguro", + showAllInsuranceStatuses: "Mostrar todos los estados del seguro", + currentlyActiveInsurance: "Seguro actualmente activo", + expiredInsurancePolicies: "Pólizas de seguro vencidas", + pendingInsuranceVerification: "Verificación de seguro pendiente", + insuranceExpirationNotices: "Avisos de Vencimiento de Seguro", + previous: "Anterior", + next: "Siguiente", + email: "Correo", + of: "de", + to: "Para", + subject: "Asunto", + message: "Mensaje", + close: "Cerrar", + sendThisEmail: "Enviar Este Correo", + sendAll: "Enviar Todos", + demoMode: "Modo Demo", + noActualEmails: "No se enviarán correos reales. Tienes", + notificationsReady: "notificación(es) lista(s) para enviar.", + urgentInsuranceStatusExpired: "URGENTE: Estado del Seguro Vencido - Acción Requerida", + dearContractor: "Estimado", + urgentNotification: "Esta es una notificación urgente sobre el estado de su seguro.", + recordsIndicate: "Nuestros registros indican que su cobertura de seguro ha VENCIDO. Esto requiere atención inmediata para mantener el cumplimiento con nuestros requisitos de contratistas.", + selectedRecordDetails: "Detalles del Registro Seleccionado", + pleaseTakeActions: "Por favor tome las siguientes acciones inmediatamente:", + reviewPolicy: "1. Revise su póliza de seguro actual", + renewCoverage: "2. Renueve o actualice su cobertura de seguro", + submitDocumentation: "3. Envíe documentación actualizada a nuestro equipo de cumplimiento", + failureWarning: "La falta de atención a este problema puede resultar en la suspensión de los privilegios de contratista.", + questionsContact: "Si tiene alguna pregunta o necesita asistencia, por favor contacte a nuestro departamento de cumplimiento.", + bestRegards: "Saludos cordiales,", + complianceTeam: "Equipo de Cumplimiento Veriforce", + demoEmailGenerated: "Este es un correo de demostración generado desde la selección de marcas de Tableau.", + // Metrics translations + metrics: { + "Total Contractors": "Total de Contratistas", + "Active Projects": "Proyectos Activos", + "Safety Incidents": "Incidentes de Seguridad", + "Compliance Rate": "Tasa de Cumplimiento", + "Insurance Expired": "Seguro Vencido", + "Training Completed": "Capacitación Completada", + "Risk Score": "Puntuación de Riesgo", + "Certifications": "Certificaciones", + "Violations": "Violaciones", + "Inspections": "Inspecciones", + "Renewals": "Renovaciones", + "Approvals": "Aprobaciones", + "Rejections": "Rechazos", + "Pending": "Pendiente", + "Overdue": "Vencido", + "Completed": "Completado", + "In Progress": "En Progreso", + "Not Started": "No Iniciado", + "High Risk": "Alto Riesgo", + "Medium Risk": "Riesgo Medio", + "Low Risk": "Bajo Riesgo", + "Critical": "Crítico", + "Warning": "Advertencia", + "Info": "Información", + "Success": "Éxito", + "Error": "Error" + } + }, + fr: { + title: "Gestion des Risques Contractuels Veriforce", + subtitle: "Tableau de bord de suivi complet de la sécurité et de la conformité avec alertes en temps réel et analyses en libre-service", + executiveSummary: "Résumé Exécutif", + executiveSummaryDesc: "Suivi de conformité en temps réel sur tous les entrepreneurs et métriques de sécurité", + complianceCenter: "Centre de Conformité", + complianceCenterDesc: "Aperçu du Statut de Conformité et de Certification des Entrepreneurs", + insuranceStatus: "Statut d'Assurance", + all: "Tous", + active: "Actif", + expired: "Expiré", + pending: "En Attente", + sendExpirationNotice: "Envoyer Avis d'Expiration", + filterByInsuranceStatus: "Filtrer par Statut d'Assurance", + showAllInsuranceStatuses: "Afficher tous les statuts d'assurance", + currentlyActiveInsurance: "Assurance actuellement active", + expiredInsurancePolicies: "Polices d'assurance expirées", + pendingInsuranceVerification: "Vérification d'assurance en attente", + insuranceExpirationNotices: "Avis d'Expiration d'Assurance", + previous: "Précédent", + next: "Suivant", + email: "Email", + of: "de", + to: "À", + subject: "Sujet", + message: "Message", + close: "Fermer", + sendThisEmail: "Envoyer Cet Email", + sendAll: "Tout Envoyer", + demoMode: "Mode Démo", + noActualEmails: "Aucun email réel ne sera envoyé. Vous avez", + notificationsReady: "notification(s) prête(s) à envoyer.", + urgentInsuranceStatusExpired: "URGENT: Statut d'Assurance Expiré - Action Requise", + dearContractor: "Cher", + urgentNotification: "Ceci est une notification urgente concernant votre statut d'assurance.", + recordsIndicate: "Nos dossiers indiquent que votre couverture d'assurance a EXPIRÉ. Cela nécessite une attention immédiate pour maintenir la conformité avec nos exigences contractuelles.", + selectedRecordDetails: "Détails du Registre Sélectionné", + pleaseTakeActions: "Veuillez prendre les mesures suivantes immédiatement:", + reviewPolicy: "1. Examinez votre police d'assurance actuelle", + renewCoverage: "2. Renouvelez ou mettez à jour votre couverture d'assurance", + submitDocumentation: "3. Soumettez la documentation mise à jour à notre équipe de conformité", + failureWarning: "Le fait de ne pas résoudre ce problème peut entraîner la suspension des privilèges contractuels.", + questionsContact: "Si vous avez des questions ou avez besoin d'assistance, veuillez contacter notre département de conformité.", + bestRegards: "Cordialement,", + complianceTeam: "Équipe de Conformité Veriforce", + demoEmailGenerated: "Ceci est un email de démonstration généré à partir de la sélection de marques Tableau.", + // Metrics translations + metrics: { + "Total Contractors": "Total des Entrepreneurs", + "Active Projects": "Projets Actifs", + "Safety Incidents": "Incidents de Sécurité", + "Compliance Rate": "Taux de Conformité", + "Insurance Expired": "Assurance Expirée", + "Training Completed": "Formation Terminée", + "Risk Score": "Score de Risque", + "Certifications": "Certifications", + "Violations": "Violations", + "Inspections": "Inspections", + "Renewals": "Renouvellements", + "Approvals": "Approbations", + "Rejections": "Rejets", + "Pending": "En Attente", + "Overdue": "En Retard", + "Completed": "Terminé", + "In Progress": "En Cours", + "Not Started": "Non Commencé", + "High Risk": "Risque Élevé", + "Medium Risk": "Risque Moyen", + "Low Risk": "Faible Risque", + "Critical": "Critique", + "Warning": "Avertissement", + "Info": "Information", + "Success": "Succès", + "Error": "Erreur" + } + } + }; + + const t = translations[language]; + + // Close language dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if (showLanguageDropdown && !event.target.closest('.language-selector')) { + setShowLanguageDropdown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showLanguageDropdown]); + + const value = { + language, + setLanguage, + showLanguageDropdown, + setShowLanguageDropdown, + t, + translations + }; + + return ( + + {children} + + ); +}; diff --git a/src/hooks/index.js b/src/hooks/index.js index 53b8bb89..3e7865bf 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -4,3 +4,4 @@ export { useInsights } from './useInsights'; export { useTimeout } from './useTimeout'; export { useMetadata } from './useMetadata'; export { useXSQuery, useSMQuery, useMDQuery, useLGQuery, useXLQuery, use2XLQuery } from './useMediaQueries'; +export { useTableauTranslation } from './useTableauTranslation'; diff --git a/src/hooks/useTableauTranslation.js b/src/hooks/useTableauTranslation.js new file mode 100644 index 00000000..b453d7ce --- /dev/null +++ b/src/hooks/useTableauTranslation.js @@ -0,0 +1,123 @@ +"use client"; + +import { useLanguage } from '@/contexts/LanguageContext'; +import { + translateTableauData, + translateTableauMetric, + translateTableauDashboard, + translateTableauWorksheet, + translateTableauFilter, + translateTableauMarks +} from '@/services/tableauTranslationService'; + +/** + * Custom hook for translating Tableau data + * @returns {object} - Translation functions and utilities + */ +export const useTableauTranslation = () => { + const { t, language } = useLanguage(); + + /** + * Translate any Tableau data object + * @param {object} data - The data to translate + * @param {string} dataType - Type of data ('metric', 'dashboard', 'worksheet', 'filter', 'marks') + * @returns {object} - Translated data + */ + const translateData = (data, dataType = 'metric') => { + return translateTableauData(data, t, dataType); + }; + + /** + * Translate Tableau metric data + * @param {object} metricData - The metric data to translate + * @returns {object} - Translated metric data + */ + const translateMetric = (metricData) => { + return translateTableauMetric(metricData, t); + }; + + /** + * Translate Tableau dashboard data + * @param {object} dashboardData - The dashboard data to translate + * @returns {object} - Translated dashboard data + */ + const translateDashboard = (dashboardData) => { + return translateTableauDashboard(dashboardData, t); + }; + + /** + * Translate Tableau worksheet data + * @param {object} worksheetData - The worksheet data to translate + * @returns {object} - Translated worksheet data + */ + const translateWorksheet = (worksheetData) => { + return translateTableauWorksheet(worksheetData, t); + }; + + /** + * Translate Tableau filter data + * @param {object} filterData - The filter data to translate + * @returns {object} - Translated filter data + */ + const translateFilter = (filterData) => { + return translateTableauFilter(filterData, t); + }; + + /** + * Translate Tableau mark selection data + * @param {object} markData - The mark data to translate + * @returns {object} - Translated mark data + */ + const translateMarks = (markData) => { + return translateTableauMarks(markData, t); + }; + + /** + * Translate an array of Tableau data objects + * @param {Array} dataArray - Array of data objects to translate + * @param {string} dataType - Type of data for all objects + * @returns {Array} - Array of translated data objects + */ + const translateDataArray = (dataArray, dataType = 'metric') => { + if (!Array.isArray(dataArray)) { + return dataArray; + } + return dataArray.map(data => translateData(data, dataType)); + }; + + /** + * Get the current language code + * @returns {string} - Current language code ('en', 'es', 'fr') + */ + const getCurrentLanguage = () => { + return language; + }; + + /** + * Check if a specific text has a translation + * @param {string} text - The text to check + * @returns {boolean} - True if translation exists + */ + const hasTranslation = (text) => { + return !!(t?.metrics && t.metrics[text]); + }; + + return { + // Translation functions + translateData, + translateMetric, + translateDashboard, + translateWorksheet, + translateFilter, + translateMarks, + translateDataArray, + + // Utility functions + getCurrentLanguage, + hasTranslation, + + // Direct access to translations + translations: t, + language + }; +}; diff --git a/src/services/tableauTranslationService.js b/src/services/tableauTranslationService.js new file mode 100644 index 00000000..3c4e09c5 --- /dev/null +++ b/src/services/tableauTranslationService.js @@ -0,0 +1,172 @@ +/** + * Tableau Data Translation Service + * Provides comprehensive translation capabilities for Tableau data including: + * - Metric names and values + * - Dashboard titles and descriptions + * - Filter options and labels + * - Chart labels and legends + * - Tooltip text and messages + */ + +import { translateMetricName, translateMetricValue } from '../utils/metricTranslations'; + +/** + * Translate Tableau metric data + * @param {object} metricData - The metric data from Tableau + * @param {object} translations - The translations object from language context + * @returns {object} - Translated metric data + */ +export const translateTableauMetric = (metricData, translations) => { + if (!metricData || !translations?.metrics) { + console.log('🚫 No metric data or translations available'); + return metricData; + } + + console.log('🔄 Translating metric:', metricData.name); + console.log('📚 Available translations:', Object.keys(translations.metrics)); + + const translated = { + ...metricData, + name: translateMetricName(metricData.name, translations), + displayName: translateMetricName(metricData.displayName || metricData.name, translations), + description: translateMetricName(metricData.description, translations), + // Translate any nested values that might contain text + values: metricData.values ? metricData.values.map(value => + typeof value === 'string' ? translateMetricValue(value, translations) : value + ) : metricData.values + }; + + console.log('✅ Translation result:', translated.name); + return translated; +}; + +/** + * Translate Tableau dashboard data + * @param {object} dashboardData - The dashboard data from Tableau + * @param {object} translations - The translations object from language context + * @returns {object} - Translated dashboard data + */ +export const translateTableauDashboard = (dashboardData, translations) => { + if (!dashboardData || !translations) { + return dashboardData; + } + + return { + ...dashboardData, + title: translateMetricName(dashboardData.title, translations), + description: translateMetricName(dashboardData.description, translations), + // Translate any worksheets or sheets + worksheets: dashboardData.worksheets ? dashboardData.worksheets.map(worksheet => + translateTableauWorksheet(worksheet, translations) + ) : dashboardData.worksheets + }; +}; + +/** + * Translate Tableau worksheet data + * @param {object} worksheetData - The worksheet data from Tableau + * @param {object} translations - The translations object from language context + * @returns {object} - Translated worksheet data + */ +export const translateTableauWorksheet = (worksheetData, translations) => { + if (!worksheetData || !translations?.metrics) { + return worksheetData; + } + + return { + ...worksheetData, + name: translateMetricName(worksheetData.name, translations), + title: translateMetricName(worksheetData.title, translations), + // Translate any columns or fields + columns: worksheetData.columns ? worksheetData.columns.map(column => ({ + ...column, + fieldName: translateMetricName(column.fieldName, translations), + displayName: translateMetricName(column.displayName || column.fieldName, translations) + })) : worksheetData.columns + }; +}; + +/** + * Translate Tableau filter data + * @param {object} filterData - The filter data from Tableau + * @param {object} translations - The translations object from language context + * @returns {object} - Translated filter data + */ +export const translateTableauFilter = (filterData, translations) => { + if (!filterData || !translations?.metrics) { + return filterData; + } + + return { + ...filterData, + fieldName: translateMetricName(filterData.fieldName, translations), + displayName: translateMetricName(filterData.displayName || filterData.fieldName, translations), + // Translate filter values + values: filterData.values ? filterData.values.map(value => + typeof value === 'string' ? translateMetricValue(value, translations) : value + ) : filterData.values, + // Translate any options + options: filterData.options ? filterData.options.map(option => + typeof option === 'string' ? translateMetricValue(option, translations) : option + ) : filterData.options + }; +}; + +/** + * Translate Tableau mark selection data + * @param {object} markData - The mark selection data from Tableau + * @param {object} translations - The translations object from language context + * @returns {object} - Translated mark data + */ +export const translateTableauMarks = (markData, translations) => { + if (!markData || !translations?.metrics) { + return markData; + } + + return { + ...markData, + // Translate columns + columns: markData.columns ? markData.columns.map(column => ({ + ...column, + fieldName: translateMetricName(column.fieldName, translations), + displayName: translateMetricName(column.displayName || column.fieldName, translations) + })) : markData.columns, + // Translate data values + data: markData.data ? markData.data.map(row => + row.map(cell => + typeof cell === 'string' ? translateMetricValue(cell, translations) : cell + ) + ) : markData.data + }; +}; + +/** + * Translate any Tableau data object + * @param {object} data - Any Tableau data object + * @param {object} translations - The translations object from language context + * @param {string} dataType - Type of data ('metric', 'dashboard', 'worksheet', 'filter', 'marks') + * @returns {object} - Translated data + */ +export const translateTableauData = (data, translations, dataType = 'metric') => { + if (!data || !translations) { + return data; + } + + switch (dataType) { + case 'metric': + return translateTableauMetric(data, translations); + case 'dashboard': + return translateTableauDashboard(data, translations); + case 'worksheet': + return translateTableauWorksheet(data, translations); + case 'filter': + return translateTableauFilter(data, translations); + case 'marks': + return translateTableauMarks(data, translations); + default: + return data; + } +}; + +// Re-export the utility functions +export { translateMetricName, translateMetricValue } from '../utils/metricTranslations'; diff --git a/src/utils/metricTranslations.js b/src/utils/metricTranslations.js new file mode 100644 index 00000000..71bd8742 --- /dev/null +++ b/src/utils/metricTranslations.js @@ -0,0 +1,76 @@ +/** + * Utility function to translate metric names from Tableau data + * @param {string} metricName - The original metric name from Tableau + * @param {object} translations - The translations object from language context + * @returns {string} - The translated metric name or original if no translation found + */ +export const translateMetricName = (metricName, translations) => { + if (!metricName || !translations?.metrics) { + console.log('🚫 No metric name or translations available'); + return metricName; + } + + console.log('🔍 Looking for translation for:', metricName); + console.log('📚 Available metric keys:', Object.keys(translations.metrics)); + + // Direct lookup in metrics translations + if (translations.metrics[metricName]) { + console.log('✅ Direct match found:', translations.metrics[metricName]); + return translations.metrics[metricName]; + } + + // Try to find partial matches for dynamic metric names + const metricKeys = Object.keys(translations.metrics); + + // Look for partial matches (useful for dynamic metric names) + for (const key of metricKeys) { + if (metricName.toLowerCase().includes(key.toLowerCase()) || + key.toLowerCase().includes(metricName.toLowerCase())) { + console.log('✅ Partial match found:', key, '->', translations.metrics[key]); + return translations.metrics[key]; + } + } + + // If no translation found, return original name + console.log('❌ No translation found for:', metricName); + return metricName; +}; + +/** + * Utility function to translate metric values/units + * @param {string} value - The value to translate + * @param {object} translations - The translations object from language context + * @returns {string} - The translated value or original if no translation found + */ +export const translateMetricValue = (value, translations) => { + if (!value || !translations?.metrics) { + return value; + } + + // Direct lookup in metrics translations + if (translations.metrics[value]) { + return translations.metrics[value]; + } + + return value; +}; + +/** + * Utility function to translate all metric-related text + * @param {object} metric - The metric object from Tableau + * @param {object} translations - The translations object from language context + * @returns {object} - The metric object with translated text + */ +export const translateMetric = (metric, translations) => { + if (!metric || !translations?.metrics) { + return metric; + } + + return { + ...metric, + name: translateMetricName(metric.name, translations), + // Add other fields that might need translation + displayName: translateMetricName(metric.displayName || metric.name, translations), + description: translateMetricName(metric.description, translations) + }; +}; From fb6461b5b096496971c6ee7c913115b169370dbe Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Thu, 16 Oct 2025 13:09:20 -0500 Subject: [PATCH 07/21] questions --- ...90XncbEU5PwfWbtdvoZfPbdSlxTLVQV7OBuOcKe7vU | 8 ++ ...cbEU5PwfWbtdvoZfPbdSlxTLVQV7OBuOcKe7vU.pub | 1 + src/app/demo/veriforce/Home.jsx | 112 ++++++------------ src/app/demo/veriforce/config.js | 8 +- 4 files changed, 47 insertions(+), 82 deletions(-) create mode 100644 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 create mode 100644 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.pub diff --git a/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 b/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 new file mode 100644 index 00000000..2b7778a8 --- /dev/null +++ b/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 @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDrWpY2DG +LS76oMng5F90DBAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKTlmV/tcXfrlxWG +UH+xdR0uofwQNjJczPEoc1hr9GHPAAAAoMgND3S4JelppliKkIVPGxVmdYm0BsLZofP+NT +W5i4Arv/y741ji++C8m55aKepwXP86Q+pvMOVq4PkLwsDce+1WhGlbOrzi6PXSLjjguwDp +CmPLzuqVmKbLodAdFAWPAvGD7EekpshoX2WcmHNTyHuoSKCVvzH6OFkX8jtepbPyECU0Qp +o/X+YzhZ2UJ/g7WEk3oFqm4ax0QV8uQx6FbS8= +-----END OPENSSH PRIVATE KEY----- diff --git a/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.pub b/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.pub new file mode 100644 index 00000000..052e0278 --- /dev/null +++ b/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.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKTlmV/tcXfrlxWGUH+xdR0uofwQNjJczPEoc1hr9GHP allisonreynoldsc@gmail.com diff --git a/src/app/demo/veriforce/Home.jsx b/src/app/demo/veriforce/Home.jsx index 08e9ba8a..1384e329 100644 --- a/src/app/demo/veriforce/Home.jsx +++ b/src/app/demo/veriforce/Home.jsx @@ -34,30 +34,6 @@ export const Home = () => { // Get language context const { t } = useLanguage(); - - - // Prevent page jumping by maintaining scroll position - useEffect(() => { - const initialScrollY = window.scrollY; - - const preventScroll = () => { - window.scrollTo(0, initialScrollY); - }; - - // Prevent any scrolling during dashboard load - window.addEventListener('scroll', preventScroll, { passive: false }); - - // Remove after 10 seconds to allow normal scrolling - const timer = setTimeout(() => { - window.removeEventListener('scroll', preventScroll); - }, 10000); - - return () => { - clearTimeout(timer); - window.removeEventListener('scroll', preventScroll); - }; - }, []); - // Apply filter to Tableau dashboards when insurance status changes useEffect(() => { const applyFilter = async () => { @@ -283,21 +259,8 @@ ${t.demoEmailGenerated}` }; return ( - <> - -
-
+
+
{/* Header Section */}
@@ -341,18 +304,16 @@ ${t.demoEmailGenerated}` -
- -
+
@@ -364,7 +325,7 @@ ${t.demoEmailGenerated}` className="flex items-center gap-2 px-6 py-3 bg-[#CEAB73] hover:bg-[#B89558] text-white rounded-lg transition-colors shadow-lg" > - {t.insuranceStatus}: {t[insuranceStatus]} + Insurance Status: {insuranceStatus.charAt(0).toUpperCase() + insuranceStatus.slice(1)} {/* Email Button - Shows when marks are selected */} @@ -374,7 +335,7 @@ ${t.demoEmailGenerated}` className="flex items-center gap-2 px-6 py-3 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors shadow-lg animate-pulse" > - {t.sendExpirationNotice} ({selectedMarks.length}) + Send Expiration Notice ({selectedMarks.length}) )}
@@ -392,18 +353,16 @@ ${t.demoEmailGenerated}` -
- -
+
@@ -484,11 +443,11 @@ ${t.demoEmailGenerated}` {showFilterPopup && (
setShowFilterPopup(false)}>
e.stopPropagation()}> -
-

- - {t.filterByInsuranceStatus} -

+
+

+ + {t.filterByInsuranceStatus} +

))} @@ -637,7 +596,6 @@ ${t.demoEmailGenerated}`
)} -
- +
); }; diff --git a/src/app/demo/veriforce/config.js b/src/app/demo/veriforce/config.js index 9e97ce46..7cc6f3ff 100644 --- a/src/app/demo/veriforce/config.js +++ b/src/app/demo/veriforce/config.js @@ -21,11 +21,9 @@ export const settings = { ai_chat: true, ai_avatar: '/img/themes/veriforce/veriforce-logo.jpeg', sample_questions: [ - "Show me contractors with safety compliance issues", - "What's our overall risk score across all contractors?", - "Which contractors need certification renewals?", - "List all procurement metrics for this quarter", - "Show me the compliance dashboard for safety team" + "What are the total sales across each region?", + "What's our profit margin by category?", + "List the Datasources", ], sections: [ { From fb78a1058b7339d329bf974a114e2dd32e4b5795 Mon Sep 17 00:00:00 2001 From: Allison Bierschenk Date: Thu, 16 Oct 2025 13:12:04 -0500 Subject: [PATCH 08/21] fix filter --- src/app/demo/veriforce/Home.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/demo/veriforce/Home.jsx b/src/app/demo/veriforce/Home.jsx index 1384e329..6f9839e3 100644 --- a/src/app/demo/veriforce/Home.jsx +++ b/src/app/demo/veriforce/Home.jsx @@ -457,7 +457,7 @@ ${t.demoEmailGenerated}`
- {['all', 'active', 'expired', 'pending'].map((status) => ( + {['All', 'Active', 'Expired', 'Pending'].map((status) => ( - {/* Email Button - Shows when marks are selected */} + {/* Action Button - Shows when marks are selected */} {selectedMarks.length > 0 && ( )}
@@ -353,16 +459,18 @@ ${t.demoEmailGenerated}` - +
+ +
@@ -596,6 +704,92 @@ ${t.demoEmailGenerated}`
)} -
+ + {/* Slack Message Modal - For Mike Chen */} + {showSlackModal && ( +
setShowSlackModal(false)}> +
e.stopPropagation()}> +
+ + +
+ +
+ {/* Slack Header */} +
+
+ Slack +
+

Mike Chen

+

to #safety-team

+
+
+
+ + {/* Slack Message Body */} +
+ +