fixed combos, and pdf format updated
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -39,7 +39,6 @@ import {
|
|||||||
} from "@repo/db/types";
|
} from "@repo/db/types";
|
||||||
import { Decimal } from "decimal.js";
|
import { Decimal } from "decimal.js";
|
||||||
import {
|
import {
|
||||||
COMBO_BUTTONS,
|
|
||||||
mapPricesForForm,
|
mapPricesForForm,
|
||||||
applyComboToForm,
|
applyComboToForm,
|
||||||
getDescriptionForCode,
|
getDescriptionForCode,
|
||||||
@@ -257,6 +256,16 @@ export function ClaimForm({
|
|||||||
setForm({ ...form, serviceLines: updatedLines });
|
setForm({ ...form, serviceLines: updatedLines });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// for serviceLine rows, to auto scroll when it got updated by combo buttons and all.
|
||||||
|
const rowRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
|
|
||||||
|
const scrollToLine = (index: number) => {
|
||||||
|
const el = rowRefs.current[index];
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Map Price function
|
// Map Price function
|
||||||
const onMapPrice = () => {
|
const onMapPrice = () => {
|
||||||
setForm((prev) =>
|
setForm((prev) =>
|
||||||
@@ -573,7 +582,11 @@ export function ClaimForm({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="grid grid-cols-[1.5fr,0.5fr,1fr,1fr,1fr,1fr,1fr] gap-1 mb-2 items-center"
|
ref={(el) => {
|
||||||
|
rowRefs.current[i] = el;
|
||||||
|
if (!el) rowRefs.current.splice(i, 1);
|
||||||
|
}}
|
||||||
|
className="scroll-mt-28 grid grid-cols-[1.5fr,0.5fr,1fr,1fr,1fr,1fr,1fr] gap-1 mb-2 items-center"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-[auto,1fr] items-center gap-2">
|
<div className="grid grid-cols-[auto,1fr] items-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -708,23 +721,25 @@ export function ClaimForm({
|
|||||||
+ Add Service Line
|
+ Add Service Line
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="space-y-8 mt-10 mb-10">
|
<div className="space-y-4 mt-8">
|
||||||
{Object.entries(COMBO_CATEGORIES).map(([section, ids]) => (
|
{Object.entries(COMBO_CATEGORIES).map(([section, ids]) => (
|
||||||
<div key={section}>
|
<div key={section}>
|
||||||
<div className="mb-3 text-sm font-semibold opacity-70">
|
<div className="mb-3 text-sm font-semibold opacity-70">
|
||||||
{section}
|
{section}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-1">
|
||||||
{ids.map((id) => {
|
{ids.map((id) => {
|
||||||
const b = PROCEDURE_COMBOS[id];
|
const b = PROCEDURE_COMBOS[id];
|
||||||
if(!b){return}
|
if (!b) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={b.id}
|
key={b.id}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setForm((prev) =>
|
setForm((prev) => {
|
||||||
applyComboToForm(
|
const next = applyComboToForm(
|
||||||
prev,
|
prev,
|
||||||
b.id as any,
|
b.id as any,
|
||||||
patient?.dateOfBirth ?? "",
|
patient?.dateOfBirth ?? "",
|
||||||
@@ -732,8 +747,12 @@ export function ClaimForm({
|
|||||||
replaceAll: true,
|
replaceAll: true,
|
||||||
lineDate: prev.serviceDate,
|
lineDate: prev.serviceDate,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
)
|
|
||||||
|
setTimeout(() => scrollToLine(0), 0);
|
||||||
|
|
||||||
|
return next;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{b.label}
|
{b.label}
|
||||||
@@ -744,13 +763,12 @@ export function ClaimForm({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="pt-4">
|
<div className="pt-2">
|
||||||
<Button variant="success" onClick={onMapPrice}>
|
<Button variant="success" onClick={onMapPrice}>
|
||||||
Map Price
|
Map Price
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File Upload Section */}
|
{/* File Upload Section */}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const PROCEDURE_COMBOS: Record<
|
export const PROCEDURE_COMBOS: Record<
|
||||||
string,
|
string,
|
||||||
{ id: string; label: string; codes: string[] }
|
{ id: string; label: string; codes: string[]; toothNumbers?: (string | null)[] }
|
||||||
> = {
|
> = {
|
||||||
childRecall: {
|
childRecall: {
|
||||||
id: "childRecall",
|
id: "childRecall",
|
||||||
@@ -16,6 +16,7 @@ export const PROCEDURE_COMBOS: Record<
|
|||||||
id: "adultRecall",
|
id: "adultRecall",
|
||||||
label: "Adult Recall",
|
label: "Adult Recall",
|
||||||
codes: ["D0120", "D0220", "D0230", "D0274", "D1110"],
|
codes: ["D0120", "D0220", "D0230", "D0274", "D1110"],
|
||||||
|
toothNumbers: [null, "9", "24", null, null], // only these two need values
|
||||||
},
|
},
|
||||||
newChildPatient: {
|
newChildPatient: {
|
||||||
id: "newChildPatient",
|
id: "newChildPatient",
|
||||||
|
|||||||
@@ -251,7 +251,10 @@ export function applyComboToForm<T extends ClaimFormLike>(
|
|||||||
procedureCode: code,
|
procedureCode: code,
|
||||||
procedureDate: lineDate,
|
procedureDate: lineDate,
|
||||||
oralCavityArea: original?.oralCavityArea ?? "",
|
oralCavityArea: original?.oralCavityArea ?? "",
|
||||||
toothNumber: original?.toothNumber ?? "",
|
toothNumber:
|
||||||
|
preset.toothNumbers?.[j] ??
|
||||||
|
original?.toothNumber ??
|
||||||
|
"",
|
||||||
toothSurface: original?.toothSurface ?? "",
|
toothSurface: original?.toothSurface ?? "",
|
||||||
totalBilled: price,
|
totalBilled: price,
|
||||||
totalAdjusted: new Decimal(0),
|
totalAdjusted: new Decimal(0),
|
||||||
|
|||||||
@@ -4,35 +4,68 @@ import re
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
DOB_RE = re.compile(r'(?<!\d)(\d{1,2})/(\d{1,2})/(\d{4})(?!\d)')
|
||||||
|
ID_RE = re.compile(r'^\d{8,14}$') # 8–14 digits, whole line
|
||||||
|
|
||||||
|
# lines that tell us we've moved past the name/DOB area
|
||||||
|
STOP_WORDS = {
|
||||||
|
'eligibility', 'coverage', 'age band', 'date of', 'service',
|
||||||
|
'tooth', 'number', 'surface', 'procedure', 'code', 'description',
|
||||||
|
'provider', 'printed on', 'member id', 'name', 'date of birth'
|
||||||
|
}
|
||||||
|
|
||||||
@app.route("/extract", methods=["POST"])
|
@app.route("/extract", methods=["POST"])
|
||||||
def extract():
|
def extract():
|
||||||
file = request.files['pdf']
|
file = request.files['pdf']
|
||||||
doc = fitz.open(stream=file.read(), filetype="pdf")
|
doc = fitz.open(stream=file.read(), filetype="pdf")
|
||||||
text = "\n".join(page.get_text() for page in doc)
|
text = "\n".join(page.get_text("text") for page in doc)
|
||||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||||
|
|
||||||
member_id = ""
|
member_id = ""
|
||||||
name = ""
|
name = ""
|
||||||
dob = ""
|
dob = ""
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.isdigit() and (len(line) <= 14 or len(line) >= 8):
|
|
||||||
member_id = line
|
|
||||||
name_lines = []
|
|
||||||
j = i + 1
|
|
||||||
while j < len(lines) and not re.match(r"\d{1,2}/\d{1,2}/\d{4}", lines[j]):
|
|
||||||
name_lines.append(lines[j])
|
|
||||||
j += 1
|
|
||||||
name = " ".join(name_lines).strip()
|
|
||||||
|
|
||||||
if j < len(lines):
|
# 1) Find the first plausible member ID (8–14 digits)
|
||||||
dob = lines[j].strip()
|
id_idx = -1
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if ID_RE.match(line):
|
||||||
|
member_id = line
|
||||||
|
id_idx = i
|
||||||
break
|
break
|
||||||
|
|
||||||
return {
|
if id_idx == -1:
|
||||||
|
return jsonify({"memberId": "", "name": "", "dob": ""})
|
||||||
|
|
||||||
|
# 2) Scan forward to collect name + DOB; handle both same-line and next-line cases
|
||||||
|
collected = []
|
||||||
|
j = id_idx + 1
|
||||||
|
while j < len(lines):
|
||||||
|
low = lines[j].lower()
|
||||||
|
if any(sw in low for sw in STOP_WORDS):
|
||||||
|
break
|
||||||
|
collected.append(lines[j])
|
||||||
|
# If we already found a DOB, we can stop early
|
||||||
|
if DOB_RE.search(lines[j]):
|
||||||
|
break
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
# Flatten the collected chunk to search for a date (works if DOB is on same line or next)
|
||||||
|
blob = " ".join(collected).strip()
|
||||||
|
|
||||||
|
m = DOB_RE.search(blob)
|
||||||
|
if m:
|
||||||
|
dob = m.group(0)
|
||||||
|
# name is everything before the date within the same blob
|
||||||
|
name = blob[:m.start()].strip()
|
||||||
|
else:
|
||||||
|
# fallback: if we didn't find a date, assume first collected line(s) are name
|
||||||
|
name = blob
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
"memberId": member_id,
|
"memberId": member_id,
|
||||||
"name": name,
|
"name": name,
|
||||||
"dob": dob
|
"dob": dob
|
||||||
}
|
})
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(port=5001)
|
app.run(port=5001)
|
||||||
|
|||||||
Reference in New Issue
Block a user