feat: add missing backend route and frontend utility/config files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-25 22:26:58 -04:00
co-authored by Claude Sonnet 4.6
parent adb5801023
commit fcb049273a
16 changed files with 1370 additions and 0 deletions
+245
View File
@@ -0,0 +1,245 @@
/**
* Use parseLocalDate when you need a Date object at local midnight
* (for calendars, date pickers, Date math in the browser).
*
*
* Parse a date string in yyyy-MM-dd format (assumed local) into a JS Date object.
* No timezone conversion is applied. Returns a Date at midnight local time.
*
* * Accepts:
* - "YYYY-MM-DD"
* - ISO/timestamp string (will take left-of-'T' date portion)
* - Date object (will return a new Date set to that local calendar day at midnight)
*/
export function parseLocalDate(input) {
if (input instanceof Date) {
return new Date(input.getFullYear(), input.getMonth(), input.getDate());
}
if (typeof input === "string") {
const dateString = input?.split("T")[0] ?? "";
const parts = dateString.split("-");
const [yearStr, monthStr, dayStr] = parts;
// Validate all parts are defined and valid strings
if (!yearStr || !monthStr || !dayStr) {
throw new Error("Invalid date string format. Expected yyyy-MM-dd.");
}
const year = parseInt(yearStr, 10);
const month = parseInt(monthStr, 10) - 1; // JS Date months are 0-based
const day = parseInt(dayStr, 10);
if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) {
throw new Error("Invalid numeric values in date string.");
}
return new Date(year, month, day);
}
throw new Error("Unsupported input to parseLocalDate. Expected string or Date.");
}
/**
* Use formatLocalDate when you need a date-only string "YYYY-MM-DD" (for displaying stable date values in UI lists,
* sending to APIs, storing in sessionStorage/DB where date-only is required).
*
*
* Format a date value into a "YYYY-MM-DD" string with **no timezone shifts**.
*
* Handles all common input cases:
* - "YYYY-MM-DD" string → returned as-is.
* - ISO/timestamp string → takes the date portion before "T" (safe, no TZ math).
* - Date object:
* - If created via `new Date("2025-07-15T00:00:00Z")` (ISO instant),
* UTC vs local calendar components may differ. In this case, use UTC
* fields so the original calendar day (15th) is preserved across timezones.
* - If created via `parseLocalDate("2025-07-15")` or `new Date(2025, 6, 15)`
* (local-midnight Date), UTC and local calendar components match,
* so local fields are safe to use.
*
* This hybrid logic ensures:
* - DOBs and other date-only values will never appear off by one day
* due to timezone differences.
* - Works with both string and Date inputs without requiring code changes elsewhere.
*/
export function formatLocalDate(input) {
if (!input)
return "";
// Case 1: already "YYYY-MM-DD" string
if (typeof input === "string" && /^\d{4}-\d{2}-\d{2}$/.test(input)) {
return input;
}
// Case 2: ISO/timestamp string -> take the left-of-T portion
if (typeof input === "string") {
const dateString = input.split("T")[0] ?? "";
return dateString;
}
// Case 3: Date object
if (input instanceof Date) {
if (isNaN(input.getTime()))
return "";
// HYBRID LOGIC:
// - If this Date was likely created from an ISO instant at UTC midnight
// (e.g. "2025-10-15T00:00:00Z"), then getUTCHours() === 0 but getHours()
// will be non-zero in most non-UTC timezones. In that case use UTC date
// parts to preserve the original calendar day.
// - Otherwise use the local calendar fields (safe for local-midnight Dates).
const utcHours = input.getUTCHours();
const localHours = input.getHours();
const useUTC = utcHours === 0 && localHours !== 0;
const year = useUTC ? input.getUTCFullYear() : input.getFullYear();
const month = useUTC ? input.getUTCMonth() + 1 : input.getMonth() + 1;
const day = useUTC ? input.getUTCDate() : input.getDate();
const m = `${month}`.padStart(2, "0");
const d = `${day}`.padStart(2, "0");
return `${year}-${m}-${d}`;
}
return "";
}
// ---------- helpers ----------
const MONTH_SHORT = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
function isDateOnlyString(s) {
return /^\d{4}-\d{2}-\d{2}$/.test(s);
}
// ---------- formatDateToHumanReadable ----------
/**
* Frontend-safe human readable formatter.
*
* Rules:
* - If input is a date-only string "YYYY-MM-DD", format it directly (no TZ math).
* - If input is a Date object, use its local calendar fields (getFullYear/getMonth/getDate).
* - If input is any other string (ISO/timestamp), DO NOT call new Date(isoString) directly
* for display. Instead, use parseLocalDate(dateInput) to extract the local calendar day
* (strip time portion) and render that. This prevents off-by-one day drift.
*
* Output example: "Oct 7, 2025"
*/
export function formatDateToHumanReadable(dateInput) {
if (!dateInput)
return "N/A";
// date-only string -> show as-is using MONTH_SHORT
if (typeof dateInput === "string" && isDateOnlyString(dateInput)) {
const [y, m, d] = dateInput.split("-");
if (!y || !m || !d)
return "Invalid Date";
return `${MONTH_SHORT[parseInt(m, 10) - 1]} ${d}, ${y}`;
}
// Date object -> use local calendar fields
if (dateInput instanceof Date) {
if (isNaN(dateInput.getTime()))
return "Invalid Date";
const dd = String(dateInput.getDate());
const mm = MONTH_SHORT[dateInput.getMonth()];
const yy = dateInput.getFullYear();
return `${mm} ${dd}, ${yy}`;
}
// Other string (likely ISO/timestamp) -> normalize via parseLocalDate
// This preserves the calendar day the user expects (no timezone drift).
if (typeof dateInput === "string") {
try {
const localDate = parseLocalDate(dateInput);
const dd = String(localDate.getDate());
const mm = MONTH_SHORT[localDate.getMonth()];
const yy = localDate.getFullYear();
return `${mm} ${dd}, ${yy}`;
}
catch (err) {
console.error("Invalid date input provided:", dateInput, err);
return "Invalid Date";
}
}
return "Invalid Date";
}
// ---------------- OCR Date helper --------------------------
/**
* Convert any OCR numeric-ish value into a number.
* Handles string | number | null | undefined gracefully.
*/
export function toNum(val) {
if (val == null || val === "")
return 0;
if (typeof val === "number")
return val;
const parsed = Number(val);
return isNaN(parsed) ? 0 : parsed;
}
/**
* Convert any OCR string-like value into a safe string.
*/
export function toStr(val) {
if (val == null)
return "";
return String(val).trim();
}
/**
* Convert OCR date strings like "070825" (MMDDYY) into a JS Date object.
* Example: "070825" → 2025-08-07.
*/
export function convertOCRDate(input) {
const raw = toStr(input);
if (!/^\d{6}$/.test(raw)) {
throw new Error(`Invalid OCR date format: ${raw}`);
}
const month = parseInt(raw.slice(0, 2), 10) - 1;
const day = parseInt(raw.slice(2, 4), 10);
const year2 = parseInt(raw.slice(4, 6), 10);
const year = year2 < 50 ? 2000 + year2 : 1900 + year2;
return new Date(year, month, day);
}
/**
* Format a Date or date string into "HH:mm" (24-hour) string.
*
* Options:
* - By default, hours/minutes are taken in local time.
* - Pass { asUTC: true } to format using UTC hours/minutes.
*
* Examples:
* formatLocalTime(new Date(2025, 6, 15, 9, 5)) → "09:05"
* formatLocalTime("2025-07-15") → "00:00"
* formatLocalTime("2025-07-15T14:30:00Z") → "20:30" (in +06:00)
* formatLocalTime("2025-07-15T14:30:00Z", { asUTC:true }) → "14:30"
*/
export function formatLocalTime(d, opts = {}) {
if (!d)
return "";
const { asUTC = false } = opts;
const pad2 = (n) => n.toString().padStart(2, "0");
let dateObj;
if (d instanceof Date) {
if (isNaN(d.getTime()))
return "";
dateObj = d;
}
else if (typeof d === "string") {
const raw = d.trim();
const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(raw);
if (isDateOnly) {
// Parse yyyy-MM-dd safely as local midnight
try {
dateObj = parseLocalDate(raw);
}
catch {
dateObj = new Date(raw); // fallback
}
}
else {
// For full ISO/timestamp strings, let Date handle TZ
dateObj = new Date(raw);
}
if (isNaN(dateObj.getTime()))
return "";
}
else {
return "";
}
const hours = asUTC ? dateObj.getUTCHours() : dateObj.getHours();
const minutes = asUTC ? dateObj.getUTCMinutes() : dateObj.getMinutes();
return `${pad2(hours)}:${pad2(minutes)}`;
}
@@ -0,0 +1,21 @@
export function getPageNumbers(current, total, maxButtons = 7) {
const pages = [];
if (total <= maxButtons) {
for (let i = 1; i <= total; i++)
pages.push(i);
return pages;
}
const delta = 2;
const start = Math.max(2, current - delta);
const end = Math.min(total - 1, current + delta);
pages.push(1);
if (start > 2)
pages.push("...");
for (let i = start; i <= end; i++)
pages.push(i);
if (end < total - 1)
pages.push("...");
if (total > 1)
pages.push(total);
return pages;
}
+351
View File
@@ -0,0 +1,351 @@
export const PROCEDURE_COMBOS = {
childRecall: {
id: "childRecall",
label: "Child Recall",
codes: ["D0120", "D1120", "D0272", "D1208"],
},
childRecallDirect: {
id: "childRecallDirect",
label: "Child Recall Direct(no x-ray)",
codes: ["D0120", "D1120", "D1208"],
},
childRecallDirect2BW: {
id: "childRecallDirect2BW",
label: "Child Recall Direct 2BW",
codes: ["D0120", "D1120", "D1208", "D0272"],
},
childRecallDirect4BW: {
id: "childRecallDirect4BW",
label: "Child Recall Direct 4BW",
codes: ["D0120", "D1120", "D1208", "D0274"],
},
childRecallDirect2PA2BW: {
id: "childRecallDirect2PA2BW",
label: "Child Recall Direct 2PA 2BW",
codes: ["D0120", "D1120", "D1208", "D0220", "D0230", "D0272"],
toothNumbers: [null, null, null, "9", "24", null], // only these two need values
},
childRecallDirect2PA4BW: {
id: "childRecallDirect2PA4BW",
label: "Child Recall Direct 2PA 4BW",
codes: ["D0120", "D1120", "D1208", "D0220", "D0230", "D0274"],
toothNumbers: [null, null, null, "9", "24", null], // only these two need values
},
childRecallDirect3PA2BW: {
id: "childRecallDirect3PA2BW",
label: "Child Recall Direct 3PA 2BW",
codes: [
"D0120", // exam
"D1120", // prophy
"D1208", // fluoride
"D0220",
"D0230",
"D0230", // extra PA
"D0272", // 2BW
],
},
childRecallDirect4PA: {
id: "childRecallDirect4PA",
label: "Child Recall Direct 4PA",
codes: ["D0120", "D1120", "D1208", "D0220", "D0230", "D0230", "D0230"],
},
childRecallDirect3PA: {
id: "childRecallDirect3PA",
label: "Child Recall Direct 3PA",
codes: ["D0120", "D1120", "D1208", "D0220", "D0230", "D0230"],
},
childRecallDirectPANO: {
id: "childRecallDirectPANO",
label: "Child Recall Direct PANO",
codes: ["D0120", "D1120", "D1208", "D0330"],
},
adultRecall: {
id: "adultRecall",
label: "Adult Recall",
codes: ["D0120", "D0220", "D0230", "D0274", "D1110"],
toothNumbers: [null, "9", "24", null, null], // only these two need values
},
adultRecallDirect: {
id: "adultRecallDirect",
label: "Adult Recall Direct(no x-ray)",
codes: ["D0120", "D1110"],
},
adultRecallDirect2BW: {
id: "adultRecallDirect2BW",
label: "Adult Recall Direct - 2bw (no x-ray)",
codes: ["D0120", "D1110", "D0272"],
},
adultRecallDirect4BW: {
id: "adultRecallDirect4BW",
label: "Adult Recall Direct - 4bw (no x-ray)",
codes: ["D0120", "D1110", "D0274"],
},
adultRecallDirect2PA2BW: {
id: "adultRecallDirect2PA2BW",
label: "Adult Recall Direct - 2PA 2BW",
codes: ["D0120", "D0220", "D0230", "D0272", "D1110"],
toothNumbers: [null, "9", "24", null, null], // only these two need values
},
adultRecallDirect2PA4BW: {
id: "adultRecallDirect2PA4BW",
label: "Adult Recall Direct - 2PA 4BW",
codes: ["D0120", "D0220", "D0230", "D0274", "D1110"],
toothNumbers: [null, "9", "24", null, null], // only these two need values
},
adultRecallDirect4PA: {
id: "adultRecallDirect4PA",
label: "Adult Recall Direct 4PA",
codes: ["D0120", "D1110", "D0220", "D0230", "D0230", "D0230"],
},
adultRecallDirectPano: {
id: "adultRecallDirectPano",
label: "Adult Recall Direct - PANO",
codes: ["D0120", "D1110", "D0330"],
},
newChildPatient: {
id: "newChildPatient",
label: "New Child Patient",
codes: ["D0150", "D1120", "D1208"],
},
newAdultPatientPano: {
id: "newAdultPatientPano",
label: "New Adult Patient - PANO",
codes: ["D0150", "D0330", "D1110"],
},
newAdultPatientFMX: {
id: "newAdultPatientFMX",
label: "New Adult Patient (FMX)",
codes: ["D0150", "D0210", "D1110"],
},
newPatientLimitedPano: {
id: "newPatientLimitedPano",
label: "Patient (Limited exam+Pano)",
codes: ["D0140", "D0330"],
},
newAdultPatientLimited1PA: {
id: "newAdultPatientLimited1PA",
label: "Patient (Limited exam+1PA)",
codes: ["D0140", "D0220"],
},
patientD9110_1PA: {
id: "patientD9110_1PA",
label: "Patient (D9110+1PA)",
codes: ["D9110", "D0220"],
},
//Compostie
oneSurfCompFront: {
id: "oneSurfCompFront",
label: "One Surface Composite (Front)",
codes: ["D2330"],
},
oneSurfCompBack: {
id: "oneSurfCompBack",
label: "One Surface Composite (Back)",
codes: ["D2391"],
},
twoSurfCompFront: {
id: "twoSurfCompFront",
label: "Two Surface Composite (Front)",
codes: ["D2331"],
},
twoSurfCompBack: {
id: "twoSurfCompBack",
label: "Two Surface Composite (Back)",
codes: ["D2392"],
},
threeSurfCompFront: {
id: "threeSurfCompFront",
label: "Three Surface Composite (Front)",
codes: ["D2332"],
},
threeSurfCompBack: {
id: "threeSurfCompBack",
label: "Three Surface Composite (Back)",
codes: ["D2393"],
},
fourSurfCompFront: {
id: "fourSurfCompFront",
label: "Four Surface Composite (Front)",
codes: ["D2335"],
},
fourSurfCompBack: {
id: "fourSurfCompBack",
label: "Four Surface Composite (Back)",
codes: ["D2394"],
},
// Pedo
pedoSealants: {
id: "pedoSealants",
label: "Sealants",
codes: ["D1351"],
},
pedoPulpotomy: {
id: "pedoPulpotomy",
label: "Pulpotomy",
codes: ["D3220"],
},
pedoSSCrown: {
id: "pedoSSCrown",
label: "Stainless Steel Crown",
codes: ["D2930"],
},
// Dentures / Partials
fu: {
id: "fu",
label: "FU",
codes: ["D5110"],
},
fl: {
id: "fl",
label: "FL",
codes: ["D5120"],
},
puResin: {
id: "puResin",
label: "PU (Resin)",
codes: ["D5211"],
},
puCast: {
id: "puCast",
label: "PU (Cast)",
codes: ["D5213"],
},
plResin: {
id: "plResin",
label: "PL (Resin)",
codes: ["D5212"],
},
plCast: {
id: "plCast",
label: "PL (Cast)",
codes: ["D5214"],
},
// Endodontics
rctAnterior: {
id: "rctAnterior",
label: "RCT Anterior",
codes: ["D3310"],
},
rctPremolar: {
id: "rctPremolar",
label: "RCT PreM",
codes: ["D3320"],
},
rctMolar: {
id: "rctMolar",
label: "RCT Molar",
codes: ["D3330"],
},
postCore: {
id: "postCore",
label: "Post/Core",
codes: ["D2954"],
},
coreBU: {
id: "coreBU",
label: "Core BU",
codes: ["D2950"],
},
// Prostho / Perio / Oral Surgery
crown: {
id: "crown",
label: "Crown",
codes: ["D2740"],
},
deepCleaning: {
id: "deepCleaning",
label: "Deep Cleaning",
codes: ["D4341"],
},
simpleExtraction: {
id: "simpleExtraction",
label: "Simple EXT",
codes: ["D7140"],
},
surgicalExtraction: {
id: "surgicalExtraction",
label: "Surg EXT",
codes: ["D7210"],
},
babyTeethExtraction: {
id: "babyTeethExtraction",
label: "Baby Teeth EXT",
codes: ["D7111"],
},
fullBonyExtraction: {
id: "fullBonyExtraction",
label: "Full Bony EXT",
codes: ["D7240"],
},
// Orthodontics
orthPreExamDirect: {
id: "orthPreExamDirect",
label: "Direct Pre-Orth Exam",
codes: ["D9310"],
},
orthRecordDirect: {
id: "orthRecordDirect",
label: "Direct Orth Record",
codes: ["D8660"],
},
orthPerioVisitDirect: {
id: "orthPerioVisitDirect",
label: "Direct Perio Orth Visit ",
codes: ["D8670"],
},
orthRetentionDirect: {
id: "orthRetentionDirect",
label: "Direct Orth Retention",
codes: ["D8680"],
},
orthPA: {
id: "orthPA",
label: "Orth PA",
codes: ["D8080", "D8670", "D8660"],
},
// add more…
};
// Which combos appear under which heading
export const COMBO_CATEGORIES = {
"Recalls & New Patients": [
"childRecall",
"adultRecall",
"newChildPatient",
"newAdultPatientPano",
"newAdultPatientFMX",
"newPatientLimitedPano",
"newAdultPatientLimited1PA",
"patientD9110_1PA",
],
"Composite Fillings (Front)": [
"oneSurfCompFront",
"twoSurfCompFront",
"threeSurfCompFront",
"fourSurfCompFront",
],
"Composite Fillings (Back)": [
"oneSurfCompBack",
"twoSurfCompBack",
"threeSurfCompBack",
"fourSurfCompBack",
],
Pedo: ["pedoSealants", "pedoPulpotomy", "pedoSSCrown"],
"Dentures / Partials (>21 price)": [
"fu",
"fl",
"puResin",
"puCast",
"plResin",
"plCast",
],
Endodontics: ["rctAnterior", "rctPremolar", "rctMolar", "postCore", "coreBU"],
Prosthodontics: ["crown"],
Periodontics: ["deepCleaning"],
Extractions: [
"simpleExtraction",
"surgicalExtraction",
"babyTeethExtraction",
"fullBonyExtraction",
],
Orthodontics: ["orthPA"],
};