feat: United/DentalHub + Tufts SCO pre-auth, cloud storage search & file thumbnails

- Add United/DentalHub pre-authorization Selenium worker, helpers, backend route/processor/client, and frontend OTP modal + wired button
- Add Tufts SCO pre-authorization Selenium worker, helpers, backend route/processor/client, and frontend OTP modal + wired button
- Fix btnSubmitAuthorization selector for UnitedDH preauth step2
- Fix Tufts SCO preauth step3 to target <span>pre-authorization</span> button
- Cloud storage search: default to "both" mode so patient folder names match on first search
- Cloud storage folder panel: auto-scroll to panel when opened from search results
- Cloud storage files: show inline image thumbnails and PDF badge in file grid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-13 00:03:58 -04:00
co-authored by Claude Sonnet 4.6
parent fd4feb3e76
commit 3978b16d7d
20 changed files with 4044 additions and 19 deletions
@@ -75,6 +75,51 @@ function fileIcon(mime?: string) {
return <FileIcon className="h-6 w-6" />;
}
function FileThumbnail({ fileId, mime, name }: { fileId: number; mime?: string | null; name?: string | null }) {
const [blobUrl, setBlobUrl] = useState<string | null>(null);
useEffect(() => {
if (!mime?.startsWith("image/")) return;
let url: string | null = null;
apiRequest("GET", `/api/cloud-storage/files/${fileId}/content`)
.then((res) => {
if (!res.ok) throw new Error();
return res.blob();
})
.then((blob) => {
url = URL.createObjectURL(blob);
setBlobUrl(url);
})
.catch(() => {});
return () => { if (url) URL.revokeObjectURL(url); };
}, [fileId, mime]);
if (mime?.startsWith("image/")) {
return blobUrl ? (
<img src={blobUrl} alt={name ?? ""} className="w-full h-full object-cover rounded" />
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400">
<ImageIcon className="h-6 w-6" />
</div>
);
}
if (mime === "application/pdf" || mime?.endsWith("/pdf")) {
return (
<div className="w-full h-full flex flex-col items-center justify-center bg-red-50 rounded gap-1">
<FileText className="h-7 w-7 text-red-500" />
<span className="text-[10px] font-bold text-red-500 tracking-wide">PDF</span>
</div>
);
}
return (
<div className="w-full h-full flex items-center justify-center text-gray-400">
{fileIcon(mime ?? undefined)}
</div>
);
}
export default function FilesSection({
parentId,
pageSize = FILES_LIMIT_DEFAULT,
@@ -388,22 +433,23 @@ export default function FilesSection({
{data.map((file) => (
<div
key={file.id}
className="p-3 rounded border hover:bg-gray-50 cursor-pointer"
className="rounded border hover:border-primary hover:shadow-sm cursor-pointer overflow-hidden transition-all"
onContextMenu={(e) => showMenu(e, file)}
onClick={() => openPreview(file)}
>
<div className="flex flex-col items-center">
<div className="h-10 w-10 text-gray-500 mb-2 flex items-center justify-center">
{fileIcon((file as any).mimeType)}
<div className="h-28 w-full bg-gray-100">
<FileThumbnail
fileId={Number(file.id)}
mime={(file as any).mimeType}
name={file.name}
/>
</div>
<div className="p-2">
<div className="text-sm font-medium truncate" title={file.name ?? ""}>
{file.name}
</div>
<div
className="text-sm truncate text-center"
style={{ maxWidth: 140 }}
>
<div title={file.name}>{file.name}</div>
<div className="text-xs text-gray-400">
{((file as any).fileSize ?? 0).toString()} bytes
</div>
<div className="text-xs text-gray-400">
{((file as any).fileSize ?? 0).toString()} bytes
</div>
</div>
</div>
@@ -53,7 +53,7 @@ export default function CloudSearchBar({
const [q, setQ] = useState("");
const [searchTarget, setSearchTarget] = useState<
"filename" | "foldername" | "both"
>("filename"); // default filename
>("both");
const [typeFilter, setTypeFilter] = useState<
"any" | "images" | "pdf" | "video" | "audio"
>("any");
@@ -265,7 +265,7 @@ export default function CloudSearchBar({
<SelectValue placeholder="Search target" />
</SelectTrigger>
<SelectContent>
<SelectItem value="filename">Filename (default)</SelectItem>
<SelectItem value="filename">Filename only</SelectItem>
<SelectItem value="foldername">
Folder name (top-level)
</SelectItem>