feat(missing teeth) - ui added
This commit is contained in:
@@ -16,6 +16,12 @@ import {
|
|||||||
import { formatDateToHumanReadable } from "@/utils/dateUtils";
|
import { formatDateToHumanReadable } from "@/utils/dateUtils";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { ClaimStatus, ClaimWithServiceLines } from "@repo/db/types";
|
import { ClaimStatus, ClaimWithServiceLines } from "@repo/db/types";
|
||||||
|
import {
|
||||||
|
safeParseMissingTeeth,
|
||||||
|
splitTeeth,
|
||||||
|
ToothChip,
|
||||||
|
toStatusLabel,
|
||||||
|
} from "./tooth-ui";
|
||||||
|
|
||||||
type ClaimEditModalProps = {
|
type ClaimEditModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -223,6 +229,65 @@ export default function ClaimEditModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Missing Teeth */}
|
||||||
|
<div className="space-y-2 pt-4">
|
||||||
|
<h4 className="font-medium text-gray-900">Missing Teeth</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<span className="text-gray-500">Status:</span>{" "}
|
||||||
|
{toStatusLabel((claim as any).missingTeethStatus)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Only show details when the user chose "Specify Missing" */}
|
||||||
|
{(claim as any).missingTeethStatus === "Yes_missing" &&
|
||||||
|
(() => {
|
||||||
|
const map = safeParseMissingTeeth((claim as any).missingTeeth);
|
||||||
|
const { permanent, primary } = splitTeeth(map);
|
||||||
|
const hasAny = permanent.length > 0 || primary.length > 0;
|
||||||
|
|
||||||
|
if (!hasAny) {
|
||||||
|
return (
|
||||||
|
<p className="text-gray-500">
|
||||||
|
No specific teeth marked as missing.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-2 space-y-3">
|
||||||
|
{permanent.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
Permanent
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{permanent.map((t) => (
|
||||||
|
<ToothChip key={t.name} name={t.name} v={t.v} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{primary.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
Primary
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{primary.map((t) => (
|
||||||
|
<ToothChip key={t.name} name={t.name} v={t.v} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{(claim as any).missingTeethStatus === "endentulous" && (
|
||||||
|
<p className="text-sm text-gray-700">Patient is edentulous.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end space-x-2 pt-4">
|
<div className="flex justify-end space-x-2 pt-4">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import React from "react";
|
|||||||
import { formatDateToHumanReadable } from "@/utils/dateUtils";
|
import { formatDateToHumanReadable } from "@/utils/dateUtils";
|
||||||
import { ClaimFileMeta, ClaimWithServiceLines } from "@repo/db/types";
|
import { ClaimFileMeta, ClaimWithServiceLines } from "@repo/db/types";
|
||||||
import { FileText, Paperclip } from "lucide-react";
|
import { FileText, Paperclip } from "lucide-react";
|
||||||
|
import {
|
||||||
|
safeParseMissingTeeth,
|
||||||
|
splitTeeth,
|
||||||
|
ToothChip,
|
||||||
|
toStatusLabel,
|
||||||
|
} from "./tooth-ui";
|
||||||
|
|
||||||
type ClaimViewModalProps = {
|
type ClaimViewModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -239,6 +245,67 @@ export default function ClaimViewModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Missing Teeth */}
|
||||||
|
<div className="space-y-2 pt-4">
|
||||||
|
<h4 className="font-medium text-gray-900">Missing Teeth</h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<span className="text-gray-500">Status:</span>{" "}
|
||||||
|
{toStatusLabel((claim as any).missingTeethStatus)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Only show details when the user chose "Specify Missing" */}
|
||||||
|
{(claim as any).missingTeethStatus === "Yes_missing" &&
|
||||||
|
(() => {
|
||||||
|
const map = safeParseMissingTeeth(
|
||||||
|
(claim as any).missingTeeth
|
||||||
|
);
|
||||||
|
const { permanent, primary } = splitTeeth(map);
|
||||||
|
const hasAny = permanent.length > 0 || primary.length > 0;
|
||||||
|
|
||||||
|
if (!hasAny) {
|
||||||
|
return (
|
||||||
|
<p className="text-gray-500">
|
||||||
|
No specific teeth marked as missing.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-2 space-y-3">
|
||||||
|
{permanent.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
Permanent
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{permanent.map((t) => (
|
||||||
|
<ToothChip key={t.name} name={t.name} v={t.v} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{primary.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium text-gray-600 mb-2">
|
||||||
|
Primary
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{primary.map((t) => (
|
||||||
|
<ToothChip key={t.name} name={t.name} v={t.v} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{(claim as any).missingTeethStatus === "endentulous" && (
|
||||||
|
<p className="text-sm text-gray-700">Patient is edentulous.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Claim Files (metadata) */}
|
{/* Claim Files (metadata) */}
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<h4 className="font-medium text-gray-900 flex items-center space-x-2">
|
<h4 className="font-medium text-gray-900 flex items-center space-x-2">
|
||||||
|
|||||||
@@ -99,3 +99,57 @@ export function TeethGrid({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ——— Missing Teeth helpers for claim-view and edit modal———
|
||||||
|
type MissingMap = Record<string, ToothVal | undefined>;
|
||||||
|
|
||||||
|
export function toStatusLabel(s?: string) {
|
||||||
|
if (!s) return "Unknown";
|
||||||
|
if (s === "No_missing") return "No Missing";
|
||||||
|
if (s === "endentulous") return "Edentulous";
|
||||||
|
if (s === "Yes_missing") return "Specify Missing";
|
||||||
|
// best-effort prettify
|
||||||
|
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function safeParseMissingTeeth(raw: unknown): MissingMap {
|
||||||
|
if (!raw) return {};
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (parsed && typeof parsed === "object") return parsed as MissingMap;
|
||||||
|
} catch {}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (typeof raw === "object") return raw as MissingMap;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERM = new Set(Array.from({ length: 32 }, (_, i) => `T_${i + 1}`));
|
||||||
|
const PRIM = new Set(Array.from("ABCDEFGHIJKLMNOPQRST").map((ch) => `T_${ch}`));
|
||||||
|
|
||||||
|
export function splitTeeth(map: MissingMap) {
|
||||||
|
const permanent: Array<{ name: string; v: ToothVal }> = [];
|
||||||
|
const primary: Array<{ name: string; v: ToothVal }> = [];
|
||||||
|
for (const [k, v] of Object.entries(map)) {
|
||||||
|
if (!v) continue;
|
||||||
|
if (PERM.has(k)) permanent.push({ name: k, v });
|
||||||
|
else if (PRIM.has(k)) primary.push({ name: k, v });
|
||||||
|
}
|
||||||
|
// stable, human-ish order
|
||||||
|
permanent.sort((a, b) => Number(a.name.slice(2)) - Number(b.name.slice(2)));
|
||||||
|
primary.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
return { permanent, primary };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToothChip({ name, v }: { name: string; v: ToothVal }) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs bg-white">
|
||||||
|
<span className="font-medium">{name.replace("T_", "")}</span>
|
||||||
|
<span className="inline-flex h-5 w-5 items-center justify-center rounded border">
|
||||||
|
{v}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user