-
-
+ if (!sessionCookie) {
+ return
No active session. Please log in.
;
+ }
+
+ 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 (
+
-
- )
+ )
+ } catch (error) {
+ // Fails if token is expired, tampered with, or invalid
+ console.error("JWT verification failed:", error);
+ return
Session invalid or expired.
;
+ }
}
\ No newline at end of file
diff --git a/src/app/dashboard/login/page.tsx b/src/app/dashboard/login/page.tsx
new file mode 100644
index 0000000..6e20031
--- /dev/null
+++ b/src/app/dashboard/login/page.tsx
@@ -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 (
+
+
+ Britton Benefits
+ {/*
Britton
+
Benefits
*/}
+
+
+
+ )
+}
diff --git a/src/app/dashboard/member/claims/page.tsx b/src/app/dashboard/member/claims/page.tsx
new file mode 100644
index 0000000..002c5e5
--- /dev/null
+++ b/src/app/dashboard/member/claims/page.tsx
@@ -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 &&
+
+
+
+
+
+
+
+
+ Claim #
+
+
+ Patient Name
+
+
+ Provider
+
+
+ Visit Date
+
+
+ Claim Status
+
+
+ {(claimsData).map((mc) => (
+
+
+ {mc.claim_number}
+
+
+ {mc.patient_name}
+
+
+ {mc.provider_name}
+
+
+ {mc.visit_date}
+
+
+ {mc.claim_status}
+
+
+ ))}
+
+
+
+
+ }
+ >
+ );
+ } catch (error: any) {
+ if (error.message === 'NEXT_REDIRECT') throw error;
+
+ if (error.message === 'Unauthenticated') {
+ redirect('/account/signin');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/dashboard/member/page.tsx b/src/app/dashboard/member/page.tsx
index eb6ea40..2bd75d1 100644
--- a/src/app/dashboard/member/page.tsx
+++ b/src/app/dashboard/member/page.tsx
@@ -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 (
-
-
-
-
-
-
-
-
+// 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 &&
+
-
- )
+ }
+ >
+ );
+ } 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
Session invalid or expired.
;
+// }
}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 592eda8..fe52d38 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -19,7 +19,7 @@ export default function RootLayout({
return (
-
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index c085a94..a081cca 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -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"
/>
-
+
- {data.homePage.section1Body}
+ What we do for members
- {data.homePage.section2Body}
+ What we do for employers
-
- {data.homePage.section3Body}
-
-
+
+ What we do for providers
+
+
+
+
- {data.homePage.section4Body}
+ What we do for brokers
diff --git a/src/components/AccountDropdown.tsx b/src/components/AccountDropdown.tsx
index c2432e1..2fda480 100644
--- a/src/components/AccountDropdown.tsx
+++ b/src/components/AccountDropdown.tsx
@@ -28,8 +28,8 @@ const AccountDropdown: React.FC
= ({userRole}) => {
-
- Change Password
+
+ Dashboard
diff --git a/src/components/BenefitsAAGWidget.tsx b/src/components/BenefitsAAGWidget.tsx
index 8d46dc6..2e81567 100644
--- a/src/components/BenefitsAAGWidget.tsx
+++ b/src/components/BenefitsAAGWidget.tsx
@@ -3,50 +3,42 @@
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 = ({ planBenefits }) => {
+
+ const planTitle = planBenefits[0]?.title || "Unknown Plan Benefits"
+
return (
-
+
Benefits At A Glance
-
CLASSIC 5K 100/5000
-
-
-
Physician Visit
-
Specialist Visit
-
Urgent Care
-
INN-Ind Ded
-
INN-Family Ded
-
OON-Ind Ded
-
OON-Family Ded
-
Co-Insurance
-
INN-Ind OOP
-
INN-Family OOP
-
OON-Ind OOP
-
OON-Family OOP
-
Emergency Room
-
Preventive Care
-
-
-
$25.00
-
$50.00
-
$50.00
-
$5,000
-
$10,000
-
$10,000
-
$20,000
-
100%/0%
-
$5,000
-
$10,000
-
$10,000
-
$20,000
-
Ded & Coin
-
100%
-
+
{planTitle}
+
+ {(planBenefits).map((mb) => (
+
+
+ {mb.benefit_desc}
+
+
+
+ {mb.benefit}
+
+
+ ))}
);
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index c7dc002..6dc6819 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -5,16 +5,18 @@ import classnames from 'classnames';
interface ButtonProps {
onClick?: () => void;
+ disabled?: boolean;
children: React.ReactNode;
className?: string;
}
-const Button: React.FC
= ({ onClick, children, className }) => {
+const Button: React.FC = ({ onClick, disabled, children, className }) => {
const [isHovered, setIsHovered] = useState(false);
return (
= ({ userDisplayName }) =>
return (
{userDisplayName}
diff --git a/src/components/EmployerMemberSelect.tsx b/src/components/EmployerMemberSelect.tsx
new file mode 100644
index 0000000..ba37b1b
--- /dev/null
+++ b/src/components/EmployerMemberSelect.tsx
@@ -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
= ({ onSelectionCompleteEmployer, onSelectionCompleteMember, userRole, employerAllButtonText, plPlanKey, employerName, brokerId, carrierId }) => {
+ const [optionsEmployer, setOptionsEmployer] = useState([]);
+ const [selectedEmployer, setSelectedEmployer] = useState('');
+ const [loadingEmployer, setLoadingEmployer] = useState(false);
+
+ const [optionsMember, setOptionsMember] = useState([]);
+ const [selectedMember, setSelectedMember] = useState('');
+ const [loadingMember, setLoadingMember] = useState(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) => {
+ const val = e.target.value;
+ setSelectedEmployer(val);
+ };
+
+ const handleAllEmployerSelect = () => {
+ if (selectedEmployer) {
+ onSelectionCompleteEmployer(selectedEmployer);
+ }
+ };
+
+ const handleSecondChange = (e: ChangeEvent) => {
+ const val = e.target.value;
+ setSelectedMember(val);
+ // if (val) {
+ // onSelectionCompleteMember(val);
+ // }
+ };
+
+ const handleMemberSelect = () => {
+ if (selectedMember) {
+ onSelectionCompleteMember(selectedMember);
+ }
+ };
+
+ return (
+
+ {/* First Select Dropdown */}
+ {!plPlanKey &&
+
+
Employer
+
+
+ {loadingEmployer ? 'Loading...' : '-- Select Employer --'}
+ {optionsEmployer.map((opt) => (
+
+ {opt.name}
+
+ ))}
+
+
+
+ {employerAllButtonText}
+
+
+
+
+ }
+
+ {/* Second Select Dropdown */}
+
+
Member
+
+
+
+ {!selectedEmployer
+ ? '-- Select a category first --'
+ : loadingMember ? 'Loading...' : '-- Select Subcategory --'}
+
+ {optionsMember.map((opt) => (
+
+ {opt.name}
+
+ ))}
+
+
+
+ Select Member
+
+
+
+
+
+ );
+}
diff --git a/src/components/FindProvider.tsx b/src/components/FindProvider.tsx
index 7935b60..10fabb8 100644
--- a/src/components/FindProvider.tsx
+++ b/src/components/FindProvider.tsx
@@ -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 = ({ zipOnly }) => {
-
+interface NetworkProvider {
+ network_provider: string;
+}
+
+const FindProviderWidget: React.FC = ({ networkProvider }) => {
+
return (
-
-
-
-
Find A Provider By...
+
+
+ {networkProvider &&
+
+
Find A {networkProvider} Provider
-
-
- {/*
*/}
-
-
+ }
+ {networkProvider == "Cigna" &&
+
+
+
-
-
- Search
+
+ }
+ {networkProvider == "MedCost" &&
+
+
+
-
-
-
- View Full Provider List
-
-
-
+
+ }
+
);
};
-export default FindProvider;
\ No newline at end of file
+export default FindProviderWidget;
\ No newline at end of file
diff --git a/src/components/FindProviderOld.tsx b/src/components/FindProviderOld.tsx
new file mode 100644
index 0000000..7935b60
--- /dev/null
+++ b/src/components/FindProviderOld.tsx
@@ -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
= ({ zipOnly }) => {
+
+ return (
+
+
+
+
Find A Provider By...
+
+
+
+ {/*
*/}
+
+
+
+
+
+ Search
+
+
+
+
+ View Full Provider List
+
+
+
+
+ );
+};
+
+export default FindProvider;
\ No newline at end of file
diff --git a/src/components/FindRxInformation.tsx b/src/components/FindRxInformation.tsx
new file mode 100644
index 0000000..d3a65b9
--- /dev/null
+++ b/src/components/FindRxInformation.tsx
@@ -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 (
+
+ );
+};
+
+export default FindRxInformationWidget;
\ No newline at end of file
diff --git a/src/components/IdCardDownloader.tsx b/src/components/IdCardDownloader.tsx
new file mode 100644
index 0000000..b161f5e
--- /dev/null
+++ b/src/components/IdCardDownloader.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import { useState, useEffect } from "react";
+
+interface IdCardDownloaderProps {
+ plPlanKey: number;
+}
+
+const IdCardDownloader: React.FC = ({ 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 (
+
+
+ Download Employer Cards
+
+
+ {(loading) && (
+
+ {loading && (
+
+ {/* Spinner */}
+
+
+
+
+
Generating ID Cards...
+
+ )}
+
+ )}
+
+ {/* Loading Animation */}
+ {/* {loading && (
+
+
+
+
+
+ Fetching ID Card, Please Wait...
+
+ )} */}
+
+ {/* Display PDF */}
+ {/* {pdfUrl && (
+
+
+
+ )} */}
+
+ );
+}
+
+export default IdCardDownloader;
\ No newline at end of file
diff --git a/src/components/IdCardViewer.tsx b/src/components/IdCardViewer.tsx
new file mode 100644
index 0000000..d760b2f
--- /dev/null
+++ b/src/components/IdCardViewer.tsx
@@ -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 = ({ cardLaout, pbEntityKey }) => {
+ const [loading, setLoading] = useState(false);
+ const [pdfUrl, setPdfUrl] = useState(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 (
+
+
+
+
View Temporary ID Card
+
+
+
+
+
+
+
+ {/*
+
+ View ID Card
+ */}
+
+ {(loading || pdfUrl) && (
+
+ {loading && (
+
+ {/* Spinner */}
+
+
+
+
+
Generating ID Card...
+
+ )}
+
+ {pdfUrl && (
+
+
+ {/* Top Bar with Close Button */}
+
+ ID Card Preview
+
+ Close
+
+ {/*
+ Close
+ */}
+
+
+
+
+ )}
+
+ )}
+
+ {/* Loading Animation */}
+ {/* {loading && (
+
+
+
+
+
+ Fetching ID Card, Please Wait...
+
+ )} */}
+
+ {/* Display PDF */}
+ {/* {pdfUrl && (
+
+
+
+ )} */}
+
+ );
+}
+
+export default IdCardViewer;
\ No newline at end of file
diff --git a/src/components/InfoCard.tsx b/src/components/InfoCard.tsx
index fe8e6f7..18ab907 100644
--- a/src/components/InfoCard.tsx
+++ b/src/components/InfoCard.tsx
@@ -41,14 +41,14 @@ const InfoCard: React.FC
= (
-
= (
"relative w-full pt-8 mb-2 ml-8 mr-100 border-b-3",
{
"border-atmosphere" : !invert,
- "border-bronze" : invert
+ "border-platinum" : invert
}
)}>
= (
= (
)}>
Learn More
diff --git a/src/components/InsuranceCard.tsx b/src/components/InsuranceCard.tsx
index eda3efe..c17b8da 100644
--- a/src/components/InsuranceCard.tsx
+++ b/src/components/InsuranceCard.tsx
@@ -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
= ({ pbEntityKey, plPlanKey }) => {
return (
-
+
-
-
-
+
+ {/*
View
-
-
|
-
+ */}
+ {/*
+
+
*/}
+ {/*
+ |
+
*/}
+ {/*
|
*/}
+ {/*
+
+
*/}
+ {/*
Download
-
+ */}
-
+ {pbEntityKey &&
+
+
+
+ }
+ {plPlanKey &&
+
+
+
+ }
+
);
};
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 775a74f..ef47843 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -145,11 +145,11 @@ const Navbar: React.FC
= ({ collapseHeight }) => {
Brokers
-
+ {/*
Services
-
+ */}
{/*
{menuItems.map((item) => {
@@ -164,10 +164,15 @@ const Navbar: React.FC = ({ collapseHeight }) => {
})}
*/}
-
-
+ {/*
+
+ Sign Up
+
+ */}
+
+
Sign In
-
+
{/*
Sign In
diff --git a/src/components/RecentClaimsWidget.tsx b/src/components/RecentClaimsWidget.tsx
index d40c0a0..76c4cb2 100644
--- a/src/components/RecentClaimsWidget.tsx
+++ b/src/components/RecentClaimsWidget.tsx
@@ -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 = ({ recentClaims }) => {
+
+
return (
-
-
+
+
-
-
-
-
- 2907457
-
+
+
+
+ Claim #
-
- LA'BLAH, ROBERT
+
+ Patient Name
-
- HOUSE, GREGORY
+
+ Provider
-
- 07/21/2025
+
+ Visit Date
+
+
+ Claim Status
-
-
-
- 2907201
-
+ {(recentClaims ?? []).map((mc) => (
+
+
+ {mc.claim_number}
+
+
+ {mc.patient_name}
+
+
+ {mc.provider_name}
+
+
+ {mc.visit_date}
+
+
+ {mc.claim_status}
+
-
- LA'BLAH, ROBERT
-
-
- QUINN, MICHAELA
-
-
- 07/02/2025
-
-
-
-
-
- 2905891
-
-
-
- LA'BLAH, JENNY
-
-
- HOWSER, DOGGIE
-
-
- 05/15/2025
-
-
-
-
-
- 2905547
-
-
-
- LA'BLAH, ROBERT
-
-
- HOUSE, GREGORY
-
-
- 04/29/2025
-
-
+ ))}
-
- View All Claims
+
+
+ View All Claims
+
diff --git a/src/components/SideNav.tsx b/src/components/SideNav.tsx
index 02b120d..659816e 100644
--- a/src/components/SideNav.tsx
+++ b/src/components/SideNav.tsx
@@ -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
= (
}
)}>
-
-
+ {/* setIsSidebarOpen(!isSidebarOpen)}
className="h-10 flex justify-end items-center pr-2"
>
{isSidebarOpen ? : }
-
+ */}
{navItems.map((navItem, index) => (
-
{isSidebarOpen ? (
<>{navItem.title}>
diff --git a/src/components/SignOutTimer.tsx b/src/components/SignOutTimer.tsx
new file mode 100644
index 0000000..a81d57b
--- /dev/null
+++ b/src/components/SignOutTimer.tsx
@@ -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 (
+
+
+
Session Timeout Warning
+
+ You will be automatically logged out in {remainingTime} seconds due to inactivity.
+
+
+ Click anywhere or press any key to stay logged in.
+
+
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
+
+
+
+ );
+}
diff --git a/src/lib/dashboard.tsx b/src/lib/dashboard.tsx
index 9571d3d..152ab67 100644
--- a/src/lib/dashboard.tsx
+++ b/src/lib/dashboard.tsx
@@ -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"
+ // }
]
}
\ No newline at end of file
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 4ce612d..d189b84 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -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 {
diff --git a/tsconfig.json b/tsconfig.json
index c133409..d2bba22 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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"
+ ]
}