Its been a while

This commit is contained in:
Jason Jordan
2026-07-23 10:38:20 -04:00
parent 77f294c972
commit 8b3bb209b3
50 changed files with 4147 additions and 1452 deletions
+1 -1
View File
@@ -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 />
+54
View File
@@ -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>
)
}
+94
View File
@@ -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>
)
}
+188
View File
@@ -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>
)
}
+100
View File
@@ -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.' }
}
}
+91
View File
@@ -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.' }
}
}
+98
View File
@@ -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.' }
}
}
+18
View File
@@ -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 });
}
+148
View File
@@ -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');
}
}
}
+149
View File
@@ -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>;
// }
}
+82 -45
View File
@@ -1,63 +1,100 @@
"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
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" />
</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-copper border-atmosphere"
)} />
</div>
</div>
</div>
<div className="w-full h-full flex items-start">
<SideNav
maxWidth="w-[16vw]"
bgColor="bg-atmosphere"
navItems={pageNavItems}
/>
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
<div className="w-[90.15vw] h-20 flex justify-between items-center">
<div className={classnames('flex items-center')}>
<div className="-ml-8.5 z-2">
<Image
src="/images/bbstpa-forsite.png"
alt="Britton Logo"
width={205}
height={60}
/>
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">
<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" />
</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-copper border-atmosphere"
)} />
</div>
</div>
<div className="pr-20 z-2">
<AccountDropdown userRole="member" />
</div>
<div className="w-full h-full flex items-start">
<SideNav
maxWidth="w-[16vw]"
bgColor="bg-atmosphere"
navItems={pageNavItems}
/>
<div className="w-full min-w-[87vw] max-w-[94vw] lg:min-w-[89vw] lg:max-w-[95vw] flex flex-col items-end">
<div className="w-[90.15vw] h-20 flex justify-between items-center">
<div className={classnames('flex items-center')}>
<div className="-ml-4.5 z-2">
<Image
src="/images/bbstpa-forsite.png"
alt="Britton Logo"
width={205}
height={60}
/>
</div>
</div>
<div className="pr-20 z-2">
<AccountDropdown userRole="member" />
</div>
</div>
<div className="w-full h-full flex justify-start">
{children}
</div>
</div>
</div>
<div className="w-full h-full flex justify-start">
{children}
</div>
</div>
</div>
</div>
)
)
} catch (error) {
// Fails if token is expired, tampered with, or invalid
console.error("JWT verification failed:", error);
return <div className='flex w-full justify-center'>Session invalid or expired.</div>;
}
}
+76
View File
@@ -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>
)
}
+148
View File
@@ -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');
}
}
}
+137 -19
View File
@@ -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';
export default function Member() {
return (
<div className="flex flex-col w-full pl-8 pt-5 2xl:pt-7">
<DashboardHeader
userDisplayName="Robert La'Blah"
/>
<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 />
// 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>
<FindProvider />
<RecentClaimsWidget />
<BenefitsAAGWidget
planBenefits={dashboardData.plan_benefits}
/>
</div>
<BenefitsAAGWidget />
</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 -1
View File
@@ -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>
+26 -46
View File
@@ -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>
<InfoCard
title={data.homePage.section3Header}
learnMoreUrl={data.homePage.section3Cta}
imageUrl="/images/providers-stock.jpg"
>
{data.homePage.section3Body}
</InfoCard>
<div className='bg-platinum rounded-3xl'>
<div className="w-full">
<InfoCard
title={data.homePage.section4Header}
learnMoreUrl={data.homePage.section4Cta}
imageUrl="/images/brokers-stock.jpg"
title="Providers"
learnMoreUrl="/providers"
imageUrl="/images/provider-image.png"
>
What we do for providers
</InfoCard>
</div>
<div className='bg-light-bronze rounded-3xl'>
<InfoCard
title="Brokers"
learnMoreUrl="/brokers"
imageUrl="/images/broker-image.jpg"
invert
>
{data.homePage.section4Body}
What we do for brokers
</InfoCard>
</div>
<Footer />