import React, { useEffect, useRef, useState } from 'react';
import { useDirtyReport } from '../../../../hooks/useDirtyReport';
import { CheckboxControl } from "../../../../components/ui/checkbox-control";
import { TextControl } from "../../../../components/ui/text-control";
import { DismissibleAlert } from '../../../../components/ui/dismissible-alert';
import { Flex, FlexItem } from '../../../../components/ui/flex';
import { Spacer } from '../../../../components/ui/spacer';
import { useAuthServiceContext } from '../../../../services/AuthService';
import {
BaaRecord,
PublishedBaaDocument,
SignBaaRequest,
useBAAService,
BAA_SIGNING_ATTESTATION,
} from '../../../../services/BAAService.tsx';
import { AgreementBlocks } from '../../../../components/FirstSignInGate/AgreementBlocks';
import { formatSingleAddress } from '../../../../utils/fhirAddress';
import { formatPhoneNumber } from '../../../../utils/phoneNumber';
import { formatFhirHumanName } from '../../../../utils/fhirHumanName.tsx';
import { baaSignErrorMessage, isAgreementChanged } from './baaSignErrors';
import { isBaaFormDirty } from './baaSignDraft';
import { isSaveRetired, startSaveOperation } from '../../../../utils/saveOperation';
import { onViewerChange } from '../../../../utils/writeThroughHold';
import { environment } from '../../../../environments/environment.ts';
import {
CHECK_UNAVAILABLE, EARLIER_UNCONFIRMED, NOT_TRACKED, NOT_YET_CONFIRMED, SIGN_AGAIN_WARNING, SIGN_UNCONFIRMED,
intentDigest, observeBaa, onHeldAttemptsChange, readHeldAttempts, receiptStanding, reserveAttempt, settleAttempt, signReceiptOf, updateAttempt,
type BaaObservation, type HeldRead, type SignProof, type SignScope,
} from './baaSignOutcome';
import './BaaSignModal.css';
export interface BaaSignFormParts {
body: React.ReactNode;
actions: React.ReactNode;
// The agreement shown in full wants a wider frame.
wide: boolean;
}
interface BaaSignFormProps {
pdfTemplateUrl?: string;
htmlTemplateUrl?: string;
/** The published agreement in force. When set it is the text shown and the version signed. */
published?: PublishedBaaDocument | null;
/** `proof` says whether this attempt's own answer is known, or only what the agreement now shows. */
onSigned: (record: BaaRecord, proof?: SignProof) => void;
onDirtyChange?: (dirty: boolean) => void;
// The Sign Akashic BAA modal and the setup guide's BAA panel frame the same form differently.
children: (parts: BaaSignFormParts) => React.ReactNode;
}
const escapeHtml = (value: string): string =>
value
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
// Tokens expected in the S3 HTML template (baa-templates/akashic-standard-baa.html).
// The authoritative document is still the server-generated PDF; this HTML is a
// presentation copy and must be kept in step with the PDF template in S3.
const fillTemplate = (template: string, form: SignBaaRequest, signDate: string): string =>
template
.replace(/\{\{ORG_NAME\}\}/g, escapeHtml(form.orgName))
.replace(/\{\{SIGNER_NAME\}\}/g, escapeHtml(form.signerName))
.replace(/\{\{SIGNER_TITLE\}\}/g, escapeHtml(form.signerTitle))
.replace(/\{\{ORG_ADDRESS\}\}/g, escapeHtml(form.orgAddress))
.replace(/\{\{ORG_PHONE\}\}/g, escapeHtml(form.orgPhone))
.replace(/\{\{SIGN_DATE\}\}/g, escapeHtml(signDate));
// The deck shows the agreement as compact reading text; the template's own heading sizes are for the PDF.
// Placed last in the head so it wins over the template's own rules.
const COMPACT_DOC_STYLE = '';
const withCompactStyle = (html: string): string =>
/<\/head>/i.test(html) ? html.replace(/<\/head>/i, (end) => COMPACT_DOC_STYLE + end) : html + COMPACT_DOC_STYLE;
// Every field is printed into the executed agreement, so none may be blank; the labels name what is missing.
const REQUIRED_FIELDS: ReadonlyArray<[keyof SignBaaRequest, string]> = [
['signerName', 'Who is signing'],
['signerTitle', 'Your title or role'],
['orgName', 'Organization legal name'],
['orgAddress', 'Organization address'],
['orgPhone', 'Organization phone'],
];
// The published agreement, through the same block renderer the sign-in gate uses for Akashic's own
// agreements, so a numbered list keeps its numbers and a link stays a link here too.
const PublishedAgreement = ({ agreement }: { agreement: PublishedBaaDocument }) => (
);
// A refusal the route answered before writing anything: it settles only the attempt it answered.
const refusedBeforeWrite = (error: unknown): boolean => {
const status = (error as { status?: unknown } | null)?.status;
return typeof status === 'number' && status >= 400 && status < 500;
};
const signedNote = (observation: Extract): string => {
const signedBy = observation.record.signedBy as { name?: unknown; title?: unknown } | undefined;
const name = typeof signedBy?.name === 'string' && signedBy.name ? signedBy.name : undefined;
const title = typeof signedBy?.title === 'string' && signedBy.title ? signedBy.title : undefined;
const edition = observation.edition ? ` (version ${observation.edition})` : '; its version could not be confirmed';
return `This practice's agreement is signed${edition}${name ? ` by ${name}${title ? `, ${title}` : ''}` : ''}.`;
};
const BaaSignForm = ({ pdfTemplateUrl, htmlTemplateUrl, published, onSigned, onDirtyChange, children }: BaaSignFormProps) => {
const { wpOrganization, currentUser } = useAuthServiceContext();
const { signBaa, getBaa } = useBAAService();
const viewer = currentUser?.fhir?.id ? `${currentUser.fhir.resourceType}/${currentUser.fhir.id}` : undefined;
const practiceId = wpOrganization.current?.id;
const scopeKey = practiceId && viewer ? JSON.stringify([practiceId, viewer, environment.fhirBaseApi]) : undefined;
const scopeOf = (): SignScope | undefined => (scopeKey && practiceId && viewer
? { practiceId, viewer, apiOrigin: environment.fhirBaseApi } : undefined);
const scopeNow = useRef(scopeOf);
scopeNow.current = scopeOf;
const heldIn = (scope: SignScope | undefined): HeldRead => (scope ? readHeldAttempts(scope) : { held: [], unreadable: false });
const [held, setHeld] = useState(() => heldIn(scopeOf()));
const [checking, setChecking] = useState(false);
const [checkNote, setCheckNote] = useState(null);
// Signing again after an unconfirmed attempt is a new act: offered only once Check found nothing yet,
// and only after the warning is acknowledged and the agreement confirmed again.
const [mayResign, setMayResign] = useState(false);
const [resignAcknowledged, setResignAcknowledged] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
// Each opening is its own identity, set up by its effect: an answer commits only into the opening it
// started in. Another viewer or practice is a new opening with its own state, while every attempt still
// kept in this tab stays held for it to Check.
const opening = useRef