feat(claim-drawer): validation rule opens peek with rule catalog

This commit is contained in:
Tyler
2026-06-21 17:58:36 -06:00
parent 786ead8c94
commit 8db5db7610
3 changed files with 304 additions and 5 deletions
@@ -0,0 +1,134 @@
interface Props {
/** The rule code (e.g. "R050_diagnosis_present" or just "R050"). */
rule: string;
}
interface RuleDoc {
/** Short human title (e.g. "Diagnosis pointer present"). */
title: string;
/** Plain-English description of what the rule checks. */
description: string;
/** Why the rule matters — operator-facing rationale. */
whyItMatters: string;
/** How to fix — short, actionable. */
howToFix: string;
}
/**
* SP21 Phase 5 Task 5.9: rule catalog used by ValidationRulePeekContent.
*
* The catalog is intentionally small — it covers the rules we actually
* emit today (R050_diagnosis_present, R200_units_recommended). For
* anything not in the catalog the peek still renders (with an "Unknown
* rule" note) — operators should still be able to open the peek for
* any rule code so they can see the originating message verbatim.
*
* Adding a new entry here is the source-of-truth for the rule's
* documentation. The ValidationPanel wires the peek by rule code; if
* we add new rules later (Phase 6+), add a new entry here.
*/
const RULE_CATALOG: Record<string, RuleDoc> = {
R050_diagnosis_present: {
title: "Diagnosis pointer present",
description:
"Each service line must point to at least one diagnosis code in the claim header (the HL segment's HI element). A missing pointer makes the line unprocessable on the payer side.",
whyItMatters:
"Payers reject claims with missing diagnosis pointers at the 999 stage, which would otherwise re-trigger the 999 rejection loop. Catching it here gives the operator a chance to attach the dx before submission.",
howToFix:
"Open the claim's Service Lines table and attach the relevant diagnosis code (e.g. E11.9) to the line. The pointer is the line's diagnosis pointer list.",
},
R200_units_recommended: {
title: "Service line units recommended",
description:
"Service lines that represent timed procedures (anesthesia, critical care, psychotherapy time-based codes) should carry an explicit units value. Defaulting to 1 is acceptable for most codes but flagged here for review.",
whyItMatters:
"Timed codes without units get under-reimbursed — payers default to 1 unit when the field is blank, even when the procedure took 45 minutes. The warning exists so an operator can verify the units are correct before submission.",
howToFix:
"Confirm the units value on the service line matches the documented encounter time. If the code is not time-based, no action is required.",
},
};
/**
* Peek body for a validation rule — opens on top of the ClaimDrawer
* via PeekModal when the operator clicks a rule code in the
* ValidationPanel. The body shows the rule's title, description, why
* it matters, and how to fix it.
*
* Unknown rules (codes not in the catalog) render a small "Unknown
* rule — see originating message" note rather than blowing up. The
* peek still renders so the operator can correlate the rule code to
* whatever they were just looking at.
*
* No fetch — the catalog is static and bundled. (A future phase
* could swap this for a backend-served catalog if rules become
* user-extensible.)
*/
export function ValidationRulePeekContent({ rule }: Props) {
// Normalize: the rule code in the validation payload is the full
// form (`R050_diagnosis_present`), but a future backend response
// might use the short form (`R050`). Look up both.
const doc =
RULE_CATALOG[rule] ??
RULE_CATALOG[rule.split("_")[0] ?? ""] ??
null;
if (!doc) {
return (
<div className="space-y-2">
<div className="display text-[14px] text-foreground">
{rule}
</div>
<div
className="text-[12.5px]"
style={{ color: "hsl(var(--muted-foreground))" }}
>
Unknown rule. The originating message is the authoritative
description this peek is a no-op for undocumented rule codes.
</div>
</div>
);
}
return (
<div className="space-y-3">
<div className="display text-[15px] text-foreground">
{doc.title}
</div>
<div
className="mono text-[11px]"
style={{ color: "hsl(var(--muted-foreground))" }}
>
{rule}
</div>
<div className="text-[13px] leading-relaxed text-[color:var(--m-ink-primary)]">
{doc.description}
</div>
<div className="space-y-1.5 pt-1">
<Section heading="Why it matters">{doc.whyItMatters}</Section>
<Section heading="How to fix">{doc.howToFix}</Section>
</div>
</div>
);
}
function Section({
heading,
children,
}: {
heading: string;
children: React.ReactNode;
}) {
return (
<div>
<div
className="mono text-[10px] uppercase tracking-[0.18em] font-semibold mb-0.5"
style={{ color: "hsl(var(--muted-foreground))" }}
>
{heading}
</div>
<div className="text-[12.5px] leading-relaxed text-[color:var(--m-ink-secondary)]">
{children}
</div>
</div>
);
}