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 ./
|
||||
|
||||
ARG NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
|
||||
ENV NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=$NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install --legacy-peer-deps
|
||||
RUN npm install
|
||||
|
||||
# RUN build
|
||||
|
||||
# Copy the rest of your application code
|
||||
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
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
- BE_API_SERVER_HOST=web_api
|
||||
- BE_API_SERVER_PORT=3005
|
||||
volumes:
|
||||
- .:/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
|
||||
networks:
|
||||
- britton_network
|
||||
|
||||
networks:
|
||||
britton_network:
|
||||
external: true
|
||||
@@ -1,7 +1,19 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
"@apollo/client": "^3.13.9",
|
||||
"@apollo/experimental-nextjs-app-support": "^0.12.3",
|
||||
"graphql": "^16.11.0",
|
||||
"jose": "^6.2.3",
|
||||
"next": "15.3.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-idle-timer": "^5.7.3",
|
||||
"react-markdown": "^10.1.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 className="w-full">
|
||||
<div className="w-full text-platinum">
|
||||
{children}
|
||||
</div>
|
||||
<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,24 +1,56 @@
|
||||
"user client"
|
||||
|
||||
import { sideNavItems } from '@/lib/dashboard';
|
||||
import { headers } from "next/headers";
|
||||
import Script from 'next/script';
|
||||
import { cookies } from "next/headers";
|
||||
import { JWTPayload, jwtVerify } from "jose";
|
||||
|
||||
import AccountDropdown from '../../components/AccountDropdown';
|
||||
import SideNav from "@/components/SideNav";
|
||||
import Image from 'next/image';
|
||||
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 }) {
|
||||
const headersList = await headers();
|
||||
const pathname = headersList.get("x-current-path");
|
||||
const dashboardType = pathname?.split('/').pop() || "";
|
||||
const pageNavItems = sideNavItems[dashboardType];
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get("session"); // Replace with your cookie name
|
||||
|
||||
if (!sessionCookie) {
|
||||
return <div className='flex w-full justify-center'>No active session. Please log in.</div>;
|
||||
}
|
||||
|
||||
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 userRole = data.user.role
|
||||
|
||||
// const headersList = await headers();
|
||||
// const pathname = headersList.get("x-current-path");
|
||||
// const dashboardType = pathname?.split('/').pop() || "";
|
||||
const pageNavItems = sideNavItems[userRole];
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<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">
|
||||
<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" />
|
||||
@@ -40,7 +72,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
<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">
|
||||
<div className="-ml-4.5 z-2">
|
||||
<Image
|
||||
src="/images/bbstpa-forsite.png"
|
||||
alt="Britton Logo"
|
||||
@@ -60,4 +92,9 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
||||
</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 DashboardHeader from "@/components/DashboardHeader";
|
||||
import FindProvider from '@/components/FindProvider';
|
||||
import FindProviderWidget from '@/components/FindProvider';
|
||||
import InsuranceCardWidget from '@/components/InsuranceCard';
|
||||
import DeductableProgressWidget from '@/components/DeductableProgressWidget';
|
||||
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()
|
||||
|
||||
export default function Member() {
|
||||
return (
|
||||
<>
|
||||
{dashboardData &&
|
||||
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
|
||||
<DashboardHeader
|
||||
userDisplayName="Robert La'Blah"
|
||||
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 space-y-4">
|
||||
<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 />
|
||||
<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>
|
||||
<FindProvider />
|
||||
<RecentClaimsWidget />
|
||||
<FindRxInformationWidget />
|
||||
<RecentClaimsWidget
|
||||
recentClaims={dashboardData.recent_claims}
|
||||
/>
|
||||
</div>
|
||||
<BenefitsAAGWidget />
|
||||
<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>;
|
||||
// }
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<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]">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -4,33 +4,11 @@ import InlineLink from "../components/InlineLink";
|
||||
import Navbar from '@/components/Navbar';
|
||||
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() {
|
||||
const { data } = await getClient().query({
|
||||
query: HOME_GET_DATA_QUERY,
|
||||
});
|
||||
// const { data } = await getClient().query({
|
||||
// query: HOME_GET_DATA_QUERY,
|
||||
// });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -41,40 +19,42 @@ export default async function Home() {
|
||||
textTop="Healthcare that's"
|
||||
textBottom="truly affordable"
|
||||
/>
|
||||
<div className="pt-4">
|
||||
<div className="pt-10">
|
||||
<InfoCard
|
||||
title={data.homePage.section1Header}
|
||||
learnMoreUrl={data.homePage.section1Cta}
|
||||
imageUrl="/images/members-stock.jpg"
|
||||
title="Members"
|
||||
learnMoreUrl="/members"
|
||||
imageUrl="/images/member-image.jpg"
|
||||
>
|
||||
{data.homePage.section1Body}
|
||||
What we do for members
|
||||
</InfoCard>
|
||||
</div>
|
||||
<div className='bg-atmosphere rounded-3xl'>
|
||||
<InfoCard
|
||||
title={data.homePage.section2Header}
|
||||
learnMoreUrl={data.homePage.section2Cta}
|
||||
imageUrl='/images/employers-stock.jpg'
|
||||
title="Employers"
|
||||
learnMoreUrl="/employers"
|
||||
imageUrl='/images/employer-image.jpg'
|
||||
invert
|
||||
>
|
||||
{data.homePage.section2Body}
|
||||
What we do for employers
|
||||
</InfoCard>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<InfoCard
|
||||
title={data.homePage.section3Header}
|
||||
learnMoreUrl={data.homePage.section3Cta}
|
||||
imageUrl="/images/providers-stock.jpg"
|
||||
title="Providers"
|
||||
learnMoreUrl="/providers"
|
||||
imageUrl="/images/provider-image.png"
|
||||
>
|
||||
{data.homePage.section3Body}
|
||||
What we do for providers
|
||||
</InfoCard>
|
||||
<div className='bg-platinum rounded-3xl'>
|
||||
</div>
|
||||
<div className='bg-light-bronze rounded-3xl'>
|
||||
<InfoCard
|
||||
title={data.homePage.section4Header}
|
||||
learnMoreUrl={data.homePage.section4Cta}
|
||||
imageUrl="/images/brokers-stock.jpg"
|
||||
title="Brokers"
|
||||
learnMoreUrl="/brokers"
|
||||
imageUrl="/images/broker-image.jpg"
|
||||
invert
|
||||
>
|
||||
{data.homePage.section4Body}
|
||||
What we do for brokers
|
||||
</InfoCard>
|
||||
</div>
|
||||
<Footer />
|
||||
|
||||
@@ -28,8 +28,8 @@ const AccountDropdown: React.FC<DropdownProps> = ({userRole}) => {
|
||||
</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"}>
|
||||
<MenuItem as="div" className={"px-3 py-1.5"}>
|
||||
<a className="block data-focus:text-bluemana hover:text-bluemana" href="/">
|
||||
Change Password
|
||||
<a className="block data-focus:text-bluemana hover:text-bluemana" href="/dashboard/member">
|
||||
Dashboard
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem as="div" className={"px-3 py-1.5"}>
|
||||
|
||||
@@ -3,51 +3,43 @@
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
import { useEffect, useState } from 'react';
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
|
||||
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 (
|
||||
<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={classnames("text-2xl font-semibold py-2", font.className)}>CLASSIC 5K 100/5000</p>
|
||||
<div className={classnames("w-full flex justify-between", font.className)}>
|
||||
<div className="flex flex-col">
|
||||
<p>Physician Visit</p>
|
||||
<p>Specialist Visit</p>
|
||||
<p>Urgent Care</p>
|
||||
<p>INN-Ind Ded</p>
|
||||
<p>INN-Family Ded</p>
|
||||
<p>OON-Ind Ded</p>
|
||||
<p>OON-Family Ded</p>
|
||||
<p>Co-Insurance</p>
|
||||
<p>INN-Ind OOP</p>
|
||||
<p>INN-Family OOP</p>
|
||||
<p>OON-Ind OOP</p>
|
||||
<p>OON-Family OOP</p>
|
||||
<p>Emergency Room</p>
|
||||
<p>Preventive Care</p>
|
||||
<p className={classnames("text-2xl font-semibold py-2", font.className)}>{planTitle}</p>
|
||||
<div className={classnames("w-full flex-col items-between px-4", font.className)}>
|
||||
{(planBenefits).map((mb) => (
|
||||
<div key={mb.sequence} className="flex items-center w-full">
|
||||
<div className="flex-none">
|
||||
{mb.benefit_desc}
|
||||
</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 className="flex-grow h-[1px] mx-1 mt-1 bg-verdigris"></div>
|
||||
<div className="flex-none justify-end">
|
||||
{mb.benefit}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,16 +5,18 @@ import classnames from 'classnames';
|
||||
|
||||
interface ButtonProps {
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({ onClick, children, className }) => {
|
||||
const Button: React.FC<ButtonProps> = ({ onClick, disabled, children, className }) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={classnames(
|
||||
className, {
|
||||
'cursor-pointer': isHovered,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import classnames from 'classnames';
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
|
||||
interface DashboardHeaderProps {
|
||||
userDisplayName: string;
|
||||
@@ -9,7 +12,8 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({ userDisplayName }) =>
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-start">
|
||||
<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}
|
||||
</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";
|
||||
|
||||
import classnames from 'classnames';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
import { MdLocationOn } from "react-icons/md";
|
||||
import LocationSearchInput from '@/components/LocationSearchInput';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
|
||||
interface FindProviderProps {
|
||||
zipOnly?: boolean;
|
||||
import Link from 'next/link';
|
||||
|
||||
interface FindProviderWidgetProps {
|
||||
networkProvider: string;
|
||||
}
|
||||
|
||||
const FindProvider: React.FC<FindProviderProps> = ({ zipOnly }) => {
|
||||
interface NetworkProvider {
|
||||
network_provider: string;
|
||||
}
|
||||
|
||||
const FindProviderWidget: React.FC<FindProviderWidgetProps> = ({ networkProvider }) => {
|
||||
|
||||
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 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">
|
||||
{networkProvider &&
|
||||
<div className="flex items-end mb-3">
|
||||
<div className="text-platinum font-semibold">Find A {networkProvider} Provider</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">
|
||||
<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} />
|
||||
}
|
||||
{networkProvider == "Cigna" &&
|
||||
<Link href="http://cigna.com" target="_blank" rel="noopener noreferrer">
|
||||
<Button className="flex items-center justify-center bg-cobalt rounded hover:bg-copper rounded-xl">
|
||||
<Image
|
||||
src="/images/Cigna_deepcove.svg"
|
||||
alt="Cigna Logo"
|
||||
width={150}
|
||||
height={150}
|
||||
priority
|
||||
/>
|
||||
</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
|
||||
</Link>
|
||||
}
|
||||
{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>
|
||||
</Link>
|
||||
}
|
||||
</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>
|
||||
</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 flex-col w-11/12'>
|
||||
<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,
|
||||
"shadow-[8px_8px_6px,2px_2px_2px_-1px]" : !invert
|
||||
})}>
|
||||
<div className={classnames({
|
||||
"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(
|
||||
@@ -61,7 +61,7 @@ const InfoCard: React.FC<InfoCardProps> = (
|
||||
"relative w-full pt-8 mb-2 ml-8 mr-100 border-b-3",
|
||||
{
|
||||
"border-atmosphere" : !invert,
|
||||
"border-bronze" : invert
|
||||
"border-platinum" : invert
|
||||
}
|
||||
)}>
|
||||
<div className={classnames(
|
||||
@@ -105,7 +105,7 @@ const InfoCard: React.FC<InfoCardProps> = (
|
||||
</div>
|
||||
</div>
|
||||
<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,
|
||||
{
|
||||
"ml-[calc(2/9*100%)]" : !invert,
|
||||
@@ -114,7 +114,7 @@ const InfoCard: React.FC<InfoCardProps> = (
|
||||
)}>
|
||||
<Link href={learnMoreUrl ? learnMoreUrl : '/'}
|
||||
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
|
||||
|
||||
@@ -4,29 +4,67 @@ import classnames from 'classnames';
|
||||
import Button from '@/components/Button';
|
||||
import { FaRegIdCard } from "react-icons/fa";
|
||||
|
||||
import IdCardViewer from '@/components/IdCardViewer';
|
||||
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
import IdCardDownloader from './IdCardDownloader';
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
|
||||
const InsuranceCardWidget = () => {
|
||||
interface InsuranceCardWidgettProps {
|
||||
pbEntityKey?: number;
|
||||
plPlanKey?: number;
|
||||
}
|
||||
|
||||
const InsuranceCardWidget: React.FC<InsuranceCardWidgettProps> = ({ pbEntityKey, plPlanKey }) => {
|
||||
|
||||
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="z-3 flex items-end">
|
||||
<div className="text-deepcove -mb-11 font-semibold">My Card</div>
|
||||
<div className="flex items-end">
|
||||
<div className="text-deepcove -mb-11 font-semibold z-1">My Card</div>
|
||||
</div>
|
||||
<FaRegIdCard size={150} className="text-bluemana" />
|
||||
</div>
|
||||
<div className={classnames("flex justify-between items-center space-x-1", font.className)}>
|
||||
<Button className="hover:text-bronze">
|
||||
<div className={classnames("flex w-full items-center space-x-1", font.className)}>
|
||||
{/* <Button className="hover:text-bronze">
|
||||
View
|
||||
</Button>
|
||||
<p>|</p>
|
||||
<Button className="hover:text-bronze">
|
||||
</Button> */}
|
||||
{/* <div className={classnames("flex w-[45%] justify-end")}>
|
||||
<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
|
||||
</Button>
|
||||
</Button> */}
|
||||
</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
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
{/* <li>
|
||||
<Link href="/" className="hover:text-bluemana">
|
||||
Services
|
||||
</Link>
|
||||
</li>
|
||||
</li> */}
|
||||
</ul>
|
||||
{/* <div className={classnames("flex space-x-4 font-semibold text-lg", {"pl-2" : isScrolled})}>
|
||||
{menuItems.map((item) => {
|
||||
@@ -164,10 +164,15 @@ const Navbar: React.FC<NavbarProps> = ({ collapseHeight }) => {
|
||||
})}
|
||||
</div> */}
|
||||
<div className={classnames("flex space-x-2 font-semibold", {"pt-1" : isScrolled})}>
|
||||
<Link href="/dashboard/member">
|
||||
<button className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
|
||||
{/* <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">
|
||||
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
|
||||
</button>
|
||||
</Button>
|
||||
</Link>
|
||||
{/* <Button href="" className="bg-platinum text-copper hover:text-bronze py-1 px-4 rounded border-1 border-copper hover:border-bronze">
|
||||
Sign In
|
||||
|
||||
@@ -4,88 +4,75 @@ import classnames from 'classnames';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
import { Schibsted_Grotesk } from 'next/font/google'
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
const font = Schibsted_Grotesk({ subsets: ['latin'] })
|
||||
|
||||
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 (
|
||||
<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-bronze rounded-4xl space-y-2">
|
||||
<div className="2xl:min-w-[48vw] xl:min-w-[50vw] flex z-1">
|
||||
<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">
|
||||
<p>Recent Claims</p>
|
||||
</div>
|
||||
<div className={classnames("w-full flex flex-col px-2 text-lg", font.className)}>
|
||||
<div className="flex">
|
||||
<div className="w-1/6">
|
||||
<Button className="hover:text-bronze text-bluemana">
|
||||
2907457
|
||||
</Button>
|
||||
<div className={classnames("w-full flex flex-col px-2 text-sm", font.className)}>
|
||||
<div className='flex w-full mb-4 border-b border-bluemana'>
|
||||
<div className="flex w-2/20">
|
||||
Claim #
|
||||
</div>
|
||||
<div className="w-2/6">
|
||||
LA'BLAH, ROBERT
|
||||
<div className="flex w-4/20 pl-3">
|
||||
Patient Name
|
||||
</div>
|
||||
<div className="w-2/6">
|
||||
HOUSE, GREGORY
|
||||
<div className="flex w-6/20 pl-3">
|
||||
Provider
|
||||
</div>
|
||||
<div className="w-1/6">
|
||||
07/21/2025
|
||||
<div className="flex w-3/20 pl-3">
|
||||
Visit Date
|
||||
</div>
|
||||
<div className="flex w-4/20 pl-8.5">
|
||||
Claim Status
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="w-1/6">
|
||||
<Button className="hover:text-bronze text-bluemana">
|
||||
2907201
|
||||
</Button>
|
||||
{(recentClaims ?? []).map((mc) => (
|
||||
<div key={mc.claim_number} className="flex w-full">
|
||||
<div className="flex w-2/20">
|
||||
{mc.claim_number}
|
||||
</div>
|
||||
<div className="w-2/6">
|
||||
LA'BLAH, ROBERT
|
||||
<div className="flex w-4/20 pl-3">
|
||||
{mc.patient_name}
|
||||
</div>
|
||||
<div className="w-2/6">
|
||||
QUINN, MICHAELA
|
||||
<div className="flex w-6/20 pl-3">
|
||||
{mc.provider_name}
|
||||
</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 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-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">
|
||||
<Link
|
||||
href="/dashboard/member/claims"
|
||||
>
|
||||
View All Claims
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TbCircleArrowLeft, TbCircleArrowRight } from "react-icons/tb";
|
||||
import { PiBreadFill } from "react-icons/pi";
|
||||
|
||||
import Button from './Button';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
|
||||
interface SideNavProps {
|
||||
@@ -39,18 +40,18 @@ const SideNav: React.FC<SideNavProps> = (
|
||||
}
|
||||
|
||||
)}>
|
||||
<div className="h-25" />
|
||||
<Button
|
||||
<div className="h-20" />
|
||||
{/* <Button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="h-10 flex justify-end items-center pr-2"
|
||||
>
|
||||
{isSidebarOpen ? <FaArrowCircleLeft size={30} /> : <FaArrowCircleRight size={30} />}
|
||||
</Button>
|
||||
</Button> */}
|
||||
<div className="flex flex-col divide-y divide-bluetang">
|
||||
{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",
|
||||
{"bg-bluetang text-platinum" : index == 0}
|
||||
{"bg-bluetang text-platinum" : usePathname() == navItem.url}
|
||||
)}>
|
||||
{isSidebarOpen ? (
|
||||
<>{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": [
|
||||
{
|
||||
title: "Dashboard",
|
||||
url: "/",
|
||||
url: "/dashboard/member",
|
||||
iconName: "LuLayoutDashboard"
|
||||
},
|
||||
// {
|
||||
// title: "My Plan & Benefits",
|
||||
// url: "/",
|
||||
// iconName: "LuClipboardList"
|
||||
// },
|
||||
// {
|
||||
// title: "Plan Utilization",
|
||||
// url: "/",
|
||||
// iconName: "TbProgress"
|
||||
// },
|
||||
{
|
||||
title: "My Plan & Benefits",
|
||||
url: "/",
|
||||
iconName: "LuClipboardList"
|
||||
},
|
||||
{
|
||||
title: "Plan Utilization",
|
||||
url: "/",
|
||||
iconName: "TbProgress"
|
||||
},
|
||||
{
|
||||
title: "Claims & Authorizations",
|
||||
url: "/",
|
||||
title: "Claims",
|
||||
url: "/dashboard/member/claims",
|
||||
iconName: "BsClipboardCheck"
|
||||
},
|
||||
{
|
||||
title: "Providers & Services",
|
||||
url: "/",
|
||||
iconName: "FaStethoscope"
|
||||
},
|
||||
{
|
||||
title: "Insurance Card",
|
||||
url: "/",
|
||||
iconName: "FaRegIdCard"
|
||||
},
|
||||
{
|
||||
title: "Benefits Documents",
|
||||
url: "/",
|
||||
iconName: "IoDocumentsOutline"
|
||||
}
|
||||
// {
|
||||
// title: "Providers & Services",
|
||||
// url: "/",
|
||||
// iconName: "FaStethoscope"
|
||||
// },
|
||||
// {
|
||||
// title: "Insurance Card",
|
||||
// url: "/",
|
||||
// iconName: "FaRegIdCard"
|
||||
// },
|
||||
// {
|
||||
// title: "Benefits Documents",
|
||||
// url: "/",
|
||||
// iconName: "IoDocumentsOutline"
|
||||
// }
|
||||
]
|
||||
}
|
||||
@@ -21,6 +21,12 @@
|
||||
--color-copper: #B06E30;
|
||||
/* bronze */
|
||||
--color-bronze: #D38F4A;
|
||||
/* bronze */
|
||||
--color-light-bronze: #CD7F32;
|
||||
/* cobalt blue */
|
||||
--color-cobalt: #0047AB;
|
||||
/* oxidized copper/bronze green-blue */
|
||||
--color-verdigris: #618D94;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -19,9 +23,18 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||