feat: add license activation system with feature gates
- License key generator tool at ~/Desktop/LicenseGenerator - Backend validator route (GET /api/license/status, POST /api/license/activate) - Activation page in sidebar with status, key input, and free/premium feature list - useLicense hook for frontend license state - Feature gates: premium eligibility buttons (DDMA, DeltaIns, Tufts, United, CCA) disabled without license - Feature gates: premium claim buttons (CCA, Delta MA, United, Tufts) and all PreAuth buttons disabled without license - Free features always active: MassHealth eligibility/claim, Documents, Payments, Backups, Reports - README: license key generator usage instructions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ const JobMonitorPage = lazy(() => import("./pages/job-monitor-page"));
|
||||
const ChartPage = lazy(() => import("./pages/chart-page"));
|
||||
const DentalShoppingSearchTagPage = lazy(() => import("./pages/dental-shopping-search-tag-page"));
|
||||
const DentalShoppingLoginInfoPage = lazy(() => import("./pages/dental-shopping-login-info-page"));
|
||||
const ActivationPage = lazy(() => import("./pages/activation-page"));
|
||||
const NotFound = lazy(() => import("./pages/not-found"));
|
||||
|
||||
function Router() {
|
||||
@@ -65,6 +66,7 @@ function Router() {
|
||||
<ProtectedRoute path="/cloud-storage" component={() => <CloudStoragePage />} />
|
||||
<ProtectedRoute path="/dental-shopping/search-tag" component={() => <DentalShoppingSearchTagPage />} />
|
||||
<ProtectedRoute path="/dental-shopping/login-info" component={() => <DentalShoppingLoginInfoPage />} />
|
||||
<ProtectedRoute path="/activation" component={() => <ActivationPage />} adminOnly />
|
||||
<ProtectedRoute
|
||||
path="/job-monitor"
|
||||
component={() => <JobMonitorPage />}
|
||||
|
||||
@@ -93,6 +93,7 @@ interface ClaimFormProps {
|
||||
onHandleForUnitedDHSeleniumClaim: (data: ClaimFormData) => void;
|
||||
onHandleForTuftsSCOSeleniumClaim: (data: ClaimFormData) => void;
|
||||
onClose: () => void;
|
||||
isLicensed?: boolean;
|
||||
}
|
||||
|
||||
export function ClaimForm({
|
||||
@@ -111,6 +112,7 @@ export function ClaimForm({
|
||||
onHandleForTuftsSCOSeleniumClaim,
|
||||
onSubmit,
|
||||
onClose,
|
||||
isLicensed = false,
|
||||
}: ClaimFormProps) {
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
@@ -2196,24 +2198,32 @@ export function ClaimForm({
|
||||
<Button
|
||||
className="w-32 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={() => runWithPriceCheck(handleCCAClaim)}
|
||||
disabled={!isLicensed}
|
||||
title={!isLicensed ? "License required" : undefined}
|
||||
>
|
||||
CCA Claim
|
||||
</Button>
|
||||
<Button
|
||||
className="w-36 bg-violet-600 hover:bg-violet-700 text-white"
|
||||
onClick={() => runWithPriceCheck(handleDDMAClaim)}
|
||||
disabled={!isLicensed}
|
||||
title={!isLicensed ? "License required" : undefined}
|
||||
>
|
||||
Delta MA Claim
|
||||
</Button>
|
||||
<Button
|
||||
className="w-44 bg-orange-600 hover:bg-orange-700 text-white"
|
||||
onClick={() => runWithPriceCheck(handleUnitedDHClaim)}
|
||||
disabled={!isLicensed}
|
||||
title={!isLicensed ? "License required" : undefined}
|
||||
>
|
||||
United/DentalHub Claim
|
||||
</Button>
|
||||
<Button
|
||||
className="w-32 bg-teal-600 hover:bg-teal-700 text-white"
|
||||
onClick={() => runWithPriceCheck(handleTuftsSCOClaim)}
|
||||
disabled={!isLicensed}
|
||||
title={!isLicensed ? "License required" : undefined}
|
||||
>
|
||||
Tufts Claim
|
||||
</Button>
|
||||
@@ -2723,24 +2733,32 @@ export function ClaimForm({
|
||||
<Button
|
||||
className="w-32 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={() => handleMHPreAuth()}
|
||||
disabled={!isLicensed}
|
||||
title={!isLicensed ? "License required" : undefined}
|
||||
>
|
||||
MH PreAuth
|
||||
</Button>
|
||||
<Button
|
||||
className="w-32 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={handleCCAPreAuth}
|
||||
disabled={!isLicensed}
|
||||
title={!isLicensed ? "License required" : undefined}
|
||||
>
|
||||
CCA PreAuth
|
||||
</Button>
|
||||
<Button
|
||||
className="w-44"
|
||||
variant="secondary"
|
||||
disabled={!isLicensed}
|
||||
title={!isLicensed ? "License required" : undefined}
|
||||
>
|
||||
United/DentalHub PreAuth
|
||||
</Button>
|
||||
<Button
|
||||
className="w-32"
|
||||
variant="secondary"
|
||||
disabled={!isLicensed}
|
||||
title={!isLicensed ? "License required" : undefined}
|
||||
>
|
||||
Tufts PreAuth
|
||||
</Button>
|
||||
|
||||
@@ -186,6 +186,12 @@ export function Sidebar() {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Activation",
|
||||
path: "/activation",
|
||||
icon: <KeyRound className="h-5 w-5 text-amber-500" />,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
name: "Database Management",
|
||||
path: "/database-management",
|
||||
|
||||
24
apps/Frontend/src/hooks/use-license.ts
Normal file
24
apps/Frontend/src/hooks/use-license.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
|
||||
interface LicenseStatus {
|
||||
activated: boolean;
|
||||
expired: boolean;
|
||||
expiry: string | null;
|
||||
daysLeft: number | null;
|
||||
}
|
||||
|
||||
export function useLicense() {
|
||||
const { data, isLoading } = useQuery<LicenseStatus>({
|
||||
queryKey: ["/api/license/status"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/license/status");
|
||||
return res.json();
|
||||
},
|
||||
staleTime: 1000 * 60 * 5, // cache for 5 minutes
|
||||
});
|
||||
|
||||
const isLicensed = !isLoading && !!data?.activated && !data?.expired;
|
||||
|
||||
return { isLicensed, isLoading, licenseData: data };
|
||||
}
|
||||
184
apps/Frontend/src/pages/activation-page.tsx
Normal file
184
apps/Frontend/src/pages/activation-page.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { KeyRound, CheckCircle2, XCircle, AlertCircle, Shield, ShieldOff } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function ActivationPage() {
|
||||
const { toast } = useToast();
|
||||
const [keyInput, setKeyInput] = useState("");
|
||||
|
||||
const { data: status, isLoading } = useQuery({
|
||||
queryKey: ["/api/license/status"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/license/status");
|
||||
return res.json() as Promise<{
|
||||
activated: boolean;
|
||||
expired: boolean;
|
||||
expiry: string | null;
|
||||
daysLeft: number | null;
|
||||
}>;
|
||||
},
|
||||
});
|
||||
|
||||
const activateMutation = useMutation({
|
||||
mutationFn: async (key: string) => {
|
||||
const res = await apiRequest("POST", "/api/license/activate", { key });
|
||||
if (!res.ok) {
|
||||
const body = await res.json();
|
||||
throw new Error(body.error || "Activation failed");
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({ title: "License activated successfully" });
|
||||
setKeyInput("");
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/license/status"] });
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
toast({ title: err.message, variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const formatExpiry = (expiry: string) => {
|
||||
return new Date(expiry).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const isActive = status?.activated && !status?.expired;
|
||||
const isExpired = status?.activated && status?.expired;
|
||||
const isWarning = isActive && status?.daysLeft !== null && status.daysLeft <= 30;
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl mx-auto space-y-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<KeyRound className="h-6 w-6 text-gray-600" />
|
||||
<h1 className="text-2xl font-semibold text-gray-800">License Activation</h1>
|
||||
</div>
|
||||
|
||||
{/* Status Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">License Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<p className="text-gray-500 text-sm">Checking license...</p>
|
||||
) : isActive ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
<span className="font-medium text-green-700">Active</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
Expires: <span className="font-medium">{formatExpiry(status!.expiry!)}</span>
|
||||
</p>
|
||||
{isWarning && (
|
||||
<div className="flex items-center space-x-2 mt-2 p-2 bg-yellow-50 rounded-md border border-yellow-200">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 flex-shrink-0" />
|
||||
<p className="text-sm text-yellow-700">
|
||||
Expires in {status!.daysLeft} day{status!.daysLeft !== 1 ? "s" : ""}. Please renew soon.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : isExpired ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
<div>
|
||||
<span className="font-medium text-red-700">Expired</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
Expired on {formatExpiry(status!.expiry!)}. Enter a new key to reactivate.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<XCircle className="h-5 w-5 text-gray-400" />
|
||||
<span className="text-gray-500">Not activated</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Activate Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Enter License Key</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Input
|
||||
placeholder="DENTAL-XXXXXXXXXXXXXXXXXXXXXXXX-YYYY-MM-DD"
|
||||
value={keyInput}
|
||||
onChange={(e) => setKeyInput(e.target.value)}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => activateMutation.mutate(keyInput)}
|
||||
disabled={!keyInput.trim() || activateMutation.isPending}
|
||||
className="w-full"
|
||||
>
|
||||
{activateMutation.isPending ? "Activating..." : "Activate"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Features Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Features</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">Free</p>
|
||||
<div className="space-y-1">
|
||||
{[
|
||||
"MassHealth Eligibility",
|
||||
"MassHealth Claim",
|
||||
"Documents",
|
||||
"Payments",
|
||||
"Database Backups",
|
||||
"Reports",
|
||||
].map((f) => (
|
||||
<div key={f} className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500 flex-shrink-0" />
|
||||
<span className="text-sm text-gray-700">{f}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">Premium</p>
|
||||
<div className="space-y-1">
|
||||
{[
|
||||
"CCA, DDMA, United, Tufts Eligibility",
|
||||
"CCA, DDMA, United, Tufts Claims",
|
||||
"Pre-Authorizations",
|
||||
"AI SMS",
|
||||
].map((f) => (
|
||||
<div key={f} className="flex items-center space-x-2">
|
||||
{isActive ? (
|
||||
<Shield className="h-4 w-4 text-blue-500 flex-shrink-0" />
|
||||
) : (
|
||||
<ShieldOff className="h-4 w-4 text-gray-300 flex-shrink-0" />
|
||||
)}
|
||||
<span className={cn("text-sm", isActive ? "text-gray-700" : "text-gray-400")}>
|
||||
{f}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import ClaimDocumentsUploadMultiple from "@/components/claims/claim-document-upl
|
||||
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
||||
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
||||
import { socket } from "@/lib/socket";
|
||||
import { useLicense } from "@/hooks/use-license";
|
||||
|
||||
export default function ClaimsPage() {
|
||||
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
|
||||
@@ -52,6 +53,7 @@ export default function ClaimsPage() {
|
||||
const { status, message, show } = useAppSelector(
|
||||
(state) => state.seleniumTasks.claimSubmit
|
||||
);
|
||||
const { isLicensed } = useLicense();
|
||||
|
||||
// Track pending selenium jobs so we can react to completion via socket
|
||||
const [pendingClaimJobId, setPendingClaimJobId] = useState<string | null>(null);
|
||||
@@ -911,6 +913,7 @@ export default function ClaimsPage() {
|
||||
onHandleForDDMASeleniumClaim={handleDDMAClaimSubmitSelenium}
|
||||
onHandleForUnitedDHSeleniumClaim={handleUnitedDHClaimSubmitSelenium}
|
||||
onHandleForTuftsSCOSeleniumClaim={handleTuftsSCOClaimSubmitSelenium}
|
||||
isLicensed={isLicensed}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import { DeltaInsEligibilityButton } from "@/components/insurance-status/deltain
|
||||
import { TuftsSCOEligibilityButton } from "@/components/insurance-status/tufts-sco-button-modal";
|
||||
import { UnitedSCOEligibilityButton } from "@/components/insurance-status/united-sco-button-modal";
|
||||
import { CCAEligibilityButton } from "@/components/insurance-status/cca-button-modal";
|
||||
import { useLicense } from "@/hooks/use-license";
|
||||
|
||||
/**
|
||||
* Waits for a Selenium job to complete by racing socket.io events against
|
||||
@@ -95,6 +96,7 @@ function waitForSeleniumJob(
|
||||
export default function InsuranceStatusPage() {
|
||||
const { user } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const { isLicensed } = useLicense();
|
||||
const dispatch = useAppDispatch();
|
||||
const { status, message, show } = useAppSelector(
|
||||
(state) => state.seleniumTasks.eligibilityCheck,
|
||||
@@ -868,39 +870,43 @@ export default function InsuranceStatusPage() {
|
||||
|
||||
{/* Row 1 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<DdmaEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "ddma"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
<div title={!isLicensed ? "License required" : undefined} className={!isLicensed ? "opacity-40 pointer-events-none" : ""}>
|
||||
<DdmaEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "ddma"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DeltaInsEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "delta-ins"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
<div title={!isLicensed ? "License required" : undefined} className={!isLicensed ? "opacity-40 pointer-events-none" : ""}>
|
||||
<DeltaInsEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "delta-ins"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
@@ -914,56 +920,62 @@ export default function InsuranceStatusPage() {
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<TuftsSCOEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "tufts-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
<div title={!isLicensed ? "License required" : undefined} className={!isLicensed ? "opacity-40 pointer-events-none" : ""}>
|
||||
<TuftsSCOEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "tufts-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UnitedSCOEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "united-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
<div title={!isLicensed ? "License required" : undefined} className={!isLicensed ? "opacity-40 pointer-events-none" : ""}>
|
||||
<UnitedSCOEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "united-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CCAEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "cca"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_cca_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
<div title={!isLicensed ? "License required" : undefined} className={!isLicensed ? "opacity-40 pointer-events-none" : ""}>
|
||||
<CCAEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "cca"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_cca_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
|
||||
Reference in New Issue
Block a user