Its been a while
@@ -7,8 +7,13 @@ WORKDIR /usr/src/app
|
|||||||
# Copy package.json and package-lock.json (or yarn.lock/pnpm-lock.yaml)
|
# Copy package.json and package-lock.json (or yarn.lock/pnpm-lock.yaml)
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
|
ARG NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
|
||||||
|
ENV NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=$NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm install --legacy-peer-deps
|
RUN npm install
|
||||||
|
|
||||||
|
# RUN build
|
||||||
|
|
||||||
# Copy the rest of your application code
|
# Copy the rest of your application code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<div
|
||||||
|
class="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-col text-8xl font-bold text-[#2A4B6F] space-y-30">
|
||||||
|
<div>
|
||||||
|
<span class="[text-shadow:0_0_20px_#fff,0_0_50px_#0090D9,0_0_80px_#0ff] text-[#04153E]">B</span>acl<span class="relative inline-block">i<span class="absolute top-0 right-0 h-5 w-5 rounded-full bg-[#CD7F32] translate-y-[12px] -translate-x-[4px]"></span></span>ght
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 class="mt-10 text-center text-2xl font-extrabold text-[#CD7F32]">
|
||||||
|
Britton Ancillary Center
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { data: { turbo: false} }) do |f| %>
|
||||||
|
<div class="mt-8 space-y-6">
|
||||||
|
<input type="hidden" name="remember" value="true" />
|
||||||
|
<div class="rounded-md shadow-sm -space-y-px">
|
||||||
|
<div>
|
||||||
|
<%= f.label :email, class: "sr-only", for: "email-address" %>
|
||||||
|
<%= f.email_field :email, id: "email-address", autofocus: true, autocomplete: "email", placeholder: "Email address", class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" %>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<%= f.label :password, for: "password", class: "sr-only" %>
|
||||||
|
<%= f.password_field :password, id: "password", autocomplete: "current-password", placeholder: "Password", class: "appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if devise_mapping.rememberable? %>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="field flex items-center">
|
||||||
|
<%= f.check_box :remember_me, class: "h-4 w-4 text-[#CD7F32] focus:ring-indigo-500 border-[#CD7F32] rounded" %>
|
||||||
|
<%= f.label :remember_me, class: "ml-2 block font-semibold text-md text-[#CD7F32]" %>
|
||||||
|
</div>
|
||||||
|
<div class="text-md">
|
||||||
|
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||||
|
<%= link_to "Forgot your password?", new_password_path(resource_name), class: "font-semibold text-atmosphere hover:text-bluemana" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div>
|
||||||
|
<%= f.submit "Sign in", { class: "relative w-full flex justify-center py-2 px-4 border-4 font-bold rounded-lg text-platinum" } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -5,6 +5,15 @@ services:
|
|||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile.dev
|
||||||
ports:
|
ports:
|
||||||
- "3001:3001"
|
- "3001:3001"
|
||||||
|
environment:
|
||||||
|
- BE_API_SERVER_HOST=web_api
|
||||||
|
- BE_API_SERVER_PORT=3005
|
||||||
volumes:
|
volumes:
|
||||||
- .:/usr/src/app # Mount the current directory into the container for live reloading
|
- .:/usr/src/app # Mount the current directory into the container for live reloading
|
||||||
- /usr/src/app/node_modules # Isolate node_modules to prevent host-container conflicts
|
- /usr/src/app/node_modules # Isolate node_modules to prevent host-container conflicts
|
||||||
|
networks:
|
||||||
|
- britton_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
britton_network:
|
||||||
|
external: true
|
||||||
@@ -1,7 +1,19 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
// async rewrites() {
|
||||||
|
// return [
|
||||||
|
// {
|
||||||
|
// source: '/api-backend/:path*',
|
||||||
|
// destination: `http://web_api:3005`, // The external api URL
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
// },
|
||||||
|
outputFileTracingIncludes: {
|
||||||
|
'/api/**/*': ['./node_modules/jose/**/*'],
|
||||||
|
},
|
||||||
|
allowedDevOrigins: [`${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}`]
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,11 @@
|
|||||||
"@apollo/client": "^3.13.9",
|
"@apollo/client": "^3.13.9",
|
||||||
"@apollo/experimental-nextjs-app-support": "^0.12.3",
|
"@apollo/experimental-nextjs-app-support": "^0.12.3",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
|
"jose": "^6.2.3",
|
||||||
"next": "15.3.4",
|
"next": "15.3.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-idle-timer": "^5.7.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-places-autocomplete": "^7.3.0"
|
"react-places-autocomplete": "^7.3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 184.69333 172.64"
|
||||||
|
height="172.64"
|
||||||
|
width="184.69333"
|
||||||
|
xml:space="preserve"
|
||||||
|
id="svg2"
|
||||||
|
version="1.1"
|
||||||
|
sodipodi:docname="Cigna_deepcove.svg"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"><sodipodi:namedview
|
||||||
|
id="namedview27"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="3.6376274"
|
||||||
|
inkscape:cx="48.520637"
|
||||||
|
inkscape:cy="86.32"
|
||||||
|
inkscape:window-width="1512"
|
||||||
|
inkscape:window-height="873"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g12" />
|
||||||
|
<metadata
|
||||||
|
id="metadata8">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>
|
||||||
|
image/svg+xml
|
||||||
|
</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage">
|
||||||
|
</dc:type>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs6">
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
transform="matrix(1.3333333,0,0,-1.3333333,0,172.64)"
|
||||||
|
id="g10">
|
||||||
|
<g
|
||||||
|
transform="scale(0.1)"
|
||||||
|
id="g12">
|
||||||
|
<path
|
||||||
|
id="path14"
|
||||||
|
style="fill:#04153e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
d="m 973.246,741.891 c 0,-8.696 -11.594,-20.286 -23.183,-37.672 -46.372,-57.961 -150.7,-159.391 -150.7,-414.418 0,-130.41 20.289,-185.469 20.289,-246.3322 C 819.652,14.4883 813.855,0 799.363,0 c -31.875,0 -89.836,139.109 -89.836,304.293 0,350.656 220.246,446.293 255.024,446.293 5.797,0 8.695,-2.898 8.695,-8.695 m -553.519,8.695 c 34.777,0 252.125,-95.637 252.125,-443.395 C 671.852,142 616.793,2.89844 582.016,2.89844 c -14.493,0 -20.286,14.49216 -20.286,43.47266 0,60.8589 20.286,115.9179 20.286,246.3279 0,252.129 -104.328,356.457 -150.696,414.414 -14.492,17.391 -23.183,28.981 -23.183,37.676 2.894,2.899 5.793,5.797 11.59,5.797">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
id="path16"
|
||||||
|
style="fill:#04153e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
d="m 611.109,681.008 c 0,-46.367 34.774,-81.145 81.145,-81.145 43.469,0 78.242,34.778 78.242,81.145 0,46.367 -34.773,81.144 -81.141,81.144 -40.574,0 -78.246,-34.777 -78.246,-81.144">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
id="path18"
|
||||||
|
style="fill:#04153e;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
d="m 692.621,1060.09 c -31.875,14.49 -63.754,43.47 -63.754,101.43 0,55.07 26.082,110.12 63.754,133.31 37.676,-23.19 63.758,-78.24 63.758,-133.31 0,-57.96 -31.879,-84.04 -63.758,-101.43 M 1376.55,164.609 c -113.02,69.551 -286.9,142 -539.027,165.188 0,-2.899 -2.898,-20.289 -2.898,-40.578 0,-8.688 0,-20.278 2.898,-20.278 246.327,-14.492 420.207,-66.66 539.027,-110.132 2.9,0 5.8,-2.887 5.8,-2.887 0,0 2.9,0 2.9,2.887 -2.9,2.902 -5.8,2.902 -8.7,5.8 M 823.031,1100.66 c -20.285,17.39 -34.773,43.47 -23.183,84.05 11.593,37.67 40.574,69.55 72.453,78.24 20.285,-26.08 28.976,-66.65 17.387,-107.22 -14.493,-37.68 -43.469,-49.27 -66.657,-55.07 M 1005.61,947.07 c -5.797,23.184 -5.797,52.164 26.08,81.14 28.98,26.09 69.55,34.78 98.53,26.09 5.8,-31.88 -11.59,-69.558 -40.57,-92.741 -31.88,-28.977 -60.86,-23.184 -84.04,-14.489 m 5.79,-231.84 c 8.7,23.184 23.19,46.368 63.76,52.165 37.67,2.898 75.35,-8.696 95.63,-34.778 -14.49,-28.98 -46.37,-49.265 -84.04,-52.164 -40.57,-5.797 -60.86,14.488 -75.35,34.777 m -66.65,-84.042 c 14.488,17.386 34.773,34.777 69.55,26.082 31.88,-5.797 60.86,-28.981 72.45,-55.063 -17.39,-20.285 -52.16,-28.98 -86.937,-23.184 -34.778,8.696 -49.266,31.879 -55.063,52.165 m -199.965,220.25 c -23.183,20.285 -43.469,52.164 -28.976,101.429 14.488,46.367 49.265,84.043 86.937,92.733 26.082,-28.98 34.777,-81.143 20.285,-127.51 -14.488,-46.367 -49.265,-63.754 -78.246,-66.652 m 86.942,-52.165 c -8.696,26.082 -5.797,60.856 28.98,89.836 31.879,28.981 78.246,40.575 113.023,28.981 5.793,-34.777 -14.492,-78.246 -46.371,-104.328 -37.671,-28.981 -69.55,-26.082 -95.632,-14.489 m 57.961,188.368 c -14.493,26.079 -26.083,63.759 2.898,107.229 26.082,40.57 72.449,69.55 113.024,66.65 17.38,-34.77 11.59,-89.84 -17.391,-130.41 -28.981,-43.469 -66.653,-49.262 -98.531,-43.469 M 1008.5,825.355 c 0,28.981 11.6,63.754 55.07,84.04 43.47,17.39 89.83,14.492 121.71,-5.793 -5.79,-34.778 -34.77,-72.454 -78.24,-92.739 -43.47,-20.285 -75.35,-5.797 -98.54,14.492 M 0,158.809 v 0 c 2.89844,-2.887 5.79688,0 8.69531,0 118.81669,43.472 292.69569,95.64 539.02769,110.132 0,2.899 2.898,14.489 2.898,20.278 0,17.39 -2.898,37.679 -2.898,40.578 C 295.598,306.609 124.613,234.16 8.69531,164.609 2.89844,161.711 0,161.711 0,158.809 m 495.559,999.821 c -11.594,37.67 -5.797,81.14 17.386,107.22 31.879,-8.69 60.86,-40.57 72.45,-78.24 11.593,-40.58 -2.895,-66.66 -23.184,-84.05 -26.082,2.9 -55.063,14.49 -66.652,55.07 M 292.699,958.664 c -28.98,26.078 -46.371,63.756 -40.574,92.736 28.98,8.69 69.555,0 98.531,-26.08 31.879,-26.086 31.879,-55.066 26.082,-81.148 -23.183,-5.797 -52.164,-11.59 -84.039,14.492 m 5.793,-278.211 c -37.672,2.899 -72.449,26.082 -86.937,52.164 20.285,23.184 57.961,37.676 95.633,34.778 40.574,-2.899 55.062,-28.981 63.757,-52.165 -11.593,-20.289 -34.777,-40.574 -72.453,-34.777 m 86.942,-98.531 c -31.879,-5.797 -66.653,2.898 -86.942,23.183 8.696,26.083 37.676,46.368 72.453,52.165 34.774,8.695 55.063,-8.696 66.653,-26.082 -5.797,-20.286 -17.387,-43.469 -52.164,-49.266 M 559.313,918.09 c -14.489,46.367 -5.793,98.53 20.289,127.51 37.671,-11.59 72.449,-49.264 86.941,-92.733 14.488,-49.265 -5.797,-81.144 -26.082,-101.429 -31.879,2.898 -66.656,20.285 -81.148,66.652 M 457.883,813.762 c -31.875,28.98 -52.164,69.551 -46.367,104.328 34.777,11.594 78.246,0 113.023,-28.981 34.774,-28.98 34.774,-63.754 28.981,-89.836 -28.981,-11.593 -60.86,-14.492 -95.637,14.489 m -60.856,217.348 c -26.082,40.57 -31.879,92.74 -17.39,130.41 40.574,0 84.043,-26.08 113.023,-66.65 28.981,-43.47 17.387,-78.25 0,-107.229 -31.879,-5.793 -69.551,0 -95.633,43.469 M 275.309,810.863 c -40.571,20.285 -72.45,57.961 -78.246,92.739 31.878,20.285 81.144,23.183 121.718,2.898 43.469,-20.289 55.063,-52.164 55.063,-84.043 -23.188,-17.391 -55.063,-31.879 -98.535,-11.594">
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.3 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 474 159"
|
||||||
|
width="474"
|
||||||
|
height="159"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4971"
|
||||||
|
sodipodi:docname="Fairosrx.svg"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs4975" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview4973"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.4842767"
|
||||||
|
inkscape:cx="238.16314"
|
||||||
|
inkscape:cy="79.500002"
|
||||||
|
inkscape:window-width="1512"
|
||||||
|
inkscape:window-height="873"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4971" />
|
||||||
|
<path
|
||||||
|
d="M444.4 15.25C441.68 23.37 431.19 17.11 426.32 15.4C413.29 10.83 390.72 13.95 390.41 31.25C390.21 42.22 399.98 44.81 408.65 47.16C425.62 51.77 450.1 53.21 450.22 76.25C450.24 79.45 449.92 82.58 448.78 85.58C441.34 105.13 410.99 104.68 394.86 98.85C388.59 96.59 373.77 88.72 381.76 80.25C384.76 80.59 390.94 87.06 394.49 88.73C404.72 93.56 416.25 94.72 427.27 92.09C432.87 90.76 439.09 87.04 439.95 80.75C441.63 68.4 434.34 64.02 423.92 60.78C410.29 56.54 383.56 55.46 381.03 37.25C377.34 10.64 402.77 2.26 424.43 5.76C430.08 6.68 440.84 10.49 444.4 15.25ZM64.75 5.56C65.88 12.91 57.87 14.55 52.25 14.55C42.58 14.55 32.92 14.55 23.25 14.55C19.81 14.55 14.24 13.51 11.27 15.25C11.27 26.37 11.27 37.49 11.27 48.61C25.1 48.61 38.92 48.61 52.75 48.61C53.75 54.08 48.57 57.09 43.75 57.51C36.2 58.16 28.33 57.55 20.75 57.55C17.94 57.55 13.62 56.72 11.26 58.25C11.26 72.69 11.26 87.13 11.26 101.57C8.1 101.57 4.95 101.57 1.79 101.57C1.79 69.57 1.79 37.56 1.79 5.56C22.78 5.56 43.76 5.56 64.75 5.56ZM106.25 5.63C119.66 37.6 133.08 69.57 146.49 101.54C143.24 101.54 140 101.54 136.75 101.54C125.42 74.3 114.08 47.06 102.75 19.81C96.79 33.79 90.83 47.77 84.88 61.75C91.72 63.14 99.76 61.98 106.75 61.97C109.48 61.97 113.38 61.27 115.75 62.59C115.57 67.59 111.49 70.64 106.75 71.02C98.53 71.68 89.42 69.49 81.38 71.25C77 81.35 72.63 91.46 68.25 101.56C65.07 101.56 61.89 101.56 58.71 101.56C72.05 69.58 85.4 37.6 98.75 5.63C101.25 5.63 103.75 5.63 106.25 5.63ZM167.05 5.86C167.05 37.76 167.05 69.66 167.05 101.55C163.93 101.55 160.8 101.55 157.68 101.55C155.91 93.75 157.27 84.27 157.27 76.25C157.27 59.58 157.27 42.92 157.27 26.25C157.27 18.99 155.56 4.43 167.05 5.86ZM236.54 65.25C243.95 76.65 251.35 88.05 258.75 99.44C262.92 93.09 267.08 86.75 271.25 80.4C273.13 83.01 275.01 85.63 276.89 88.25C272.66 95.08 268.43 101.92 264.21 108.75C273.54 124.08 282.88 139.42 292.22 154.75C285.64 158.84 280.93 153.78 277.65 148.6C273.2 141.56 268.71 134.52 264.32 127.43C262.53 124.53 260.84 120.55 258.25 118.39C252.08 127.99 245.92 137.59 239.75 147.18C236.25 147.18 232.75 147.18 229.25 147.18C237.04 134.37 244.84 121.56 252.63 108.75C243.69 94.92 234.76 81.08 225.82 67.25C218.58 66.86 203.83 69.73 204.25 58.14C207.43 56.75 212.28 57.61 215.75 57.61C228.29 57.61 241.7 58.43 246 43.76C246.79 41.05 247.37 38.38 247.25 35.51C246.88 26.95 241.78 17.87 233.12 15.21C227.81 13.58 221.23 14.55 215.75 14.55C209.93 14.55 203.85 14.06 198.08 14.75C198.08 43.69 198.08 72.63 198.08 101.56C194.82 101.56 191.56 101.56 188.3 101.56C188.3 69.66 188.3 37.76 188.3 5.86C197.09 4.21 207.3 5.55 216.25 5.55C223.37 5.55 231.27 4.61 238.02 7.26C256.29 14.45 261.98 38.12 251.68 53.95C249.29 57.62 245.59 60.98 241.67 62.92C239.97 63.76 238.07 64.1 236.54 65.25ZM467.75 101.23C462.71 93.59 457.97 94.1 453.25 101.61C451.72 92.91 469.15 92.93 467.75 101.23ZM457.75 99.12C460.39 98.54 463.26 98.28 464.15 101.25C463.9 102.29 462.85 103.26 462.87 104.25C462.89 105.35 463.8 105.84 463.75 106.97C461.63 106.35 461.64 104.18 459.25 104.14C458.92 105.05 458.58 105.95 458.25 106.85C456.94 105.41 457.43 101.06 457.75 99.12ZM462.77 101.25C461.85 99.77 460.66 99.94 459.07 100.25C459.07 101.09 459.07 101.93 459.07 102.77C460.92 102.91 461.96 103.04 462.77 101.25ZM468.25 101.29C469.96 104.83 466.62 109.62 463.13 110.8C457.15 112.8 452.46 107.74 452.51 102.25C454.09 104.16 454.1 108.24 457.44 109.78C463.97 112.78 467.37 106.36 468.25 101.29Z"
|
||||||
|
fill="#3469a5"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
stroke="#3469a5"
|
||||||
|
stroke-width="0.25"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
id="path4965"
|
||||||
|
style="fill:#0047ab;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="M337.99 5.75C334.75 6.31 327.37 4.96 323.25 5.1C314.9 5.38 306.42 8.73 299.5 13.27C271.82 31.42 270.21 73.63 296.37 93.86C303.08 99.05 311.75 103.27 320.25 104C325.22 104.42 330.6 103.37 335.44 104.25C328.2 108.53 312.04 105.99 304.44 103.31C268.94 90.76 258.61 45.98 284.33 18.59C294.3 7.97 308.35 2.73 322.75 2.84C326.45 2.87 335.35 3.45 337.99 5.75ZM359.48 27.87C359.28 28 359.08 28.12 358.89 28.25C359.02 28.92 359.16 29.58 359.3 30.25C358.78 30.28 358.27 30.31 357.75 30.34C357.45 30.65 357.15 30.95 356.84 31.25C356.83 31.68 356.83 32.1 356.82 32.53C354.88 30.66 353.41 28.22 351.5 26.25C346.08 20.65 338.54 16.88 331 15.26C326.74 14.34 319.72 16.05 316.25 14.68C328.38 8.03 351.75 16.98 359.48 27.87Z"
|
||||||
|
fill="#c45d3e"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
stroke="#c45d3e"
|
||||||
|
stroke-width="0.25"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
id="path4967"
|
||||||
|
style="fill:#cd7f32;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="M359.48 27.87C363.36 31.37 365.79 37.08 367.3 41.95C373.66 62.5 363.46 85.12 343.43 93.23C337.09 95.79 321.06 98.57 315.48 94.25C321.15 93.66 326.66 95.28 332.48 93.72C351.38 88.65 364.29 71.46 363.07 51.75C362.79 47.17 361.62 42.61 359.77 38.43C358.89 36.44 357.44 34.57 356.82 32.53C356.83 32.1 356.83 31.68 356.84 31.25C357.15 30.95 357.45 30.65 357.75 30.34C358.27 30.31 358.78 30.28 359.3 30.25C359.16 29.58 359.02 28.92 358.89 28.25C359.08 28.12 359.28 28 359.48 27.87Z"
|
||||||
|
fill="#d68551"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
stroke="#d68551"
|
||||||
|
stroke-width="0.25"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
id="path4969"
|
||||||
|
style="fill:#cd7f32;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 474 159"
|
||||||
|
width="474"
|
||||||
|
height="159"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4971"
|
||||||
|
sodipodi:docname="fairosrx_inverted.svg"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs4975" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview4973"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.4842767"
|
||||||
|
inkscape:cx="237.48941"
|
||||||
|
inkscape:cy="79.5"
|
||||||
|
inkscape:window-width="1512"
|
||||||
|
inkscape:window-height="873"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4971" />
|
||||||
|
<path
|
||||||
|
d="M444.4 15.25C441.68 23.37 431.19 17.11 426.32 15.4C413.29 10.83 390.72 13.95 390.41 31.25C390.21 42.22 399.98 44.81 408.65 47.16C425.62 51.77 450.1 53.21 450.22 76.25C450.24 79.45 449.92 82.58 448.78 85.58C441.34 105.13 410.99 104.68 394.86 98.85C388.59 96.59 373.77 88.72 381.76 80.25C384.76 80.59 390.94 87.06 394.49 88.73C404.72 93.56 416.25 94.72 427.27 92.09C432.87 90.76 439.09 87.04 439.95 80.75C441.63 68.4 434.34 64.02 423.92 60.78C410.29 56.54 383.56 55.46 381.03 37.25C377.34 10.64 402.77 2.26 424.43 5.76C430.08 6.68 440.84 10.49 444.4 15.25ZM64.75 5.56C65.88 12.91 57.87 14.55 52.25 14.55C42.58 14.55 32.92 14.55 23.25 14.55C19.81 14.55 14.24 13.51 11.27 15.25C11.27 26.37 11.27 37.49 11.27 48.61C25.1 48.61 38.92 48.61 52.75 48.61C53.75 54.08 48.57 57.09 43.75 57.51C36.2 58.16 28.33 57.55 20.75 57.55C17.94 57.55 13.62 56.72 11.26 58.25C11.26 72.69 11.26 87.13 11.26 101.57C8.1 101.57 4.95 101.57 1.79 101.57C1.79 69.57 1.79 37.56 1.79 5.56C22.78 5.56 43.76 5.56 64.75 5.56ZM106.25 5.63C119.66 37.6 133.08 69.57 146.49 101.54C143.24 101.54 140 101.54 136.75 101.54C125.42 74.3 114.08 47.06 102.75 19.81C96.79 33.79 90.83 47.77 84.88 61.75C91.72 63.14 99.76 61.98 106.75 61.97C109.48 61.97 113.38 61.27 115.75 62.59C115.57 67.59 111.49 70.64 106.75 71.02C98.53 71.68 89.42 69.49 81.38 71.25C77 81.35 72.63 91.46 68.25 101.56C65.07 101.56 61.89 101.56 58.71 101.56C72.05 69.58 85.4 37.6 98.75 5.63C101.25 5.63 103.75 5.63 106.25 5.63ZM167.05 5.86C167.05 37.76 167.05 69.66 167.05 101.55C163.93 101.55 160.8 101.55 157.68 101.55C155.91 93.75 157.27 84.27 157.27 76.25C157.27 59.58 157.27 42.92 157.27 26.25C157.27 18.99 155.56 4.43 167.05 5.86ZM236.54 65.25C243.95 76.65 251.35 88.05 258.75 99.44C262.92 93.09 267.08 86.75 271.25 80.4C273.13 83.01 275.01 85.63 276.89 88.25C272.66 95.08 268.43 101.92 264.21 108.75C273.54 124.08 282.88 139.42 292.22 154.75C285.64 158.84 280.93 153.78 277.65 148.6C273.2 141.56 268.71 134.52 264.32 127.43C262.53 124.53 260.84 120.55 258.25 118.39C252.08 127.99 245.92 137.59 239.75 147.18C236.25 147.18 232.75 147.18 229.25 147.18C237.04 134.37 244.84 121.56 252.63 108.75C243.69 94.92 234.76 81.08 225.82 67.25C218.58 66.86 203.83 69.73 204.25 58.14C207.43 56.75 212.28 57.61 215.75 57.61C228.29 57.61 241.7 58.43 246 43.76C246.79 41.05 247.37 38.38 247.25 35.51C246.88 26.95 241.78 17.87 233.12 15.21C227.81 13.58 221.23 14.55 215.75 14.55C209.93 14.55 203.85 14.06 198.08 14.75C198.08 43.69 198.08 72.63 198.08 101.56C194.82 101.56 191.56 101.56 188.3 101.56C188.3 69.66 188.3 37.76 188.3 5.86C197.09 4.21 207.3 5.55 216.25 5.55C223.37 5.55 231.27 4.61 238.02 7.26C256.29 14.45 261.98 38.12 251.68 53.95C249.29 57.62 245.59 60.98 241.67 62.92C239.97 63.76 238.07 64.1 236.54 65.25ZM467.75 101.23C462.71 93.59 457.97 94.1 453.25 101.61C451.72 92.91 469.15 92.93 467.75 101.23ZM457.75 99.12C460.39 98.54 463.26 98.28 464.15 101.25C463.9 102.29 462.85 103.26 462.87 104.25C462.89 105.35 463.8 105.84 463.75 106.97C461.63 106.35 461.64 104.18 459.25 104.14C458.92 105.05 458.58 105.95 458.25 106.85C456.94 105.41 457.43 101.06 457.75 99.12ZM462.77 101.25C461.85 99.77 460.66 99.94 459.07 100.25C459.07 101.09 459.07 101.93 459.07 102.77C460.92 102.91 461.96 103.04 462.77 101.25ZM468.25 101.29C469.96 104.83 466.62 109.62 463.13 110.8C457.15 112.8 452.46 107.74 452.51 102.25C454.09 104.16 454.1 108.24 457.44 109.78C463.97 112.78 467.37 106.36 468.25 101.29Z"
|
||||||
|
fill="#3469a5"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
stroke="#3469a5"
|
||||||
|
stroke-width="0.25"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
id="path4965"
|
||||||
|
style="fill:#cd7f32;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="M337.99 5.75C334.75 6.31 327.37 4.96 323.25 5.1C314.9 5.38 306.42 8.73 299.5 13.27C271.82 31.42 270.21 73.63 296.37 93.86C303.08 99.05 311.75 103.27 320.25 104C325.22 104.42 330.6 103.37 335.44 104.25C328.2 108.53 312.04 105.99 304.44 103.31C268.94 90.76 258.61 45.98 284.33 18.59C294.3 7.97 308.35 2.73 322.75 2.84C326.45 2.87 335.35 3.45 337.99 5.75ZM359.48 27.87C359.28 28 359.08 28.12 358.89 28.25C359.02 28.92 359.16 29.58 359.3 30.25C358.78 30.28 358.27 30.31 357.75 30.34C357.45 30.65 357.15 30.95 356.84 31.25C356.83 31.68 356.83 32.1 356.82 32.53C354.88 30.66 353.41 28.22 351.5 26.25C346.08 20.65 338.54 16.88 331 15.26C326.74 14.34 319.72 16.05 316.25 14.68C328.38 8.03 351.75 16.98 359.48 27.87Z"
|
||||||
|
fill="#c45d3e"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
stroke="#c45d3e"
|
||||||
|
stroke-width="0.25"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
id="path4967"
|
||||||
|
style="fill:#0047ab;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="M359.48 27.87C363.36 31.37 365.79 37.08 367.3 41.95C373.66 62.5 363.46 85.12 343.43 93.23C337.09 95.79 321.06 98.57 315.48 94.25C321.15 93.66 326.66 95.28 332.48 93.72C351.38 88.65 364.29 71.46 363.07 51.75C362.79 47.17 361.62 42.61 359.77 38.43C358.89 36.44 357.44 34.57 356.82 32.53C356.83 32.1 356.83 31.68 356.84 31.25C357.15 30.95 357.45 30.65 357.75 30.34C358.27 30.31 358.78 30.28 359.3 30.25C359.16 29.58 359.02 28.92 358.89 28.25C359.08 28.12 359.28 28 359.48 27.87Z"
|
||||||
|
fill="#d68551"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
stroke="#d68551"
|
||||||
|
stroke-width="0.25"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
id="path4969"
|
||||||
|
style="fill:#0047ab;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 43.213475 44.200001"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:docname="MedCost_deepcove.svg"
|
||||||
|
width="43.213474"
|
||||||
|
height="44.200001"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs2819" /><sodipodi:namedview
|
||||||
|
id="namedview2817"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="5.0021322"
|
||||||
|
inkscape:cx="85.66347"
|
||||||
|
inkscape:cy="22.590367"
|
||||||
|
inkscape:window-width="1512"
|
||||||
|
inkscape:window-height="873"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="g2806" />
|
||||||
|
<style
|
||||||
|
type="text/css"
|
||||||
|
id="style2778">
|
||||||
|
.st0{fill:#235937;}
|
||||||
|
.st1{fill:#55A51C;}
|
||||||
|
</style>
|
||||||
|
<g
|
||||||
|
id="g2810"
|
||||||
|
transform="translate(0.1)">
|
||||||
|
<g
|
||||||
|
id="g2808"
|
||||||
|
transform="matrix(1.0026328,0,0,1,2.6327667e-4,0)">
|
||||||
|
<g
|
||||||
|
id="g2794">
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 73.7,10.7 v 26.9 h -3 V 25.5 c 0,-4.1 0.2,-9.5 0.1,-9.5 v 0 c 0,0 -1.4,2.5 -2.3,4.1 L 62.3,31 H 60.7 L 54.6,20.1 C 53.7,18.5 52.3,16 52.3,16 v 0 c 0,0 0.2,5.3 0.2,9.5 v 12.1 h -3 V 10.7 h 2.9 l 6.5,11.4 c 2.3,3.9 2.7,5 2.7,5 v 0 c 0,0 0.5,-1.1 2.7,-5 l 6.5,-11.4 z"
|
||||||
|
id="path2780" />
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 95.7,34.6 v 3 H 77.9 V 10.7 h 17.5 v 3 H 81 v 9.1 h 11.6 v 2.8 H 81 v 9.1 h 14.7 z"
|
||||||
|
id="path2782" />
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 111,10.7 c 7.5,0 13.6,5.5 13.6,13.6 0,8 -6,13.3 -13.4,13.3 H 99.9 V 10.7 Z m 0.2,23.9 c 5.5,0 10.2,-3.9 10.2,-10.3 0,-6.5 -4.7,-10.6 -10.4,-10.6 h -8 v 21 h 8.2 z"
|
||||||
|
id="path2784" />
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 150.1,18 c -1.8,-3 -5.1,-4.8 -8.8,-4.8 -6,0 -10.5,4.4 -10.5,10.9 0,6.5 4.6,10.9 10.5,10.9 4,0 7.5,-2.2 9.3,-5.6 l 3,0.8 c -2.2,4.8 -6.8,7.8 -12.3,7.8 -7.5,0 -13.6,-5.9 -13.6,-14 0,-8.1 6.1,-14 13.6,-14 5.1,0 9.6,2.8 11.9,7.1 z"
|
||||||
|
id="path2786" />
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 170,10.2 c 7.5,0 13.7,5.9 13.7,14 0,8.1 -6.1,14 -13.7,14 -7.5,0 -13.6,-5.9 -13.6,-14 0,-8.2 6.1,-14 13.6,-14 z m 0,24.9 c 5.9,0 10.5,-4.5 10.5,-10.9 0,-6.5 -4.5,-10.9 -10.5,-10.9 -6,0 -10.5,4.4 -10.5,10.9 0.1,6.4 4.6,10.9 10.5,10.9 z"
|
||||||
|
id="path2788" />
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 202.1,18.1 c -0.6,-3.3 -3.3,-5.2 -6.6,-5.2 -3.5,0 -5.9,1.7 -5.9,4.4 0,3 3.6,3.9 7.1,5 3.7,1.1 8.7,2.8 8.7,8 0,4.5 -3.8,7.8 -9.6,7.8 -5.6,0 -9.2,-3.4 -10,-7.9 l 3,-0.8 c 0.4,3.1 2.9,5.9 7.2,5.9 3.9,0 6.3,-2.1 6.3,-4.9 0,-3.9 -4.5,-4.7 -8.3,-5.9 -3,-1 -7.4,-2.3 -7.4,-7 0,-4.2 3.9,-7.3 9.1,-7.3 4.8,0 8.5,2.9 9.4,7.2 z"
|
||||||
|
id="path2790" />
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 227.7,10.7 v 3 h -8.8 v 23.9 h -3 v -24 h -8.8 v -3 h 20.6 z"
|
||||||
|
id="path2792" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g2806">
|
||||||
|
<path
|
||||||
|
class="st1"
|
||||||
|
d="M 29.6,13.7 C 31.4,9.5 32.5,4.9 32.5,0 h -4.2 c 0,7.9 -3.1,15.1 -8.1,20.4 -0.1,0.1 -0.3,0.3 -0.4,0.4 C 14.7,26 7.7,29.2 0,29.2 v 4.3 c 4.8,0 9.3,-1.1 13.3,-3 -1.8,4.2 -2.9,8.8 -2.9,13.7 h 4.2 c 0,-7.9 3.1,-15.1 8.1,-20.4 0.1,-0.1 0.3,-0.3 0.4,-0.4 C 28.2,18.2 35.2,15 42.9,15 v -4.3 c -4.7,0 -9.2,1.1 -13.3,3 z"
|
||||||
|
id="path2796"
|
||||||
|
style="fill:#04153e;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
class="st1"
|
||||||
|
d="m 16.6,44.2 h 4.2 c 0,-12.6 9.9,-22.8 22.2,-23 V 17 C 28.4,17.1 16.6,29.3 16.6,44.2 Z"
|
||||||
|
id="path2798"
|
||||||
|
style="fill:#04153e;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
class="st1"
|
||||||
|
d="m 22.7,44.2 h 4.2 c 0,-9.1 7.2,-16.5 16,-16.7 v -4.3 c -11.1,0.2 -20.2,9.5 -20.2,21 z"
|
||||||
|
id="path2800"
|
||||||
|
style="fill:#04153e;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
class="st1"
|
||||||
|
d="m 26.3,0 h -4.2 c 0,12.6 -9.9,22.8 -22.2,23 v 4.3 C 14.6,27.1 26.3,14.9 26.3,0 Z"
|
||||||
|
id="path2802"
|
||||||
|
style="fill:#04153e;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
class="st1"
|
||||||
|
d="M 20.2,0 H 16 C 16,9.1 8.8,16.5 0,16.7 V 21 C 11.2,20.8 20.2,11.5 20.2,0 Z"
|
||||||
|
id="path2804"
|
||||||
|
style="fill:#04153e;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g2814"
|
||||||
|
transform="translate(0.1)">
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 234.5,8.9 c 0,1.7 -1.3,3 -3,3 -1.7,0 -3,-1.3 -3,-3 0,-1.6 1.4,-3 3,-3 1.7,0.1 3,1.4 3,3 z m -5.3,0 c 0,1.3 1,2.4 2.3,2.4 1.3,0 2.3,-1 2.3,-2.3 0,-1.3 -1,-2.4 -2.3,-2.4 -1.3,0 -2.3,1 -2.3,2.3 z m 1.8,1.6 h -0.7 v -3 c 0.3,-0.1 0.6,-0.1 1.1,-0.1 0.6,0 0.8,0.1 1,0.2 0.2,0.1 0.3,0.4 0.3,0.6 0,0.3 -0.3,0.6 -0.6,0.7 v 0 c 0.3,0.1 0.4,0.3 0.5,0.7 0.1,0.4 0.1,0.6 0.2,0.7 h -0.7 C 232,10.2 232,9.9 231.9,9.6 231.8,9.3 231.7,9.1 231.3,9.1 H 231 Z m 0,-1.7 h 0.3 c 0.4,0 0.7,-0.1 0.7,-0.4 0,-0.3 -0.2,-0.5 -0.6,-0.5 -0.2,0 -0.3,0 -0.4,0 z"
|
||||||
|
id="path2812" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 366 KiB |
@@ -0,0 +1,169 @@
|
|||||||
|
belongs_to = member.joins(employer: { broker: :carrier }).select(
|
||||||
|
'brokers.id AS broker_id',
|
||||||
|
'carriers.id AS carrier_id'
|
||||||
|
)
|
||||||
|
keys = member.attributes.with_indifferent_access.slice(
|
||||||
|
:name, :family_id, :mb_member_key, :pb_entity_key, :pl_plan_key
|
||||||
|
)
|
||||||
|
|
||||||
|
enum :role, { member: 0, employer: 1, broker: 2, provider: 3, carrier: 4, admin: 5 }
|
||||||
|
|
||||||
|
http://localhost:3005/api/v1/claims/member_claims/343872
|
||||||
|
|
||||||
|
|
||||||
|
<div class="w-full flex items-center pt-6">
|
||||||
|
<h3 class="flex-none font-bold text-2xl text-brightlava">Admin</h3>
|
||||||
|
<div class="grow h-[1px] mt-2 ml-1 bg-brightlava"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"network_provider": "Cigna",
|
||||||
|
"plan_benefits": [
|
||||||
|
{
|
||||||
|
"id": 1295,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 1,
|
||||||
|
"benefit": "$25",
|
||||||
|
"benefit_desc": "Primary Visit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1296,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 2,
|
||||||
|
"benefit": "$50",
|
||||||
|
"benefit_desc": "Specialist Visit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1297,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 3,
|
||||||
|
"benefit": "$50",
|
||||||
|
"benefit_desc": "Urgent Care"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1298,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 4,
|
||||||
|
"benefit": "$3,500",
|
||||||
|
"benefit_desc": "INN-Ind Ded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1299,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 5,
|
||||||
|
"benefit": "$7,000",
|
||||||
|
"benefit_desc": "INN-Family Ded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1300,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 6,
|
||||||
|
"benefit": "$7,000",
|
||||||
|
"benefit_desc": "OON-Ind Ded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1301,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 7,
|
||||||
|
"benefit": "$14,000",
|
||||||
|
"benefit_desc": "OON-Family Ded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1302,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 8,
|
||||||
|
"benefit": "70%/30%",
|
||||||
|
"benefit_desc": "Co-Insurance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1303,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 9,
|
||||||
|
"benefit": "$7,000",
|
||||||
|
"benefit_desc": "INN-Ind OOP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1304,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 10,
|
||||||
|
"benefit": "$14,000",
|
||||||
|
"benefit_desc": "INN-Family OOP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1305,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 11,
|
||||||
|
"benefit": "$14,000",
|
||||||
|
"benefit_desc": "OON-Ind OOP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1306,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 12,
|
||||||
|
"benefit": "$28,000",
|
||||||
|
"benefit_desc": "OON-Family OOP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1307,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 13,
|
||||||
|
"benefit": "Sub to Ded",
|
||||||
|
"benefit_desc": "Emergency Room"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1308,
|
||||||
|
"title": "CLASSIC 3.5K",
|
||||||
|
"sequence": 14,
|
||||||
|
"benefit": "100%",
|
||||||
|
"benefit_desc": "Preventive Care"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recent_claims": [
|
||||||
|
{
|
||||||
|
"claim_number": 1950638,
|
||||||
|
"visit_date": "04-28-2026",
|
||||||
|
"claim_status": "Approved Claim",
|
||||||
|
"patient_name": "LAWHORN, ALMON",
|
||||||
|
"provider_name": "COLE, CHRISTOPHER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"claim_number": 1949703,
|
||||||
|
"visit_date": "02-12-2026",
|
||||||
|
"claim_status": "Approved Claim",
|
||||||
|
"patient_name": "LAWHORN, ALMON",
|
||||||
|
"provider_name": "CIGNA HEALTH AND LIFE INSURANCE COMPANY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"claim_number": 1942866,
|
||||||
|
"visit_date": "02-12-2026",
|
||||||
|
"claim_status": "Denied Claim",
|
||||||
|
"patient_name": "LAWHORN, ALMON",
|
||||||
|
"provider_name": "NELSON, JOHN C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"claim_number": 1942522,
|
||||||
|
"visit_date": "02-12-2026",
|
||||||
|
"claim_status": "Denied Claim",
|
||||||
|
"patient_name": "LAWHORN, ALMON",
|
||||||
|
"provider_name": "MALOY, JENNIFER R"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"claim_number": 1941594,
|
||||||
|
"visit_date": "02-12-2026",
|
||||||
|
"claim_status": "Denied Claim",
|
||||||
|
"patient_name": "LAWHORN, ALMON",
|
||||||
|
"provider_name": "JON K MARTI, DO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"claim_number": 1940479,
|
||||||
|
"visit_date": "02-12-2026",
|
||||||
|
"claim_status": "Approved Claim",
|
||||||
|
"patient_name": "LAWHORN, ALMON",
|
||||||
|
"provider_name": "GROUNDS, DAVID S"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ export default async function LandingPageLayout({ children }: { children: React.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
<div className="w-full">
|
<div className="w-full text-platinum">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"user client"
|
||||||
|
|
||||||
|
import { sideNavItems } from '@/lib/dashboard';
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import Script from 'next/script';
|
||||||
|
|
||||||
|
import AccountDropdown from '../../components/AccountDropdown';
|
||||||
|
import SideNav from "@/components/SideNav";
|
||||||
|
import Image from 'next/image';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const headersList = await headers();
|
||||||
|
const pathname = headersList.get("x-current-path");
|
||||||
|
const dashboardType = pathname?.split('/').pop() || "";
|
||||||
|
const pageNavItems = sideNavItems[dashboardType];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
<div className="absolute bottom-0 right-0 flex flex-col justify-end items-end w-full h-full">
|
||||||
|
<div className="w-full -mb-[2px] flex justify-start">
|
||||||
|
<div className="w-[87vw] sm:w-[91vw] md:w-[93vw] lg:w-[94.5vw] xl:w-[95.5vw] h-20 border-b-2 border-light-bronze z-2" />
|
||||||
|
</div>
|
||||||
|
<div className="w-[60px] h-full flex justify-start">
|
||||||
|
<div className="w-[32.5px] flex flex-col">
|
||||||
|
<div className={classnames(
|
||||||
|
"relative h-full border-r-6 rounded-out-tl-4xl rounded-tr-4xl bg-light-bronze border-atmosphere"
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full flex items-start">
|
||||||
|
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
|
||||||
|
<div className="w-[90.15vw] h-20 flex justify-between items-center">
|
||||||
|
<div className={classnames('flex items-center')}>
|
||||||
|
<div className="-ml-8.5 z-2">
|
||||||
|
<Image
|
||||||
|
src="/images/bbstpa-forsite.png"
|
||||||
|
alt="Britton Logo"
|
||||||
|
width={205}
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full flex justify-center ml-[60px]">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// /app/login/page.tsx
|
||||||
|
'use test' // Marks this component as a client component
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
import { signInAndFetchUser } from '@/app/actions/signin'
|
||||||
|
|
||||||
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export default function SignInPage() {
|
||||||
|
// useActionState manages the server state (errors, loading) and form binding
|
||||||
|
const [state, formAction, isPending] = useActionState(signInAndFetchUser, null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="h-full flex flex-col items-center justify-center py-6 mt-10 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center text-7xl font-bold text-atmosphere">
|
||||||
|
Britton Benefits
|
||||||
|
{/* <p>Britton</p>
|
||||||
|
<p>Benefits</p> */}
|
||||||
|
</div>
|
||||||
|
<div className={classnames("w-md space-y-4", font.className)}>
|
||||||
|
<div className="mt-10 text-center text-3xl font-extrabold text-light-bronze border-b-2 border-light-bronze">
|
||||||
|
Sign In
|
||||||
|
</div>
|
||||||
|
<form action={formAction}>
|
||||||
|
{/* Error Message Display */}
|
||||||
|
{state?.error && (
|
||||||
|
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-1">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="relative w-full flex justify-center py-2 px-4 mt-6 border-4 border-cobalt font-bold rounded-lg text-lg text-platinum hover:text-light-bronze bg-cobalt hover:bg-deepcove hover:cursor-pointer"
|
||||||
|
>
|
||||||
|
{isPending ? 'Signing In...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{/* <div className="text-bluemana">
|
||||||
|
Not signed up?
|
||||||
|
<Link
|
||||||
|
href="/account/signin"
|
||||||
|
>
|
||||||
|
<span className="hover:text-light-bronze">Click here.</span>
|
||||||
|
</Link>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
<div className="text-bluemana mt-4 z-20">
|
||||||
|
Not signed up?{' '}
|
||||||
|
<Link
|
||||||
|
href="/account/signup"
|
||||||
|
className="underline text-atmosphere hover:text-light-bronze z-10"
|
||||||
|
>
|
||||||
|
Click here.
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// /app/login/page.tsx
|
||||||
|
'use test' // Marks this component as a client component
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { useActionState, useState } from 'react'
|
||||||
|
import { registerUser } from '@/app/actions/signup'
|
||||||
|
|
||||||
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
// useActionState manages the server state (errors, loading) and form binding
|
||||||
|
const [state, formAction, isPending] = useActionState(registerUser, null)
|
||||||
|
const [role, setRole] = useState("");
|
||||||
|
const [date, setDate] = useState("");
|
||||||
|
|
||||||
|
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
let input = e.target.value.replace(/\D/g, "");
|
||||||
|
|
||||||
|
if (input.length > 2 && input.length <= 4) {
|
||||||
|
input = `${input.slice(0, 2)}/${input.slice(2)}`;
|
||||||
|
} else if (input.length > 4) {
|
||||||
|
input = `${input.slice(0, 2)}/${input.slice(2, 4)}/${input.slice(4, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDate(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="h-full flex flex-col items-center justify-center py-6 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center text-7xl font-bold text-atmosphere">
|
||||||
|
Britton Benefits
|
||||||
|
{/* <p>Britton</p>
|
||||||
|
<p>Benefits</p> */}
|
||||||
|
</div>
|
||||||
|
<div className={classnames("w-md space-y-4", font.className)}>
|
||||||
|
<div className="mt-10 text-center text-3xl font-extrabold text-light-bronze border-b-2 border-light-bronze">
|
||||||
|
Sign Up
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 text-center text-md font-extrabold text-bluemana">
|
||||||
|
(Employer and Broker users must be invited)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action={formAction}>
|
||||||
|
{/* Error Message Display */}
|
||||||
|
{state?.error && (
|
||||||
|
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="two-item-dropdown" className="block text-md font-bold text-platinum">
|
||||||
|
I Am A...
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
id="role"
|
||||||
|
name="role"
|
||||||
|
value={role}
|
||||||
|
onChange={(e) => setRole(e.target.value)}
|
||||||
|
required
|
||||||
|
className="rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
>
|
||||||
|
{/* Placeholder option */}
|
||||||
|
<option value="" disabled>Select User Role</option>
|
||||||
|
|
||||||
|
{/* The two dropdown items */}
|
||||||
|
<option value="member">Member</option>
|
||||||
|
<option value="provider">Provider</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{role === 'member' && (
|
||||||
|
<>
|
||||||
|
{/* <div className="mt-2">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Member ID</label>
|
||||||
|
<input
|
||||||
|
type="roleId"
|
||||||
|
name="roleId"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="######"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Last 4 Digits of SSN</label>
|
||||||
|
<input
|
||||||
|
type="lastFourSsn"
|
||||||
|
name="lastFourSsn"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="####"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Date of Birth</label>
|
||||||
|
<input
|
||||||
|
type="dateOfBirth"
|
||||||
|
name="dateOfBirth"
|
||||||
|
value={date}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="MM/DD/YYYY"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{role === 'provider' && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Tax ID</label>
|
||||||
|
<input
|
||||||
|
type="roleId"
|
||||||
|
name="roleId"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="######"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-1">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-1">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Confirm Password</label>
|
||||||
|
<input
|
||||||
|
type="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="relative w-full flex justify-center py-2 px-4 mt-6 border-4 border-cobalt font-bold rounded-lg text-lg text-platinum hover:text-light-bronze bg-cobalt hover:bg-deepcove hover:cursor-pointer"
|
||||||
|
>
|
||||||
|
{isPending ? 'Signing In...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// /app/actions/auth.ts
|
||||||
|
'use server'
|
||||||
|
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { SignJWT } from 'jose/jwt/sign';
|
||||||
|
|
||||||
|
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET);
|
||||||
|
|
||||||
|
function roleMapping(role_number: number) {
|
||||||
|
const map = [
|
||||||
|
"member",
|
||||||
|
"employer",
|
||||||
|
"broker",
|
||||||
|
"provider",
|
||||||
|
"carrier",
|
||||||
|
"admin"
|
||||||
|
]
|
||||||
|
|
||||||
|
return map[role_number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginUser(prevState: any, formData: FormData) {
|
||||||
|
const email = formData.get('email') as string
|
||||||
|
const password = formData.get('password') as string
|
||||||
|
|
||||||
|
// Simple validation check
|
||||||
|
if (!email || !password) {
|
||||||
|
throw new Error('Please fill in all fields.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const role = roleMapping(data.keychain.role)
|
||||||
|
|
||||||
|
const sessionPayload = {
|
||||||
|
accessToken: data.token,
|
||||||
|
user: {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
keychain: data.keychain,
|
||||||
|
role: role,
|
||||||
|
role_id: data.keychain.role_id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedSession = await new SignJWT(sessionPayload)
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime('2h')
|
||||||
|
.sign(SECRET_KEY);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.set('session', encryptedSession, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const initUserResponse = await fetch('https://yourdomain.com', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const userData = await initUserResponse.json();
|
||||||
|
|
||||||
|
// return { success: true };
|
||||||
|
|
||||||
|
if (role == "member") {
|
||||||
|
redirect('/dashboard/member')
|
||||||
|
} else if (role == "employer") {
|
||||||
|
redirect('/dashboard/employer')
|
||||||
|
} else if (role == "broker") {
|
||||||
|
redirect('/dashboard/broker')
|
||||||
|
} else if (role == "provider") {
|
||||||
|
redirect('/dashboard/provider')
|
||||||
|
} else if (role == "carrier") {
|
||||||
|
redirect('/dashboard/carrier')
|
||||||
|
} else if (role == "admin") {
|
||||||
|
redirect('/dashboard/admin')
|
||||||
|
} else {
|
||||||
|
return { error: 'Invalid email or password.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { SignJWT } from 'jose';
|
||||||
|
|
||||||
|
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||||
|
|
||||||
|
function roleMapping(role_number: number) {
|
||||||
|
const map = [
|
||||||
|
"member",
|
||||||
|
"employer",
|
||||||
|
"broker",
|
||||||
|
"provider",
|
||||||
|
"carrier",
|
||||||
|
"admin"
|
||||||
|
]
|
||||||
|
|
||||||
|
return map[role_number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signInAndFetchUser(prevState: any, formData: FormData) {
|
||||||
|
const email = formData.get('email') as string
|
||||||
|
const password = formData.get('password') as string
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
throw new Error('Please fill in all fields.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResponse = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/signin`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ user: {email, password} }),
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authResponse.ok) {
|
||||||
|
throw new Error('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = await authResponse.json();
|
||||||
|
const token = authResponse.headers.get('authorization')?.split(' ')[1];
|
||||||
|
console.log("token: ", token)
|
||||||
|
// const token = user.jti
|
||||||
|
const role = user.role
|
||||||
|
|
||||||
|
const sessionPayload = {
|
||||||
|
token: token,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: role,
|
||||||
|
roleId: user.role_id,
|
||||||
|
keychain: user.keychain
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("sessionPayload: ", sessionPayload)
|
||||||
|
|
||||||
|
const encryptedSession = await new SignJWT(sessionPayload)
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime('2h')
|
||||||
|
.sign(SECRET_KEY);
|
||||||
|
|
||||||
|
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.set('session', encryptedSession, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (role == "member") {
|
||||||
|
redirect('/dashboard/member')
|
||||||
|
} else if (role == "employer") {
|
||||||
|
redirect('/dashboard/employer')
|
||||||
|
} else if (role == "broker") {
|
||||||
|
redirect('/dashboard/broker')
|
||||||
|
} else if (role == "provider") {
|
||||||
|
redirect('/dashboard/provider')
|
||||||
|
} else if (role == "carrier") {
|
||||||
|
redirect('/dashboard/carrier')
|
||||||
|
} else if (role == "admin") {
|
||||||
|
redirect('/dashboard/admin')
|
||||||
|
} else {
|
||||||
|
return { error: 'Invalid email or password.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// app/login/actions.ts
|
||||||
|
'use server'
|
||||||
|
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { SignJWT } from 'jose';
|
||||||
|
|
||||||
|
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||||
|
|
||||||
|
function roleMapping(role_number: number) {
|
||||||
|
const map = [
|
||||||
|
"member",
|
||||||
|
"employer",
|
||||||
|
"broker",
|
||||||
|
"provider",
|
||||||
|
"carrier",
|
||||||
|
"admin"
|
||||||
|
]
|
||||||
|
|
||||||
|
return map[role_number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerUser(prevState: any, formData: FormData) {
|
||||||
|
const email = formData.get('email') as string
|
||||||
|
const password = formData.get('password') as string
|
||||||
|
const confirm_password = formData.get('confirmPassword') as string
|
||||||
|
const role = formData.get('role') as string
|
||||||
|
const role_id = formData.get('roleId') as string
|
||||||
|
const last_four_ssn = formData.get('lastFourSsn') as string
|
||||||
|
const date_of_birth = formData.get('dateOfBirth') as string
|
||||||
|
|
||||||
|
if (!email || !password || !confirm_password || !role) {
|
||||||
|
throw new Error('Please fill in all fields.');
|
||||||
|
}
|
||||||
|
if (role === "provider" && !role_id) {
|
||||||
|
throw new Error('Please fill in all fields.');
|
||||||
|
}
|
||||||
|
if (role === "member" && (!date_of_birth || !last_four_ssn)) {
|
||||||
|
throw new Error('Please fill in all fields.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const authResponse = await fetch(`${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ user: {email, password, confirm_password, role, role_id, last_four_ssn, date_of_birth} }),
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authResponse.ok) {
|
||||||
|
throw new Error('Invalid credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await authResponse.json();
|
||||||
|
const token = data.jti
|
||||||
|
|
||||||
|
const sessionPayload = {
|
||||||
|
token: token,
|
||||||
|
user: {
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
role: data.role,
|
||||||
|
roleId: data.role_id,
|
||||||
|
keychain: data.keychain
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedSession = await new SignJWT(sessionPayload)
|
||||||
|
.setProtectedHeader({ alg: 'HS256' })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime('2h')
|
||||||
|
.sign(SECRET_KEY);
|
||||||
|
|
||||||
|
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
cookieStore.set('session', encryptedSession, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (role == "member") {
|
||||||
|
redirect('/dashboard/member')
|
||||||
|
} else if (role == "employer") {
|
||||||
|
redirect('/dashboard/employer')
|
||||||
|
} else if (role == "broker") {
|
||||||
|
redirect('/dashboard/broker')
|
||||||
|
} else if (role == "provider") {
|
||||||
|
redirect('/dashboard/provider')
|
||||||
|
} else if (role == "carrier") {
|
||||||
|
redirect('/dashboard/carrier')
|
||||||
|
} else if (role == "admin") {
|
||||||
|
redirect('/dashboard/admin')
|
||||||
|
} else {
|
||||||
|
return { error: 'Invalid email or password.' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { isIPv4 } from 'net';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
// Read the runtime env variable safely on the server
|
||||||
|
let apiUrl = "";
|
||||||
|
const backendApiHost = process.env.BE_API_SERVER_HOST || "localhost";
|
||||||
|
if (isIPv4(backendApiHost)) {
|
||||||
|
apiUrl = apiUrl.concat(backendApiHost)
|
||||||
|
} else {
|
||||||
|
apiUrl = apiUrl.concat("127.0.0.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.BE_API_SERVER_PORT) {
|
||||||
|
apiUrl = apiUrl.concat(":").concat(process.env.BE_API_SERVER_PORT)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ apiUrl });
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import classnames from 'classnames';
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { JWTPayload, jwtVerify } from "jose";
|
||||||
|
|
||||||
|
import DashboardHeader from "@/components/DashboardHeader";
|
||||||
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||||
|
|
||||||
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||||
|
|
||||||
|
interface ClaimType {
|
||||||
|
claim_number: number;
|
||||||
|
visit_date: string;
|
||||||
|
claim_status: string;
|
||||||
|
patient_name: string;
|
||||||
|
provider_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserJwtPayload extends JWTPayload {
|
||||||
|
user: UserData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
roleId: string;
|
||||||
|
keychain: UserKeychain;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserKeychain {
|
||||||
|
family_id: string;
|
||||||
|
mb_member_key: number;
|
||||||
|
pb_entity_key: number;
|
||||||
|
pl_plan_key: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function EmployerIdCards() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const sessionCookie = cookieStore.get("session");
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
redirect('/account/signin')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Extract the raw string token from the cookie object
|
||||||
|
const token = sessionCookie.value;
|
||||||
|
|
||||||
|
// 3. Verify and decode the JWT using jose
|
||||||
|
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||||
|
|
||||||
|
// 3. Cast the payload to your custom interface
|
||||||
|
const data = payload as UserJwtPayload;
|
||||||
|
|
||||||
|
// const userDataTest = {
|
||||||
|
// pb_entity_key: 377527,
|
||||||
|
// pl_plan_key: '61',
|
||||||
|
// name: "John McMember"
|
||||||
|
// }
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
pb_entity_key: data.user.keychain.pb_entity_key,
|
||||||
|
pl_plan_key: data.user.keychain.pl_plan_key,
|
||||||
|
name: data.user.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/claims`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json' ,
|
||||||
|
'Authorization': `Bearer ${data.token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Unauthenticated");
|
||||||
|
|
||||||
|
|
||||||
|
const claimsData: ClaimType[] = await response.json()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{claimsData &&
|
||||||
|
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||||
|
<DashboardHeader
|
||||||
|
userDisplayName={userData.name}
|
||||||
|
/>
|
||||||
|
<div className="flex w-full pr-23">
|
||||||
|
<div className={classnames("w-full flex flex-col pt-4 z-2 space-x-4 space-y-4 xl:space-y-0 text-bluemana")}>
|
||||||
|
<div className="w-full flex text-3xl ml-2 mb-4 font-semibold">
|
||||||
|
<p>Claims</p>
|
||||||
|
</div>
|
||||||
|
<div className={classnames("w-full flex flex-col px-2 text-lg", font.className)}>
|
||||||
|
<div className='flex w-full mb-4 border-b border-bluemana'>
|
||||||
|
<div className="flex w-2/20">
|
||||||
|
Claim #
|
||||||
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-3">
|
||||||
|
Patient Name
|
||||||
|
</div>
|
||||||
|
<div className="flex w-6/20 pl-3">
|
||||||
|
Provider
|
||||||
|
</div>
|
||||||
|
<div className="flex w-3/20 pl-3">
|
||||||
|
Visit Date
|
||||||
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-8.5">
|
||||||
|
Claim Status
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(claimsData).map((mc) => (
|
||||||
|
<div key={mc.claim_number} className="flex w-full text-platinum">
|
||||||
|
<div className="flex w-2/20">
|
||||||
|
{mc.claim_number}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-3">
|
||||||
|
{mc.patient_name}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-6/20 pl-3">
|
||||||
|
{mc.provider_name}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-3/20 pl-3">
|
||||||
|
{mc.visit_date}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-8.5">
|
||||||
|
{mc.claim_status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'NEXT_REDIRECT') throw error;
|
||||||
|
|
||||||
|
if (error.message === 'Unauthenticated') {
|
||||||
|
redirect('/account/signin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import DashboardHeader from "@/components/DashboardHeader";
|
||||||
|
import FindProviderWidget from '@/components/FindProvider';
|
||||||
|
import InsuranceCardWidget from '@/components/InsuranceCard';
|
||||||
|
import FindRxInformationWidget from '@/components/FindRxInformation';
|
||||||
|
import RecentClaimsWidget from '@/components/RecentClaimsWidget';
|
||||||
|
import BenefitsAAGWidget from '@/components/BenefitsAAGWidget';
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { JWTPayload, jwtVerify } from "jose";
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import IdCardViewer from '@/components/IdCardViewer';
|
||||||
|
|
||||||
|
// Ensure your secret key matches exactly what you used to sign the token
|
||||||
|
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||||
|
|
||||||
|
interface PlanBenefitType {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
sequence: number;
|
||||||
|
benefit: string;
|
||||||
|
benefit_desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecentClaimType {
|
||||||
|
claim_number: number;
|
||||||
|
visit_date: string;
|
||||||
|
claim_status: string;
|
||||||
|
patient_name: string;
|
||||||
|
provider_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DashboardDataType {
|
||||||
|
network_provider: string;
|
||||||
|
plan_benefits: PlanBenefitType[];
|
||||||
|
recent_claims: RecentClaimType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserJwtPayload extends JWTPayload {
|
||||||
|
user: UserData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
roleId: string;
|
||||||
|
keychain: UserKeychain;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserKeychain {
|
||||||
|
family_id: string;
|
||||||
|
mb_member_key: number;
|
||||||
|
pb_entity_key: number;
|
||||||
|
pl_plan_key: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Member() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
redirect('/account/signin')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Extract the raw string token from the cookie object
|
||||||
|
const token = sessionCookie.value;
|
||||||
|
|
||||||
|
// 3. Verify and decode the JWT using jose
|
||||||
|
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||||
|
|
||||||
|
// 3. Cast the payload to your custom interface
|
||||||
|
const data = payload as UserJwtPayload;
|
||||||
|
|
||||||
|
// const userDataTest = {
|
||||||
|
// pb_entity_key: 377527,
|
||||||
|
// pl_plan_key: '61',
|
||||||
|
// name: "John McMember"
|
||||||
|
// }
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
pb_entity_key: data.user.keychain.pb_entity_key,
|
||||||
|
pl_plan_key: data.user.keychain.pl_plan_key,
|
||||||
|
name: data.user.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/initialize_dashboard`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json' ,
|
||||||
|
'Authorization': `Bearer ${data.token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Unauthenticated");
|
||||||
|
|
||||||
|
const dashboardData: DashboardDataType = await response.json()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{dashboardData &&
|
||||||
|
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||||
|
<DashboardHeader
|
||||||
|
userDisplayName={userData.name}
|
||||||
|
/>
|
||||||
|
<div className="flex w-full pr-23">
|
||||||
|
<div className={classnames("w-full flex flex-col xl:flex-row pt-4 z-2 space-x-4 space-y-4 xl:space-y-0")}>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="flex flex-col w-full h-full xl:h-auto lg:flex-row gap-y-4 lg:gap-y-0 lg:gap-x-4">
|
||||||
|
{/* <DeductableProgressWidget /> */}
|
||||||
|
<IdCardViewer
|
||||||
|
pbEntityKey={userData.pb_entity_key}
|
||||||
|
cardLaout="FullPageCard"
|
||||||
|
/>
|
||||||
|
<FindProviderWidget
|
||||||
|
// pl_plan_key='57'
|
||||||
|
networkProvider={dashboardData.network_provider}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FindRxInformationWidget />
|
||||||
|
<RecentClaimsWidget
|
||||||
|
recentClaims={dashboardData.recent_claims}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BenefitsAAGWidget
|
||||||
|
planBenefits={dashboardData.plan_benefits}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'NEXT_REDIRECT') throw error;
|
||||||
|
|
||||||
|
if (error.message === 'Unauthenticated') {
|
||||||
|
redirect('/account/signin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// } catch (error) {
|
||||||
|
// // Fails if token is expired, tampered with, or invalid
|
||||||
|
// console.error("JWT verification failed:", error);
|
||||||
|
// return <div>Session invalid or expired.</div>;
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -1,63 +1,100 @@
|
|||||||
"user client"
|
|
||||||
|
|
||||||
import { sideNavItems } from '@/lib/dashboard';
|
import { sideNavItems } from '@/lib/dashboard';
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import Script from 'next/script';
|
import Script from 'next/script';
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { JWTPayload, jwtVerify } from "jose";
|
||||||
|
|
||||||
import AccountDropdown from '../../components/AccountDropdown';
|
import AccountDropdown from '../../components/AccountDropdown';
|
||||||
import SideNav from "@/components/SideNav";
|
import SideNav from "@/components/SideNav";
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||||
|
|
||||||
|
interface UserJwtPayload extends JWTPayload {
|
||||||
|
user: UserData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
roleId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
const headersList = await headers();
|
const cookieStore = await cookies();
|
||||||
const pathname = headersList.get("x-current-path");
|
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
|
||||||
const dashboardType = pathname?.split('/').pop() || "";
|
|
||||||
const pageNavItems = sideNavItems[dashboardType];
|
|
||||||
|
|
||||||
return (
|
if (!sessionCookie) {
|
||||||
<div className="relative w-full h-full">
|
return <div className='flex w-full justify-center'>No active session. Please log in.</div>;
|
||||||
<Script src={`https://maps.googleapis.com/maps/api/js?key=${process.env.GOOGLE_PLACES_API_KEY}&libraries=places`} strategy="beforeInteractive" />
|
}
|
||||||
<div className="absolute bottom-0 right-0 flex flex-col justify-end items-end w-full h-full">
|
|
||||||
<div className="w-full -mb-[2px] flex justify-start">
|
try {
|
||||||
<div className="w-[87vw] sm:w-[91vw] md:w-[93vw] lg:w-[94.5vw] xl:w-[95.5vw] h-20 border-b-2 border-copper z-2" />
|
// 2. Extract the raw string token from the cookie object
|
||||||
</div>
|
const token = sessionCookie.value;
|
||||||
<div className="w-[60px] h-full flex justify-start">
|
|
||||||
<div className="w-[32.5px] flex flex-col">
|
// 3. Verify and decode the JWT using jose
|
||||||
<div className={classnames(
|
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||||
"relative h-full border-r-6 rounded-out-tl-4xl rounded-tr-4xl bg-copper border-atmosphere"
|
|
||||||
)} />
|
// 3. Cast the payload to your custom interface
|
||||||
</div>
|
const data = payload as UserJwtPayload;
|
||||||
</div>
|
|
||||||
</div>
|
const userRole = data.user.role
|
||||||
<div className="w-full h-full flex items-start">
|
|
||||||
<SideNav
|
// const headersList = await headers();
|
||||||
maxWidth="w-[16vw]"
|
// const pathname = headersList.get("x-current-path");
|
||||||
bgColor="bg-atmosphere"
|
// const dashboardType = pathname?.split('/').pop() || "";
|
||||||
navItems={pageNavItems}
|
const pageNavItems = sideNavItems[userRole];
|
||||||
/>
|
|
||||||
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
|
return (
|
||||||
<div className="w-[90.15vw] h-20 flex justify-between items-center">
|
<div className="relative w-full h-full">
|
||||||
<div className={classnames('flex items-center')}>
|
<div className="absolute bottom-0 right-0 flex flex-col justify-end items-end w-full h-full">
|
||||||
<div className="-ml-8.5 z-2">
|
<div className="w-full -mb-[2px] flex justify-start">
|
||||||
<Image
|
<div className="w-[87vw] sm:w-[91vw] md:w-[93vw] lg:w-[94.5vw] xl:w-[95.5vw] h-20 border-b-2 border-copper z-2" />
|
||||||
src="/images/bbstpa-forsite.png"
|
</div>
|
||||||
alt="Britton Logo"
|
<div className="w-[60px] h-full flex justify-start">
|
||||||
width={205}
|
<div className="w-[32.5px] flex flex-col">
|
||||||
height={60}
|
<div className={classnames(
|
||||||
/>
|
"relative h-full border-r-6 rounded-out-tl-4xl rounded-tr-4xl bg-copper border-atmosphere"
|
||||||
|
)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pr-20 z-2">
|
</div>
|
||||||
<AccountDropdown userRole="member" />
|
<div className="w-full h-full flex items-start">
|
||||||
|
<SideNav
|
||||||
|
maxWidth="w-[16vw]"
|
||||||
|
bgColor="bg-atmosphere"
|
||||||
|
navItems={pageNavItems}
|
||||||
|
/>
|
||||||
|
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
|
||||||
|
<div className="w-[90.15vw] h-20 flex justify-between items-center">
|
||||||
|
<div className={classnames('flex items-center')}>
|
||||||
|
<div className="-ml-4.5 z-2">
|
||||||
|
<Image
|
||||||
|
src="/images/bbstpa-forsite.png"
|
||||||
|
alt="Britton Logo"
|
||||||
|
width={205}
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pr-20 z-2">
|
||||||
|
<AccountDropdown userRole="member" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full flex justify-start">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full flex justify-start">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
</div>
|
} catch (error) {
|
||||||
)
|
// Fails if token is expired, tampered with, or invalid
|
||||||
|
console.error("JWT verification failed:", error);
|
||||||
|
return <div className='flex w-full justify-center'>Session invalid or expired.</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// /app/login/page.tsx
|
||||||
|
'use test' // Marks this component as a client component
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { useActionState } from 'react'
|
||||||
|
import { loginAndFetchUser } from '@/app/actions/signin'
|
||||||
|
|
||||||
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
// useActionState manages the server state (errors, loading) and form binding
|
||||||
|
const [state, formAction, isPending] = useActionState(loginAndFetchUser, null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="min-h-full flex flex-col items-center justify-center py-6 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center text-7xl font-bold text-atmosphere">
|
||||||
|
Britton Benefits
|
||||||
|
{/* <p>Britton</p>
|
||||||
|
<p>Benefits</p> */}
|
||||||
|
</div>
|
||||||
|
<div className={classnames("w-sm space-y-4", font.className)}>
|
||||||
|
<div className="mt-10 text-center text-3xl font-extrabold text-light-bronze border-b-1 border-light-bronze">
|
||||||
|
Sign In
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action={formAction}>
|
||||||
|
{/* Error Message Display */}
|
||||||
|
{state?.error && (
|
||||||
|
<div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-1">
|
||||||
|
<div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
<label className="block text-md font-bold text-platinum">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
className="appearance-none rounded-none relative block w-full px-3 py-2 bg-platinum border border-gray-300 placeholder-bluetang text-deepcove rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
placeholder=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isPending}
|
||||||
|
className="relative w-full flex justify-center py-2 px-4 mt-6 border-4 border-cobalt font-bold rounded-lg text-lg text-platinum hover:text-light-bronze bg-cobalt hover:bg-deepcove hover:cursor-pointer"
|
||||||
|
>
|
||||||
|
{isPending ? 'Signing In...' : 'Submit'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import classnames from 'classnames';
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { JWTPayload, jwtVerify } from "jose";
|
||||||
|
|
||||||
|
import DashboardHeader from "@/components/DashboardHeader";
|
||||||
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||||
|
|
||||||
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||||
|
|
||||||
|
interface ClaimType {
|
||||||
|
claim_number: number;
|
||||||
|
visit_date: string;
|
||||||
|
claim_status: string;
|
||||||
|
patient_name: string;
|
||||||
|
provider_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserJwtPayload extends JWTPayload {
|
||||||
|
user: UserData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
roleId: string;
|
||||||
|
keychain: UserKeychain;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserKeychain {
|
||||||
|
family_id: string;
|
||||||
|
mb_member_key: number;
|
||||||
|
pb_entity_key: number;
|
||||||
|
pl_plan_key: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function MemberClaims() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const sessionCookie = cookieStore.get("session");
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
redirect('/account/signin')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Extract the raw string token from the cookie object
|
||||||
|
const token = sessionCookie.value;
|
||||||
|
|
||||||
|
// 3. Verify and decode the JWT using jose
|
||||||
|
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||||
|
|
||||||
|
// 3. Cast the payload to your custom interface
|
||||||
|
const data = payload as UserJwtPayload;
|
||||||
|
|
||||||
|
// const userDataTest = {
|
||||||
|
// pb_entity_key: 377527,
|
||||||
|
// pl_plan_key: '61',
|
||||||
|
// name: "John McMember"
|
||||||
|
// }
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
pb_entity_key: data.user.keychain.pb_entity_key,
|
||||||
|
pl_plan_key: data.user.keychain.pl_plan_key,
|
||||||
|
name: data.user.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/claims`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json' ,
|
||||||
|
'Authorization': `Bearer ${data.token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Unauthenticated");
|
||||||
|
|
||||||
|
|
||||||
|
const claimsData: ClaimType[] = await response.json()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{claimsData &&
|
||||||
|
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||||
|
<DashboardHeader
|
||||||
|
userDisplayName={userData.name}
|
||||||
|
/>
|
||||||
|
<div className="flex w-full pr-23">
|
||||||
|
<div className={classnames("w-full flex flex-col pt-4 z-2 space-x-4 space-y-4 xl:space-y-0 text-bluemana")}>
|
||||||
|
<div className="w-full flex text-3xl ml-2 mb-4 font-semibold">
|
||||||
|
<p>Claims</p>
|
||||||
|
</div>
|
||||||
|
<div className={classnames("w-full flex flex-col px-2 text-lg", font.className)}>
|
||||||
|
<div className='flex w-full mb-4 border-b border-bluemana'>
|
||||||
|
<div className="flex w-2/20">
|
||||||
|
Claim #
|
||||||
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-3">
|
||||||
|
Patient Name
|
||||||
|
</div>
|
||||||
|
<div className="flex w-6/20 pl-3">
|
||||||
|
Provider
|
||||||
|
</div>
|
||||||
|
<div className="flex w-3/20 pl-3">
|
||||||
|
Visit Date
|
||||||
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-8.5">
|
||||||
|
Claim Status
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(claimsData).map((mc) => (
|
||||||
|
<div key={mc.claim_number} className="flex w-full text-platinum">
|
||||||
|
<div className="flex w-2/20">
|
||||||
|
{mc.claim_number}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-3">
|
||||||
|
{mc.patient_name}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-6/20 pl-3">
|
||||||
|
{mc.provider_name}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-3/20 pl-3">
|
||||||
|
{mc.visit_date}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-8.5">
|
||||||
|
{mc.claim_status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'NEXT_REDIRECT') throw error;
|
||||||
|
|
||||||
|
if (error.message === 'Unauthenticated') {
|
||||||
|
redirect('/account/signin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,149 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import DashboardHeader from "@/components/DashboardHeader";
|
import DashboardHeader from "@/components/DashboardHeader";
|
||||||
import FindProvider from '@/components/FindProvider';
|
import FindProviderWidget from '@/components/FindProvider';
|
||||||
import InsuranceCardWidget from '@/components/InsuranceCard';
|
import InsuranceCardWidget from '@/components/InsuranceCard';
|
||||||
import DeductableProgressWidget from '@/components/DeductableProgressWidget';
|
import FindRxInformationWidget from '@/components/FindRxInformation';
|
||||||
import RecentClaimsWidget from '@/components/RecentClaimsWidget';
|
import RecentClaimsWidget from '@/components/RecentClaimsWidget';
|
||||||
import BenefitsAAGWidget from '@/components/BenefitsAAGWidget';
|
import BenefitsAAGWidget from '@/components/BenefitsAAGWidget';
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { JWTPayload, jwtVerify } from "jose";
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import IdCardViewer from '@/components/IdCardViewer';
|
||||||
|
|
||||||
export default function Member() {
|
// Ensure your secret key matches exactly what you used to sign the token
|
||||||
return (
|
const SECRET_KEY = new TextEncoder().encode(process.env.JWT_SECRET_KEY);
|
||||||
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
|
||||||
<DashboardHeader
|
interface PlanBenefitType {
|
||||||
userDisplayName="Robert La'Blah"
|
id: number;
|
||||||
/>
|
title: string;
|
||||||
<div className="flex w-full pr-23">
|
sequence: number;
|
||||||
<div className={classnames("w-full flex flex-col xl:flex-row pt-4 z-2 space-x-4 space-y-4 xl:space-y-0")}>
|
benefit: string;
|
||||||
<div className="flex flex-col space-y-4">
|
benefit_desc: string;
|
||||||
<div className="flex flex-col w-full h-full xl:h-auto lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4">
|
}
|
||||||
<DeductableProgressWidget />
|
|
||||||
<InsuranceCardWidget />
|
interface RecentClaimType {
|
||||||
|
claim_number: number;
|
||||||
|
visit_date: string;
|
||||||
|
claim_status: string;
|
||||||
|
patient_name: string;
|
||||||
|
provider_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DashboardDataType {
|
||||||
|
network_provider: string;
|
||||||
|
plan_benefits: PlanBenefitType[];
|
||||||
|
recent_claims: RecentClaimType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserJwtPayload extends JWTPayload {
|
||||||
|
user: UserData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
roleId: string;
|
||||||
|
keychain: UserKeychain;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserKeychain {
|
||||||
|
family_id: string;
|
||||||
|
mb_member_key: number;
|
||||||
|
pb_entity_key: number;
|
||||||
|
pl_plan_key: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Member() {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
|
||||||
|
|
||||||
|
if (!sessionCookie) {
|
||||||
|
redirect('/account/signin')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Extract the raw string token from the cookie object
|
||||||
|
const token = sessionCookie.value;
|
||||||
|
|
||||||
|
// 3. Verify and decode the JWT using jose
|
||||||
|
const { payload } = await jwtVerify(token, SECRET_KEY)
|
||||||
|
|
||||||
|
// 3. Cast the payload to your custom interface
|
||||||
|
const data = payload as UserJwtPayload;
|
||||||
|
|
||||||
|
// const userDataTest = {
|
||||||
|
// pb_entity_key: 377527,
|
||||||
|
// pl_plan_key: '61',
|
||||||
|
// name: "John McMember"
|
||||||
|
// }
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
pb_entity_key: data.user.keychain.pb_entity_key,
|
||||||
|
pl_plan_key: data.user.keychain.pl_plan_key,
|
||||||
|
name: data.user.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`http://${process.env.BE_API_SERVER_HOST}:${process.env.BE_API_SERVER_PORT}/api/v1/members/${userData.pb_entity_key}/initialize_dashboard`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json' ,
|
||||||
|
'Authorization': `Bearer ${data.token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Unauthenticated");
|
||||||
|
|
||||||
|
const dashboardData: DashboardDataType = await response.json()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{dashboardData &&
|
||||||
|
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||||
|
<DashboardHeader
|
||||||
|
userDisplayName={userData.name}
|
||||||
|
/>
|
||||||
|
<div className="flex w-full pr-23">
|
||||||
|
<div className={classnames("w-full flex flex-col xl:flex-row pt-4 z-2 space-x-4 space-y-4 xl:space-y-0")}>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<div className="flex flex-col w-full h-full xl:h-auto lg:flex-row gap-y-4 lg:gap-y-0 lg:gap-x-4">
|
||||||
|
{/* <DeductableProgressWidget /> */}
|
||||||
|
<IdCardViewer
|
||||||
|
pbEntityKey={userData.pb_entity_key}
|
||||||
|
cardLaout="FullPageCard"
|
||||||
|
/>
|
||||||
|
<FindProviderWidget
|
||||||
|
// pl_plan_key='57'
|
||||||
|
networkProvider={dashboardData.network_provider}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FindRxInformationWidget />
|
||||||
|
<RecentClaimsWidget
|
||||||
|
recentClaims={dashboardData.recent_claims}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FindProvider />
|
<BenefitsAAGWidget
|
||||||
<RecentClaimsWidget />
|
planBenefits={dashboardData.plan_benefits}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<BenefitsAAGWidget />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
)
|
</>
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.message === 'NEXT_REDIRECT') throw error;
|
||||||
|
|
||||||
|
if (error.message === 'Unauthenticated') {
|
||||||
|
redirect('/account/signin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// } catch (error) {
|
||||||
|
// // Fails if token is expired, tampered with, or invalid
|
||||||
|
// console.error("JWT verification failed:", error);
|
||||||
|
// return <div>Session invalid or expired.</div>;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<main className={classNames("bg-deepcove min-h-screen flex justify-center", font.className)}>
|
<main className={classNames("bg-deepcove h-full min-h-screen flex justify-center", font.className)}>
|
||||||
<div className="w-full max-w-[1920px]">
|
<div className="w-full max-w-[1920px]">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,33 +4,11 @@ import InlineLink from "../components/InlineLink";
|
|||||||
import Navbar from '@/components/Navbar';
|
import Navbar from '@/components/Navbar';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
|
|
||||||
import { getClient } from "@/lib/graphqlClient";
|
|
||||||
import { gql } from "@apollo/client";
|
|
||||||
|
|
||||||
const HOME_GET_DATA_QUERY = gql`
|
|
||||||
query {
|
|
||||||
homePage {
|
|
||||||
headline
|
|
||||||
section1Header
|
|
||||||
section1Body
|
|
||||||
section1Cta
|
|
||||||
section2Header
|
|
||||||
section2Body
|
|
||||||
section2Cta
|
|
||||||
section3Header
|
|
||||||
section3Body
|
|
||||||
section3Cta
|
|
||||||
section4Header
|
|
||||||
section4Body
|
|
||||||
section4Cta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const { data } = await getClient().query({
|
// const { data } = await getClient().query({
|
||||||
query: HOME_GET_DATA_QUERY,
|
// query: HOME_GET_DATA_QUERY,
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -41,40 +19,42 @@ export default async function Home() {
|
|||||||
textTop="Healthcare that's"
|
textTop="Healthcare that's"
|
||||||
textBottom="truly affordable"
|
textBottom="truly affordable"
|
||||||
/>
|
/>
|
||||||
<div className="pt-4">
|
<div className="pt-10">
|
||||||
<InfoCard
|
<InfoCard
|
||||||
title={data.homePage.section1Header}
|
title="Members"
|
||||||
learnMoreUrl={data.homePage.section1Cta}
|
learnMoreUrl="/members"
|
||||||
imageUrl="/images/members-stock.jpg"
|
imageUrl="/images/member-image.jpg"
|
||||||
>
|
>
|
||||||
{data.homePage.section1Body}
|
What we do for members
|
||||||
</InfoCard>
|
</InfoCard>
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-atmosphere rounded-3xl'>
|
<div className='bg-atmosphere rounded-3xl'>
|
||||||
<InfoCard
|
<InfoCard
|
||||||
title={data.homePage.section2Header}
|
title="Employers"
|
||||||
learnMoreUrl={data.homePage.section2Cta}
|
learnMoreUrl="/employers"
|
||||||
imageUrl='/images/employers-stock.jpg'
|
imageUrl='/images/employer-image.jpg'
|
||||||
invert
|
invert
|
||||||
>
|
>
|
||||||
{data.homePage.section2Body}
|
What we do for employers
|
||||||
</InfoCard>
|
</InfoCard>
|
||||||
</div>
|
</div>
|
||||||
<InfoCard
|
<div className="w-full">
|
||||||
title={data.homePage.section3Header}
|
|
||||||
learnMoreUrl={data.homePage.section3Cta}
|
|
||||||
imageUrl="/images/providers-stock.jpg"
|
|
||||||
>
|
|
||||||
{data.homePage.section3Body}
|
|
||||||
</InfoCard>
|
|
||||||
<div className='bg-platinum rounded-3xl'>
|
|
||||||
<InfoCard
|
<InfoCard
|
||||||
title={data.homePage.section4Header}
|
title="Providers"
|
||||||
learnMoreUrl={data.homePage.section4Cta}
|
learnMoreUrl="/providers"
|
||||||
imageUrl="/images/brokers-stock.jpg"
|
imageUrl="/images/provider-image.png"
|
||||||
|
>
|
||||||
|
What we do for providers
|
||||||
|
</InfoCard>
|
||||||
|
</div>
|
||||||
|
<div className='bg-light-bronze rounded-3xl'>
|
||||||
|
<InfoCard
|
||||||
|
title="Brokers"
|
||||||
|
learnMoreUrl="/brokers"
|
||||||
|
imageUrl="/images/broker-image.jpg"
|
||||||
invert
|
invert
|
||||||
>
|
>
|
||||||
{data.homePage.section4Body}
|
What we do for brokers
|
||||||
</InfoCard>
|
</InfoCard>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ const AccountDropdown: React.FC<DropdownProps> = ({userRole}) => {
|
|||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuItems anchor="bottom end" className={"py-1 bg-deepcove border-2 border-copper focus:outline-hidden text-platinum text-md text-right font-semibold rounded-lg z-2"}>
|
<MenuItems anchor="bottom end" className={"py-1 bg-deepcove border-2 border-copper focus:outline-hidden text-platinum text-md text-right font-semibold rounded-lg z-2"}>
|
||||||
<MenuItem as="div" className={"px-3 py-1.5"}>
|
<MenuItem as="div" className={"px-3 py-1.5"}>
|
||||||
<a className="block data-focus:text-bluemana hover:text-bluemana" href="/">
|
<a className="block data-focus:text-bluemana hover:text-bluemana" href="/dashboard/member">
|
||||||
Change Password
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem as="div" className={"px-3 py-1.5"}>
|
<MenuItem as="div" className={"px-3 py-1.5"}>
|
||||||
|
|||||||
@@ -3,50 +3,42 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import { Schibsted_Grotesk } from 'next/font/google'
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
interface BenefitsAAGWidgetProps {
|
interface BenefitsAAGWidgetProps {
|
||||||
|
planBenefits: PlanBenefitType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const BenefitsAAGWidget = () => {
|
interface PlanBenefitType {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
sequence: number;
|
||||||
|
benefit: string;
|
||||||
|
benefit_desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BenefitsAAGWidget: React.FC<BenefitsAAGWidgetProps> = ({ planBenefits }) => {
|
||||||
|
|
||||||
|
const planTitle = planBenefits[0]?.title || "Unknown Plan Benefits"
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="2xl:min-w-[32vw] xl:min-w-[30vw] w-full flex flex-col justify-start items-center px-4 py-3 text-xl border border-6 border-platinum rounded-4xl">
|
<div className="2xl:min-w-[32vw] xl:min-w-[25vw] w-full flex flex-col justify-start items-center px-4 py-3 text-xl text-platinum border border-6 border-platinum rounded-4xl">
|
||||||
<p className="text-3xl underline py-2">Benefits At A Glance</p>
|
<p className="text-3xl underline py-2">Benefits At A Glance</p>
|
||||||
<p className={classnames("text-2xl font-semibold py-2", font.className)}>CLASSIC 5K 100/5000</p>
|
<p className={classnames("text-2xl font-semibold py-2", font.className)}>{planTitle}</p>
|
||||||
<div className={classnames("w-full flex justify-between", font.className)}>
|
<div className={classnames("w-full flex-col items-between px-4", font.className)}>
|
||||||
<div className="flex flex-col">
|
{(planBenefits).map((mb) => (
|
||||||
<p>Physician Visit</p>
|
<div key={mb.sequence} className="flex items-center w-full">
|
||||||
<p>Specialist Visit</p>
|
<div className="flex-none">
|
||||||
<p>Urgent Care</p>
|
{mb.benefit_desc}
|
||||||
<p>INN-Ind Ded</p>
|
</div>
|
||||||
<p>INN-Family Ded</p>
|
<div className="flex-grow h-[1px] mx-1 mt-1 bg-verdigris"></div>
|
||||||
<p>OON-Ind Ded</p>
|
<div className="flex-none justify-end">
|
||||||
<p>OON-Family Ded</p>
|
{mb.benefit}
|
||||||
<p>Co-Insurance</p>
|
</div>
|
||||||
<p>INN-Ind OOP</p>
|
</div>
|
||||||
<p>INN-Family OOP</p>
|
))}
|
||||||
<p>OON-Ind OOP</p>
|
|
||||||
<p>OON-Family OOP</p>
|
|
||||||
<p>Emergency Room</p>
|
|
||||||
<p>Preventive Care</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<p>$25.00</p>
|
|
||||||
<p>$50.00</p>
|
|
||||||
<p>$50.00</p>
|
|
||||||
<p>$5,000</p>
|
|
||||||
<p>$10,000</p>
|
|
||||||
<p>$10,000</p>
|
|
||||||
<p>$20,000</p>
|
|
||||||
<p>100%/0%</p>
|
|
||||||
<p>$5,000</p>
|
|
||||||
<p>$10,000</p>
|
|
||||||
<p>$10,000</p>
|
|
||||||
<p>$20,000</p>
|
|
||||||
<p>Ded & Coin</p>
|
|
||||||
<p>100%</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ import classnames from 'classnames';
|
|||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button: React.FC<ButtonProps> = ({ onClick, children, className }) => {
|
const Button: React.FC<ButtonProps> = ({ onClick, disabled, children, className }) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
className, {
|
className, {
|
||||||
'cursor-pointer': isHovered,
|
'cursor-pointer': isHovered,
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
|
||||||
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
interface DashboardHeaderProps {
|
interface DashboardHeaderProps {
|
||||||
userDisplayName: string;
|
userDisplayName: string;
|
||||||
@@ -9,7 +12,8 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({ userDisplayName }) =>
|
|||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col justify-start">
|
<div className="w-full flex flex-col justify-start">
|
||||||
<div className={classnames(
|
<div className={classnames(
|
||||||
"-mb-[34px] text-4xl text-left z-2 text-bluemana font-semibold text-shadow-[1px_1px_0,-1px_1px_0,-1px_-1px_0,1px_-1px_0] text-shadow-deepcove"
|
"-mb-[34px] ml-1 text-4xl text-left z-2 text-bluemana font-semibold text-shadow-[1px_1px_0,-1px_1px_0,-1px_-1px_0,1px_-1px_0] text-shadow-deepcove",
|
||||||
|
font.className
|
||||||
)}>
|
)}>
|
||||||
{userDisplayName}
|
{userDisplayName}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, ChangeEvent } from 'react';
|
||||||
|
import Button from './Button';
|
||||||
|
|
||||||
|
interface SelectOption {
|
||||||
|
entity_key: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmployerMemberSelectProps {
|
||||||
|
onSelectionCompleteEmployer: (employerValue: string) => void;
|
||||||
|
onSelectionCompleteMember: (memberValue: string) => void;
|
||||||
|
userRole: 'employer' | 'broker' | 'carrier';
|
||||||
|
employerAllButtonText: string;
|
||||||
|
plPlanKey?: string;
|
||||||
|
employerName?: string;
|
||||||
|
brokerId?: number;
|
||||||
|
carrierId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmployerMemberSelect: React.FC<EmployerMemberSelectProps> = ({ onSelectionCompleteEmployer, onSelectionCompleteMember, userRole, employerAllButtonText, plPlanKey, employerName, brokerId, carrierId }) => {
|
||||||
|
const [optionsEmployer, setOptionsEmployer] = useState<SelectOption[]>([]);
|
||||||
|
const [selectedEmployer, setSelectedEmployer] = useState<string>('');
|
||||||
|
const [loadingEmployer, setLoadingEmployer] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [optionsMember, setOptionsMember] = useState<SelectOption[]>([]);
|
||||||
|
const [selectedMember, setSelectedMember] = useState<string>('');
|
||||||
|
const [loadingMember, setLoadingMember] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [apiUrl, setapiUrl] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/backendApiUrl")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => setapiUrl(data.apiUrl));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchOptionsEmployer(role: string, roleId: number) {
|
||||||
|
setLoadingEmployer(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://${apiUrl}/api/v1/${role}/${roleId}/employers_list`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
setOptionsEmployer(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching employer options:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingEmployer(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (plPlanKey && employerName) {
|
||||||
|
setSelectedEmployer(plPlanKey)
|
||||||
|
setOptionsEmployer([{entity_key: plPlanKey, name: employerName}]);
|
||||||
|
} else if (brokerId) {
|
||||||
|
fetchOptionsEmployer("brokers", brokerId);
|
||||||
|
} else if (carrierId) {
|
||||||
|
fetchOptionsEmployer("carriers", carrierId);
|
||||||
|
}
|
||||||
|
}, [apiUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedEmployer) {
|
||||||
|
setOptionsMember([]);
|
||||||
|
setSelectedMember('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOptionsMember() {
|
||||||
|
setLoadingMember(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://${apiUrl}/api/v1/employers/${selectedEmployer}/members_list`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
setOptionsMember(data);
|
||||||
|
setSelectedMember('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching second options:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingMember(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchOptionsMember();
|
||||||
|
}, [selectedEmployer]);
|
||||||
|
|
||||||
|
const handleFirstChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setSelectedEmployer(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAllEmployerSelect = () => {
|
||||||
|
if (selectedEmployer) {
|
||||||
|
onSelectionCompleteEmployer(selectedEmployer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecondChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setSelectedMember(val);
|
||||||
|
// if (val) {
|
||||||
|
// onSelectionCompleteMember(val);
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMemberSelect = () => {
|
||||||
|
if (selectedMember) {
|
||||||
|
onSelectionCompleteMember(selectedMember);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 max-w-xs">
|
||||||
|
{/* First Select Dropdown */}
|
||||||
|
{!plPlanKey &&
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium">Employer</label>
|
||||||
|
<div className="flex">
|
||||||
|
<select
|
||||||
|
value={selectedEmployer}
|
||||||
|
onChange={handleFirstChange}
|
||||||
|
disabled={loadingEmployer}
|
||||||
|
className="border p-2 rounded"
|
||||||
|
>
|
||||||
|
<option value="">{loadingEmployer ? 'Loading...' : '-- Select Employer --'}</option>
|
||||||
|
{optionsEmployer.map((opt) => (
|
||||||
|
<option key={opt.entity_key} value={opt.entity_key}>
|
||||||
|
{opt.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Button
|
||||||
|
onClick={handleAllEmployerSelect}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center items-center rounded-r-2">
|
||||||
|
{employerAllButtonText}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* Second Select Dropdown */}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium">Member</label>
|
||||||
|
<div className="flex">
|
||||||
|
<select
|
||||||
|
value={selectedMember}
|
||||||
|
onChange={handleSecondChange}
|
||||||
|
disabled={!selectedEmployer || loadingMember}
|
||||||
|
className="border p-2 rounded"
|
||||||
|
>
|
||||||
|
<option value="">
|
||||||
|
{!selectedEmployer
|
||||||
|
? '-- Select a category first --'
|
||||||
|
: loadingMember ? 'Loading...' : '-- Select Subcategory --'}
|
||||||
|
</option>
|
||||||
|
{optionsMember.map((opt) => (
|
||||||
|
<option key={opt.entity_key} value={opt.entity_key}>
|
||||||
|
{opt.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Button
|
||||||
|
onClick={handleAllEmployerSelect}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center items-center rounded-r-2">
|
||||||
|
Select Member
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,57 +1,60 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import Image from 'next/image';
|
||||||
import { MdLocationOn } from "react-icons/md";
|
|
||||||
import LocationSearchInput from '@/components/LocationSearchInput';
|
|
||||||
|
|
||||||
import { Schibsted_Grotesk } from 'next/font/google'
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
interface FindProviderProps {
|
import Link from 'next/link';
|
||||||
zipOnly?: boolean;
|
|
||||||
|
interface FindProviderWidgetProps {
|
||||||
|
networkProvider: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FindProvider: React.FC<FindProviderProps> = ({ zipOnly }) => {
|
interface NetworkProvider {
|
||||||
|
network_provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FindProviderWidget: React.FC<FindProviderWidgetProps> = ({ networkProvider }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="2xl:min-w-[48vw] xl:min-w-[45vw] flex z-1">
|
<div className="2xl:min-w-[16vw] xl:min-w-[15vw] w-full h-full flex lg:flex-col justify-center items-center text-xl space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl">
|
||||||
<div className="w-full flex flex-col px-4 py-3 border border-6 border-bluemana rounded-4xl">
|
<div className="flex flex-col justify-end items-center mb-4">
|
||||||
<div className="w-full pb-2 flex justify-center text-2xl text-bronze font-semibold">
|
{networkProvider &&
|
||||||
<p>Find A Provider By...</p>
|
<div className="flex items-end mb-3">
|
||||||
|
<div className="text-platinum font-semibold">Find A {networkProvider} Provider</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex px-2">
|
}
|
||||||
<div className="w-full flex border-x-2 border-t-2 rounded-t-xl font-semibold divide-x-2 divide-bronze">
|
{networkProvider == "Cigna" &&
|
||||||
<a href="#" className="w-full h-9 flex justify-center items-center rounded-tl-lg bg-bluetang text-platinum">
|
<Link href="http://cigna.com" target="_blank" rel="noopener noreferrer">
|
||||||
Location
|
<Button className="flex items-center justify-center bg-cobalt rounded hover:bg-copper rounded-xl">
|
||||||
</a>
|
<Image
|
||||||
<a href="#" className="w-full h-9 flex justify-center items-center text-platinum hover:text-bronze">
|
src="/images/Cigna_deepcove.svg"
|
||||||
Procedure
|
alt="Cigna Logo"
|
||||||
</a>
|
width={150}
|
||||||
<a href="#" className="w-full h-9 flex justify-center items-center rounded-tr-lg text-platinum hover:text-bronze">
|
height={150}
|
||||||
Provider/Facility
|
priority
|
||||||
</a>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={classnames("w-full flex items-start px-2 pb-2 text-xl", font.className)}>
|
|
||||||
{/* <p className='pr-3'><GrMapLocation size={30} /></p> */}
|
|
||||||
<Button className="h-10 text-deepcove hover:text-atmosphere bg-platinum rounded-bl-xl">
|
|
||||||
<MdLocationOn size={30} />
|
|
||||||
</Button>
|
</Button>
|
||||||
<LocationSearchInput className="h-10 pl-3 text-bluemana border-y-2 border-platinum" />
|
</Link>
|
||||||
<Button className="h-10 pr-3 pl-2 bg-platinum hover:bg-bluemana text-center text-deepcove font-semibold rounded-br-xl border-y-2 border-r-2 border-platinum">
|
}
|
||||||
Search
|
{networkProvider == "MedCost" &&
|
||||||
|
<Link href="http://medcost.com" target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button className="flex items-center justify-center bg-cobalt rounded hover:bg-copper rounded-xl">
|
||||||
|
<Image
|
||||||
|
src="/images/MedCost_deepcove.svg"
|
||||||
|
alt="MedCost Logo"
|
||||||
|
width={150}
|
||||||
|
height={150}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Link>
|
||||||
<div className="w-full flex justify-end text-platinum font-semibold">
|
}
|
||||||
<Button className="mr-2 underline hover:text-bronze">
|
|
||||||
View Full Provider List
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FindProvider;
|
export default FindProviderWidget;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
|
import { MdLocationOn } from "react-icons/md";
|
||||||
|
import LocationSearchInput from '@/components/LocationSearchInput';
|
||||||
|
|
||||||
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
interface FindProviderProps {
|
||||||
|
zipOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FindProvider: React.FC<FindProviderProps> = ({ zipOnly }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="2xl:min-w-[48vw] xl:min-w-[45vw] flex z-1">
|
||||||
|
<div className="w-full flex flex-col px-4 py-3 border border-6 border-bluemana rounded-4xl">
|
||||||
|
<div className="w-full pb-2 flex justify-center text-2xl text-bronze font-semibold">
|
||||||
|
<p>Find A Provider By...</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex px-2">
|
||||||
|
<div className="w-full flex border-x-2 border-t-2 rounded-t-xl font-semibold divide-x-2 divide-bronze">
|
||||||
|
<a href="#" className="w-full h-9 flex justify-center items-center rounded-tl-lg bg-bluetang text-platinum">
|
||||||
|
Location
|
||||||
|
</a>
|
||||||
|
<a href="#" className="w-full h-9 flex justify-center items-center text-platinum hover:text-bronze">
|
||||||
|
Procedure
|
||||||
|
</a>
|
||||||
|
<a href="#" className="w-full h-9 flex justify-center items-center rounded-tr-lg text-platinum hover:text-bronze">
|
||||||
|
Provider/Facility
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classnames("w-full flex items-start px-2 pb-2 text-xl", font.className)}>
|
||||||
|
{/* <p className='pr-3'><GrMapLocation size={30} /></p> */}
|
||||||
|
<Button className="h-10 text-deepcove hover:text-atmosphere bg-platinum rounded-bl-xl">
|
||||||
|
<MdLocationOn size={30} />
|
||||||
|
</Button>
|
||||||
|
<LocationSearchInput className="h-10 pl-3 text-bluemana border-y-2 border-platinum" />
|
||||||
|
<Button className="h-10 pr-3 pl-2 bg-platinum hover:bg-bluemana text-center text-deepcove font-semibold rounded-br-xl border-y-2 border-r-2 border-platinum">
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex justify-end text-platinum font-semibold">
|
||||||
|
<Button className="mr-2 underline hover:text-bronze">
|
||||||
|
View Full Provider List
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FindProvider;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import { FaRegIdCard } from "react-icons/fa";
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
const FindRxInformationWidget = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="2xl:min-w-[32vw] xl:min-w-30vw] w-full h-full flex lg:flex-col justify-center items-center text-xl space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl">
|
||||||
|
<div className="flex flex-col justify-end items-center">
|
||||||
|
<div className="flex items-end mb-3">
|
||||||
|
<div className="text-platinum font-semibold">Find Rx Information</div>
|
||||||
|
</div>
|
||||||
|
<Link href="https://www.fairosrx.com/" target="_blank" rel="noopener noreferrer" className='group mb-3'>
|
||||||
|
<Button className="block group-hover:hidden items-center justify-center bg-platinum rounded rounded-xl p-1">
|
||||||
|
<Image
|
||||||
|
src="/images/Fairosrx.svg"
|
||||||
|
alt="FairosRx Logo"
|
||||||
|
width={300}
|
||||||
|
height={150}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button className="hidden group-hover:block items-center justify-center bg-platinum rounded rounded-xl p-1">
|
||||||
|
<Image
|
||||||
|
src="/images/Fairosrx_inverted.svg"
|
||||||
|
alt="Inverted FairosRx Logo"
|
||||||
|
width={300}
|
||||||
|
height={150}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FindRxInformationWidget;
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface IdCardDownloaderProps {
|
||||||
|
plPlanKey: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IdCardDownloader: React.FC<IdCardDownloaderProps> = ({ plPlanKey }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [apiUrl, setapiUrl] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/backendApiUrl")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => setapiUrl(data.apiUrl));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://${apiUrl}/api/v1/employers/${plPlanKey}/id_cards`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/zip',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to download');
|
||||||
|
|
||||||
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
|
console.log("contentDisposition: " + contentDisposition);
|
||||||
|
let fileName = 'employer_id_cards.zip'; // Default fallback name
|
||||||
|
|
||||||
|
// 2. Parse the filename using a regular expression
|
||||||
|
if (contentDisposition) {
|
||||||
|
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||||
|
if (filenameMatch && filenameMatch[1]) {
|
||||||
|
// Remove surrounding quotes if they exist
|
||||||
|
fileName = filenameMatch[1].replace(/['"]/g, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(fileName)
|
||||||
|
|
||||||
|
// Convert the response to a Blob
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
// Create a temporary object URL
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Create an invisible link and click it
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// Clean up memory
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<button
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={loading}
|
||||||
|
className="text-platinum underline hover:text-bluemana focus:outline-none"
|
||||||
|
>
|
||||||
|
Download Employer Cards
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{(loading) && (
|
||||||
|
<div className="fixed inset-x-1/6 inset-y-1/12 flex items-center justify-center w-2/3 h-[80vh] bg-atmosphere border-2 border-platinum shadow-md rounded-lg z-2">
|
||||||
|
{loading && (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
{/* Spinner */}
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-20 w-20 text-deepcove mb-2"
|
||||||
|
xmlns="http://w3.org"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-45" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<p className="text-deepcove text-lg font-medium">Generating ID Cards...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loading Animation */}
|
||||||
|
{/* {loading && (
|
||||||
|
<div className="flex h-[70vh] w-screen items-center gap-2 text-gray-500 animate-pulse z-2">
|
||||||
|
<svg className="animate-spin h-10 w-10 text-blue-600" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
Fetching ID Card, Please Wait...
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{/* Display PDF */}
|
||||||
|
{/* {pdfUrl && (
|
||||||
|
<div className="w-screen h-[70vh] border border-gray-300 rounded shadow-lg overflow-hidden z-2">
|
||||||
|
<iframe
|
||||||
|
src={pdfUrl}
|
||||||
|
title="PDF Document"
|
||||||
|
className="w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IdCardDownloader;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import Button from "./Button";
|
||||||
|
import { FaRegIdCard } from "react-icons/fa";
|
||||||
|
|
||||||
|
interface IdCardViewerProps {
|
||||||
|
cardLaout: 'FullPageCard' | 'MobileDisplayCard';
|
||||||
|
pbEntityKey: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IdCardViewer: React.FC<IdCardViewerProps> = ({ cardLaout, pbEntityKey }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||||
|
const [apiUrl, setapiUrl] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/backendApiUrl")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => setapiUrl(data.apiUrl));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFetchPdf = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setPdfUrl(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://${apiUrl}/api/v1/members/${pbEntityKey}/id_card/${cardLaout}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch PDF");
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
setPdfUrl(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching PDF:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (pdfUrl) {
|
||||||
|
URL.revokeObjectURL(pdfUrl);
|
||||||
|
}
|
||||||
|
setPdfUrl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="2xl:min-w-[16vw] xl:min-w-[15vw] w-full h-full flex lg:flex-col justify-center items-center text-xl space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl">
|
||||||
|
<div className="flex flex-col justify-end items-center mb-4">
|
||||||
|
<div className="flex items-end mb-3">
|
||||||
|
<div className="text-platinum font-semibold">View Temporary ID Card</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<div className="text-deepcove -mb-11 font-semibold z-1">My Card</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleFetchPdf}
|
||||||
|
disabled={loading}
|
||||||
|
className="focus:outline-none"
|
||||||
|
>
|
||||||
|
<FaRegIdCard size={150} className="text-bluemana hover:text-light-bronze" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="flex flex-col items-center justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={handleFetchPdf}
|
||||||
|
disabled={loading}
|
||||||
|
className="font-semibold text-platinum underline hover:text-light-bronze focus:outline-none"
|
||||||
|
>
|
||||||
|
View ID Card
|
||||||
|
</Button> */}
|
||||||
|
|
||||||
|
{(loading || pdfUrl) && (
|
||||||
|
<div className="fixed inset-x-1/6 inset-y-1/12 flex items-center justify-center w-2/3 h-[80vh] bg-atmosphere border-2 border-platinum shadow-md rounded-lg z-2">
|
||||||
|
{loading && (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
{/* Spinner */}
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-20 w-20 text-deepcove mb-2"
|
||||||
|
xmlns="http://w3.org"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-45" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<p className="text-deepcove text-lg font-medium">Generating ID Card...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pdfUrl && (
|
||||||
|
<div className="flex flex-col w-full h-full bg-platinum rounded-lg shadow-xl overflow-hidden flex flex-col">
|
||||||
|
|
||||||
|
{/* Top Bar with Close Button */}
|
||||||
|
<div className="w-full h-14 bg-atmosphere text-deepcove flex justify-between items-center px-6">
|
||||||
|
<span className="font-bold text-sm truncate">ID Card Preview</span>
|
||||||
|
<Button className="px-3 py-2 bg-bluetang text-platinum text-sm font-semibold rounded hover:bg-atmosphere hover:text-bluetang border-2 border-bluetang" onClick={handleClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
{/* <button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="px-3 py-2 bg-bluetang text-platinum text-sm font-semibold rounded hover:bg-atmosphere hover:text-bluetang border-2 border-bluetang"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
src={pdfUrl}
|
||||||
|
className="w-full h-full"
|
||||||
|
title="ID Card"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loading Animation */}
|
||||||
|
{/* {loading && (
|
||||||
|
<div className="flex h-[70vh] w-screen items-center gap-2 text-gray-500 animate-pulse z-2">
|
||||||
|
<svg className="animate-spin h-10 w-10 text-blue-600" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
Fetching ID Card, Please Wait...
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{/* Display PDF */}
|
||||||
|
{/* {pdfUrl && (
|
||||||
|
<div className="w-screen h-[70vh] border border-gray-300 rounded shadow-lg overflow-hidden z-2">
|
||||||
|
<iframe
|
||||||
|
src={pdfUrl}
|
||||||
|
title="PDF Document"
|
||||||
|
className="w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IdCardViewer;
|
||||||
@@ -41,14 +41,14 @@ const InfoCard: React.FC<InfoCardProps> = (
|
|||||||
<div className='flex m-h-150 justify-center py-10'>
|
<div className='flex m-h-150 justify-center py-10'>
|
||||||
<div className='flex flex-col w-11/12'>
|
<div className='flex flex-col w-11/12'>
|
||||||
<div className='flex flex-col w-full items-center'>
|
<div className='flex flex-col w-full items-center'>
|
||||||
<div className={classnames('flex rounded-2xl shadow-copper/90',
|
<div className={classnames('flex w-full rounded-2xl shadow-platinum/50',
|
||||||
{
|
{
|
||||||
"flex-row-reverse relative" : invert,
|
"flex-row-reverse relative" : invert,
|
||||||
"shadow-[8px_8px_6px,2px_2px_2px_-1px]" : !invert
|
"shadow-[8px_8px_6px,2px_2px_2px_-1px]" : !invert
|
||||||
})}>
|
})}>
|
||||||
<div className={classnames({
|
<div className={classnames({
|
||||||
"hidden" : !invert,
|
"hidden" : !invert,
|
||||||
"after:absolute after:bottom-0 after:right-0 after:w-full after:h-[calc(100%-(var(--spacing)*23.5))] after:rounded-2xl after:shadow-[8px_8px_6px,2px_2px_2px_-1px] after:shadow-copper/90" : invert,
|
"after:absolute after:bottom-0 after:right-0 after:w-full after:h-[calc(100%-(var(--spacing)*23.5))] after:rounded-2xl after:shadow-[8px_8px_6px,2px_2px_2px_-1px] after:shadow-platinum/50" : invert,
|
||||||
})}/>
|
})}/>
|
||||||
<div className={classnames("flex flex-col w-7/12")}>
|
<div className={classnames("flex flex-col w-7/12")}>
|
||||||
<div className={classnames(
|
<div className={classnames(
|
||||||
@@ -61,7 +61,7 @@ const InfoCard: React.FC<InfoCardProps> = (
|
|||||||
"relative w-full pt-8 mb-2 ml-8 mr-100 border-b-3",
|
"relative w-full pt-8 mb-2 ml-8 mr-100 border-b-3",
|
||||||
{
|
{
|
||||||
"border-atmosphere" : !invert,
|
"border-atmosphere" : !invert,
|
||||||
"border-bronze" : invert
|
"border-platinum" : invert
|
||||||
}
|
}
|
||||||
)}>
|
)}>
|
||||||
<div className={classnames(
|
<div className={classnames(
|
||||||
@@ -105,7 +105,7 @@ const InfoCard: React.FC<InfoCardProps> = (
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classnames(
|
<div className={classnames(
|
||||||
"w-2/9 relative flex justify-center pb-4 text-center rounded-b-3xl rounded-out-t-2xl shadow-[8px_8px_6px,2px_2px_2px_-1px] shadow-copper/90",
|
"w-2/9 relative flex justify-center pb-4 text-center rounded-b-3xl rounded-out-t-2xl shadow-[8px_8px_6px,2px_2px_2px_-1px] shadow-platinum/50",
|
||||||
cardBGColor,
|
cardBGColor,
|
||||||
{
|
{
|
||||||
"ml-[calc(2/9*100%)]" : !invert,
|
"ml-[calc(2/9*100%)]" : !invert,
|
||||||
@@ -114,7 +114,7 @@ const InfoCard: React.FC<InfoCardProps> = (
|
|||||||
)}>
|
)}>
|
||||||
<Link href={learnMoreUrl ? learnMoreUrl : '/'}
|
<Link href={learnMoreUrl ? learnMoreUrl : '/'}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"py-1 px-4 rounded text-xl text-semibold border-2 text-bronze hover:text-copper border-bronze hover:border-copper",
|
"py-1 px-4 rounded text-xl text-semibold border-2 hover:text-platinum text-bluemana hover:border-platinum border-bluemana",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Learn More
|
Learn More
|
||||||
|
|||||||
@@ -4,29 +4,67 @@ import classnames from 'classnames';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { FaRegIdCard } from "react-icons/fa";
|
import { FaRegIdCard } from "react-icons/fa";
|
||||||
|
|
||||||
|
import IdCardViewer from '@/components/IdCardViewer';
|
||||||
|
|
||||||
import { Schibsted_Grotesk } from 'next/font/google'
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
import IdCardDownloader from './IdCardDownloader';
|
||||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
const InsuranceCardWidget = () => {
|
interface InsuranceCardWidgettProps {
|
||||||
|
pbEntityKey?: number;
|
||||||
|
plPlanKey?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsuranceCardWidget: React.FC<InsuranceCardWidgettProps> = ({ pbEntityKey, plPlanKey }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="2xl:min-w-[16vw] xl:min-w-[15vw] w-full h-full flex lg:flex-col justify-center items-center text-xl space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl z-1">
|
<div className="2xl:min-w-[16vw] xl:min-w-[15vw] w-full h-full flex lg:flex-col justify-center items-center text-md space-x-10 lg:space-x-0 border border-6 border-atmosphere rounded-4xl">
|
||||||
<div className="flex flex-col justify-end items-center">
|
<div className="flex flex-col justify-end items-center">
|
||||||
<div className="z-3 flex items-end">
|
<div className="flex items-end">
|
||||||
<div className="text-deepcove -mb-11 font-semibold">My Card</div>
|
<div className="text-deepcove -mb-11 font-semibold z-1">My Card</div>
|
||||||
</div>
|
</div>
|
||||||
<FaRegIdCard size={150} className="text-bluemana" />
|
<FaRegIdCard size={150} className="text-bluemana" />
|
||||||
</div>
|
</div>
|
||||||
<div className={classnames("flex justify-between items-center space-x-1", font.className)}>
|
<div className={classnames("flex w-full items-center space-x-1", font.className)}>
|
||||||
<Button className="hover:text-bronze">
|
{/* <Button className="hover:text-bronze">
|
||||||
View
|
View
|
||||||
</Button>
|
</Button> */}
|
||||||
<p>|</p>
|
{/* <div className={classnames("flex w-[45%] justify-end")}>
|
||||||
<Button className="hover:text-bronze">
|
<IdCardViewer
|
||||||
|
pbEntityKey={pbEntityKey}
|
||||||
|
cardLaout="FullPageCard"
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
{/* <div className={classnames("flex w-[10%] justify-center")}>
|
||||||
|
|
|
||||||
|
</div> */}
|
||||||
|
{/* <p>|</p> */}
|
||||||
|
{/* <div className={classnames("flex w-[45%] justify-start")}>
|
||||||
|
<IdCardViewer
|
||||||
|
pbEntityKey={pbEntityKey}
|
||||||
|
cardLaout="MobileDisplayCard"
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
{/* <Button className="hover:text-bronze">
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{pbEntityKey &&
|
||||||
|
<div className={classnames("flex w-[45%] justify-center")}>
|
||||||
|
<IdCardViewer
|
||||||
|
pbEntityKey={pbEntityKey}
|
||||||
|
cardLaout="FullPageCard"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{plPlanKey &&
|
||||||
|
<div className={classnames("flex w-full justify-center mb-4")}>
|
||||||
|
<IdCardDownloader
|
||||||
|
plPlanKey={plPlanKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -145,11 +145,11 @@ const Navbar: React.FC<NavbarProps> = ({ collapseHeight }) => {
|
|||||||
Brokers
|
Brokers
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{/* <li>
|
||||||
<Link href="/" className="hover:text-bluemana">
|
<Link href="/" className="hover:text-bluemana">
|
||||||
Services
|
Services
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li> */}
|
||||||
</ul>
|
</ul>
|
||||||
{/* <div className={classnames("flex space-x-4 font-semibold text-lg", {"pl-2" : isScrolled})}>
|
{/* <div className={classnames("flex space-x-4 font-semibold text-lg", {"pl-2" : isScrolled})}>
|
||||||
{menuItems.map((item) => {
|
{menuItems.map((item) => {
|
||||||
@@ -164,10 +164,15 @@ const Navbar: React.FC<NavbarProps> = ({ collapseHeight }) => {
|
|||||||
})}
|
})}
|
||||||
</div> */}
|
</div> */}
|
||||||
<div className={classnames("flex space-x-2 font-semibold", {"pt-1" : isScrolled})}>
|
<div className={classnames("flex space-x-2 font-semibold", {"pt-1" : isScrolled})}>
|
||||||
<Link href="/dashboard/member">
|
{/* <Link href="/account/signup">
|
||||||
<button className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
|
<Button className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
|
||||||
|
Sign Up
|
||||||
|
</Button>
|
||||||
|
</Link> */}
|
||||||
|
<Link href="/account/signin">
|
||||||
|
<Button className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
|
||||||
Sign In
|
Sign In
|
||||||
</button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{/* <Button href="" className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
|
{/* <Button href="" className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
|
||||||
Sign In
|
Sign In
|
||||||
|
|||||||
@@ -4,88 +4,75 @@ import classnames from 'classnames';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
import { Schibsted_Grotesk } from 'next/font/google'
|
import { Schibsted_Grotesk } from 'next/font/google'
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||||
|
|
||||||
interface RecentClaimsWidgetProps {
|
interface RecentClaimsWidgetProps {
|
||||||
|
recentClaims: RecentClaimType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecentClaimsWidget = () => {
|
interface RecentClaimType {
|
||||||
|
claim_number: number;
|
||||||
|
visit_date: string;
|
||||||
|
claim_status: string;
|
||||||
|
patient_name: string;
|
||||||
|
provider_name: string;
|
||||||
|
}
|
||||||
|
const RecentClaimsWidget: React.FC<RecentClaimsWidgetProps> = ({ recentClaims }) => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="2xl:min-w-[48vw] xl:min-w-[45vw] flex z-1">
|
<div className="2xl:min-w-[48vw] xl:min-w-[50vw] flex z-1">
|
||||||
<div className="w-full flex flex-col px-4 py-3 border border-6 border-bronze rounded-4xl space-y-2">
|
<div className="w-full flex flex-col px-4 py-3 text-platinum border border-6 border-bronze rounded-4xl space-y-2">
|
||||||
<div className="w-full flex justify-center text-2xl text-atmosphere font-semibold">
|
<div className="w-full flex justify-center text-2xl text-atmosphere font-semibold">
|
||||||
<p>Recent Claims</p>
|
<p>Recent Claims</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={classnames("w-full flex flex-col px-2 text-lg", font.className)}>
|
<div className={classnames("w-full flex flex-col px-2 text-sm", font.className)}>
|
||||||
<div className="flex">
|
<div className='flex w-full mb-4 border-b border-bluemana'>
|
||||||
<div className="w-1/6">
|
<div className="flex w-2/20">
|
||||||
<Button className="hover:text-bronze text-bluemana">
|
Claim #
|
||||||
2907457
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/6">
|
<div className="flex w-4/20 pl-3">
|
||||||
LA'BLAH, ROBERT
|
Patient Name
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/6">
|
<div className="flex w-6/20 pl-3">
|
||||||
HOUSE, GREGORY
|
Provider
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/6">
|
<div className="flex w-3/20 pl-3">
|
||||||
07/21/2025
|
Visit Date
|
||||||
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-8.5">
|
||||||
|
Claim Status
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
{(recentClaims ?? []).map((mc) => (
|
||||||
<div className="w-1/6">
|
<div key={mc.claim_number} className="flex w-full">
|
||||||
<Button className="hover:text-bronze text-bluemana">
|
<div className="flex w-2/20">
|
||||||
2907201
|
{mc.claim_number}
|
||||||
</Button>
|
</div>
|
||||||
|
<div className="flex w-4/20 pl-3">
|
||||||
|
{mc.patient_name}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-6/20 pl-3">
|
||||||
|
{mc.provider_name}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-3/20 pl-3">
|
||||||
|
{mc.visit_date}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end w-4/20">
|
||||||
|
{mc.claim_status}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/6">
|
))}
|
||||||
LA'BLAH, ROBERT
|
|
||||||
</div>
|
|
||||||
<div className="w-2/6">
|
|
||||||
QUINN, MICHAELA
|
|
||||||
</div>
|
|
||||||
<div className="w-1/6">
|
|
||||||
07/02/2025
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="w-1/6">
|
|
||||||
<Button className="hover:text-bronze text-bluemana">
|
|
||||||
2905891
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="w-2/6">
|
|
||||||
LA'BLAH, JENNY
|
|
||||||
</div>
|
|
||||||
<div className="w-2/6">
|
|
||||||
HOWSER, DOGGIE
|
|
||||||
</div>
|
|
||||||
<div className="w-1/6">
|
|
||||||
05/15/2025
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<div className="w-1/6">
|
|
||||||
<Button className="hover:text-bronze text-bluemana">
|
|
||||||
2905547
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="w-2/6">
|
|
||||||
LA'BLAH, ROBERT
|
|
||||||
</div>
|
|
||||||
<div className="w-2/6">
|
|
||||||
HOUSE, GREGORY
|
|
||||||
</div>
|
|
||||||
<div className="w-1/6">
|
|
||||||
04/29/2025
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex justify-end text-platinum font-semibold">
|
<div className="w-full flex justify-end text-platinum font-semibold">
|
||||||
<Button className="mr-2 underline hover:text-bronze">
|
<Button className="mr-2 underline hover:text-light-bronze">
|
||||||
View All Claims
|
<Link
|
||||||
|
href="/dashboard/member/claims"
|
||||||
|
>
|
||||||
|
View All Claims
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { TbCircleArrowLeft, TbCircleArrowRight } from "react-icons/tb";
|
|||||||
import { PiBreadFill } from "react-icons/pi";
|
import { PiBreadFill } from "react-icons/pi";
|
||||||
|
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
|
||||||
interface SideNavProps {
|
interface SideNavProps {
|
||||||
@@ -39,18 +40,18 @@ const SideNav: React.FC<SideNavProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
)}>
|
)}>
|
||||||
<div className="h-25" />
|
<div className="h-20" />
|
||||||
<Button
|
{/* <Button
|
||||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||||
className="h-10 flex justify-end items-center pr-2"
|
className="h-10 flex justify-end items-center pr-2"
|
||||||
>
|
>
|
||||||
{isSidebarOpen ? <FaArrowCircleLeft size={30} /> : <FaArrowCircleRight size={30} />}
|
{isSidebarOpen ? <FaArrowCircleLeft size={30} /> : <FaArrowCircleRight size={30} />}
|
||||||
</Button>
|
</Button> */}
|
||||||
<div className="flex flex-col divide-y divide-bluetang">
|
<div className="flex flex-col divide-y divide-bluetang">
|
||||||
{navItems.map((navItem, index) => (
|
{navItems.map((navItem, index) => (
|
||||||
<a href={navItem.url} className={classnames(
|
<a href={navItem.url} key={index} className={classnames(
|
||||||
"w-full h-20 flex justify-center items-center px-1 text-xs md:text-sm lg:text-base font-semibold hover:text-platinum",
|
"w-full h-20 flex justify-center items-center px-1 text-xs md:text-sm lg:text-base font-semibold hover:text-platinum",
|
||||||
{"bg-bluetang text-platinum" : index == 0}
|
{"bg-bluetang text-platinum" : usePathname() == navItem.url}
|
||||||
)}>
|
)}>
|
||||||
{isSidebarOpen ? (
|
{isSidebarOpen ? (
|
||||||
<>{navItem.title}</>
|
<>{navItem.title}</>
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useIdleTimer } from "react-idle-timer";
|
||||||
|
import { signOut, useSession } from "next-auth/react"; // Or use your custom auth hook
|
||||||
|
|
||||||
|
const TIMEOUT_MINUTES = 15; // Log out after 15 minutes of inactivity
|
||||||
|
const WARNING_SECONDS = 60; // Show warning 1 minute before logout
|
||||||
|
|
||||||
|
export default function IdleTimerContainer() {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const [showWarning, setShowWarning] = useState(false);
|
||||||
|
const [remainingTime, setRemainingTime] = useState(0);
|
||||||
|
|
||||||
|
const idleTimerRef = useRef(null);
|
||||||
|
|
||||||
|
// Triggered when user becomes idle but before logging out (for a warning modal)
|
||||||
|
const onPrompt = () => {
|
||||||
|
// getRemainingTime is provided by react-idle-timer
|
||||||
|
const time = Math.floor(idleTimerRef.current.getRemainingTime() / 1000);
|
||||||
|
setRemainingTime(time);
|
||||||
|
setShowWarning(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Triggered when the user is completely idle and timer runs out
|
||||||
|
const onIdle = () => {
|
||||||
|
setShowWarning(false);
|
||||||
|
signOut({ callbackUrl: "/login" }); // Redirects to login after logging out
|
||||||
|
};
|
||||||
|
|
||||||
|
const onActive = () => {
|
||||||
|
setShowWarning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getRemainingTime, getLastActiveTime } = useIdleTimer({
|
||||||
|
timeout: 1000 * 60 * TIMEOUT_MINUTES,
|
||||||
|
promptTimeout: 1000 * 60 * WARNING_SECONDS,
|
||||||
|
onPrompt,
|
||||||
|
onIdle,
|
||||||
|
onActive,
|
||||||
|
debounce: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval;
|
||||||
|
if (showWarning) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setRemainingTime((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [showWarning]);
|
||||||
|
|
||||||
|
// Don't render anything if the user isn't logged in or no warning is active
|
||||||
|
if (!session || !showWarning) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-xl text-center max-w-sm w-full mx-4">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800">Session Timeout Warning</h2>
|
||||||
|
<p className="mt-2 text-sm text-gray-600">
|
||||||
|
You will be automatically logged out in <b>{remainingTime} seconds</b> due to inactivity.
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
Click anywhere or press any key to stay logged in.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => idleTimerRef.current?.activate()}
|
||||||
|
className="mt-4 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 w-full"
|
||||||
|
>
|
||||||
|
Stay Logged In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,38 +13,38 @@ export const sideNavItems: sideNavGroups = {
|
|||||||
"member": [
|
"member": [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
url: "/",
|
url: "/dashboard/member",
|
||||||
iconName: "LuLayoutDashboard"
|
iconName: "LuLayoutDashboard"
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// title: "My Plan & Benefits",
|
||||||
|
// url: "/",
|
||||||
|
// iconName: "LuClipboardList"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// title: "Plan Utilization",
|
||||||
|
// url: "/",
|
||||||
|
// iconName: "TbProgress"
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
title: "My Plan & Benefits",
|
title: "Claims",
|
||||||
url: "/",
|
url: "/dashboard/member/claims",
|
||||||
iconName: "LuClipboardList"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Plan Utilization",
|
|
||||||
url: "/",
|
|
||||||
iconName: "TbProgress"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Claims & Authorizations",
|
|
||||||
url: "/",
|
|
||||||
iconName: "BsClipboardCheck"
|
iconName: "BsClipboardCheck"
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: "Providers & Services",
|
// title: "Providers & Services",
|
||||||
url: "/",
|
// url: "/",
|
||||||
iconName: "FaStethoscope"
|
// iconName: "FaStethoscope"
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: "Insurance Card",
|
// title: "Insurance Card",
|
||||||
url: "/",
|
// url: "/",
|
||||||
iconName: "FaRegIdCard"
|
// iconName: "FaRegIdCard"
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: "Benefits Documents",
|
// title: "Benefits Documents",
|
||||||
url: "/",
|
// url: "/",
|
||||||
iconName: "IoDocumentsOutline"
|
// iconName: "IoDocumentsOutline"
|
||||||
}
|
// }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,12 @@
|
|||||||
--color-copper: #B06E30;
|
--color-copper: #B06E30;
|
||||||
/* bronze */
|
/* bronze */
|
||||||
--color-bronze: #D38F4A;
|
--color-bronze: #D38F4A;
|
||||||
|
/* bronze */
|
||||||
|
--color-light-bronze: #CD7F32;
|
||||||
|
/* cobalt blue */
|
||||||
|
--color-cobalt: #0047AB;
|
||||||
|
/* oxidized copper/bronze green-blue */
|
||||||
|
--color-verdigris: #618D94;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -19,9 +23,18 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||