From e75a1e12d0ad1fcc740650c82a316566f401b093 Mon Sep 17 00:00:00 2001 From: Potenz Date: Mon, 10 Nov 2025 19:06:52 +0530 Subject: [PATCH] fix( remarks, tooth missing) - ui fixed --- .../src/components/claims/claim-form.tsx | 57 ++--- .../src/components/claims/claims-ui.tsx | 83 ++++++ .../src/components/claims/tooth-ui.tsx | 238 ++++++++++-------- 3 files changed, 241 insertions(+), 137 deletions(-) create mode 100644 apps/Frontend/src/components/claims/claims-ui.tsx diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 8723ece..917b5e6 100644 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -52,7 +52,8 @@ import { } from "@/utils/procedureCombosMapping"; import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos"; import { DateInput } from "../ui/dateInput"; -import { TeethGrid, ToothSelectRadix } from "./tooth-ui"; +import { MissingTeethSimple, type MissingMapStrict } from "./tooth-ui"; +import { RemarksField } from "./claims-ui"; interface ClaimFormProps { patientId: number; @@ -349,7 +350,7 @@ export function ClaimForm({ dateOfBirth: normalizeToIsoDateString(patient?.dateOfBirth), remarks: "", missingTeethStatus: "No_missing", - missingTeeth: {}, + missingTeeth: {} as MissingMapStrict, serviceDate: serviceDate, insuranceProvider: "", insuranceSiteKey: "", @@ -453,13 +454,8 @@ export function ClaimForm({ [] ); - const onToothChange = useCallback( - (name: string, v: "" | "X" | "O") => updateMissingTooth(name, v), - [updateMissingTooth] - ); - const clearAllToothSelections = () => - setForm((prev) => ({ ...prev, missingTeeth: {} })); + setForm((prev) => ({ ...prev, missingTeeth: {} as MissingMapStrict })); // for serviceLine rows, to auto scroll when it got updated by combo buttons and all. const rowRefs = useRef<(HTMLDivElement | null)[]>([]); @@ -862,20 +858,6 @@ export function ClaimForm({ - {/* Clinical Notes Entry */} -
- - setForm({ ...form, remarks: e.target.value })} - /> -
- {/* Service Lines */}

@@ -1401,23 +1383,26 @@ export function ClaimForm({ {/* When specifying per-tooth values, show Permanent + Primary grids */} {form.missingTeethStatus === "Yes_missing" && ( -
- - -
+ + setForm((prev) => ({ ...prev, missingTeeth: next })) + } + /> )}

+ {/* Clinical Notes Entry */} +
+

Remarks

+ + setForm((prev) => ({ ...prev, remarks: next })) + } + /> +
+ {/* Insurance Carriers */}

diff --git a/apps/Frontend/src/components/claims/claims-ui.tsx b/apps/Frontend/src/components/claims/claims-ui.tsx new file mode 100644 index 0000000..e750eee --- /dev/null +++ b/apps/Frontend/src/components/claims/claims-ui.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { Input } from "@/components/ui/input"; + +export function RemarksField({ + value, + onChange, + debounceMs = 250, // tweak (150–300) if you like +}: { + value: string; + onChange: (next: string) => void; + debounceMs?: number; +}) { + const [local, setLocal] = React.useState(() => value); + + // Track last prop we saw to detect true external changes + const lastPropRef = React.useRef(value); + React.useEffect(() => { + if (value !== lastPropRef.current && value !== local) { + // Only sync when parent changed from elsewhere + setLocal(value); + } + lastPropRef.current = value; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); // (intentionally ignoring `local` in deps) + + // Debounce: call parent onChange after user pauses typing + const timerRef = React.useRef(null); + const schedulePush = React.useCallback( + (next: string) => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + timerRef.current = window.setTimeout(() => { + timerRef.current = null; + onChange(next); + // update lastPropRef so the next parent echo won't resync over local + lastPropRef.current = next; + }, debounceMs); + }, + [onChange, debounceMs] + ); + + // Flush on unmount to avoid losing the last input + React.useEffect(() => { + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + onChange(local); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ { + const next = e.target.value; + setLocal(next); // instant local update (no lag) + schedulePush(next); // debounced parent update + }} + onBlur={() => { + // ensure latest text is pushed when the field loses focus + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + if (local !== lastPropRef.current) { + onChange(local); + lastPropRef.current = local; + } + }} + /> +
+ ); +} diff --git a/apps/Frontend/src/components/claims/tooth-ui.tsx b/apps/Frontend/src/components/claims/tooth-ui.tsx index 34f54f5..ac5b05a 100644 --- a/apps/Frontend/src/components/claims/tooth-ui.tsx +++ b/apps/Frontend/src/components/claims/tooth-ui.tsx @@ -1,105 +1,7 @@ import React from "react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -const NONE = "__NONE__" as const; -export type ToothVal = "" | "X" | "O"; - -interface ToothProps { - name: string; - value: ToothVal; // "" | "X" | "O" - onChange: (name: string, v: ToothVal) => void; -} - -/** - * Clean, single select: - * - Empty state shows a blank trigger (no "None" text). - * - First menu item sets empty value, but its label is visually blank. - * - Cell fills column width so the grid can wrap -> no overflow. - */ -function ToothSelect({ name, value, onChange }: ToothProps) { - const label = name.replace("T_", ""); - const uiValue = (value === "" ? NONE : value) as typeof NONE | "X" | "O"; - - return ( -
-
{label}
- - -
- ); -} - -export const ToothSelectRadix = React.memo( - ToothSelect, - (prev, next) => prev.value === next.value && prev.name === next.name -); - -export function TeethGrid({ - title, - toothNames, - values, - onChange, -}: { - title: string; - toothNames: string[]; - values: Record; - onChange: (name: string, v: ToothVal) => void; -}) { - return ( -
-
{title}
- - {/* responsive grid that auto-fits cells; no horizontal overflow */} -
- {toothNames.map((name) => ( - - ))} -
-
- ); -} - +import { Label } from "recharts"; +import { Input } from "../ui/input"; +import { Button } from "../ui/button"; // ——— Missing Teeth helpers for claim-view and edit modal——— type MissingMap = Record; @@ -153,3 +55,137 @@ export function ToothChip({ name, v }: { name: string; v: ToothVal }) { ); } + +export type ToothVal = "X" | "O"; +export type MissingMapStrict = Record; + +/* ---------- parsing helpers ---------- */ +const PERM_NUMBERS = new Set( + Array.from({ length: 32 }, (_, i) => String(i + 1)) +); +const PRIM_LETTERS = new Set(Array.from("ABCDEFGHIJKLMNOPQRST")); + +function normalizeToothToken(token: string): string | null { + const t = token.trim().toUpperCase(); + if (!t) return null; + if (PERM_NUMBERS.has(t)) return t; // 1..32 + if (t.length === 1 && PRIM_LETTERS.has(t)) return t; // A..T + return null; +} + +function listToEntries(list: string, val: ToothVal): Array<[string, ToothVal]> { + if (!list) return []; + const seen = new Set(); + return list + .split(/[,\s]+/g) // commas OR spaces + .map(normalizeToothToken) // uppercase + validate + .filter((t): t is string => !!t) + .filter((t) => { + // de-duplicate within field + if (seen.has(t)) return false; + seen.add(t); + return true; + }) + .map((t) => [`T_${t}`, val]); +} + +/** Build map; 'O' overrides 'X' when duplicated across fields. */ +export function mapFromLists( + missingList: string, + pullList: string +): MissingMapStrict { + const map: MissingMapStrict = {}; + for (const [k, v] of listToEntries(missingList, "X")) map[k] = v; + for (const [k, v] of listToEntries(pullList, "O")) map[k] = v; + return map; +} + +/** For initializing the inputs from an existing map (used only on mount or clear). */ +export function listsFromMap(map: Record): { + missing: string; + toPull: string; +} { + const missing: string[] = []; + const toPull: string[] = []; + for (const [k, v] of Object.entries(map || {})) { + if (v === "X") missing.push(k.replace(/^T_/, "")); + else if (v === "O") toPull.push(k.replace(/^T_/, "")); + } + const sort = (a: string, b: string) => { + const na = Number(a), + nb = Number(b); + const an = !Number.isNaN(na), + bn = !Number.isNaN(nb); + if (an && bn) return na - nb; + if (an) return -1; + if (bn) return 1; + return a.localeCompare(b); + }; + missing.sort(sort); + toPull.sort(sort); + return { missing: missing.join(", "), toPull: toPull.join(", ") }; +} + +/* ---------- UI ---------- */ +export function MissingTeethSimple({ + value, + onChange, +}: { + /** Must match ClaimFormData.missingTeeth exactly */ + value: MissingMapStrict; + onChange: (next: MissingMapStrict) => void; +}) { + // initialize text inputs from incoming map + const init = React.useMemo(() => listsFromMap(value), []); // only on mount + const [missingField, setMissingField] = React.useState(init.missing); + const [pullField, setPullField] = React.useState(init.toPull); + + // only resync when parent CLEARS everything (so your Clear All works) + React.useEffect(() => { + if (!value || Object.keys(value).length === 0) { + setMissingField(""); + setPullField(""); + } + }, [value]); + + const recompute = (mStr: string, pStr: string) => { + onChange(mapFromLists(mStr, pStr)); + }; + + return ( +
+
+
+ {/* simple text label (no recharts Label) */} +
Tooth Number - Missing - X
+ { + const m = e.target.value.toUpperCase(); // keep uppercase in the field + setMissingField(m); + recompute(m, pullField); + }} + aria-label="Tooth Numbers — Missing" + /> +
+ +
+
+ Tooth Number - To be pulled - O +
+ { + const p = e.target.value.toUpperCase(); // keep uppercase in the field + setPullField(p); + recompute(missingField, p); + }} + aria-label="Tooth Numbers — To be pulled" + /> +
+
+
+ ); +}