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
parent adb5801023
commit fcb049273a
16 changed files with 1370 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { useMutation } from "@tanstack/react-query";
import { useToast } from "@/hooks/use-toast";
import { apiRequest } from "@/lib/queryClient";
export default function useExtractPdfData() {
const { toast } = useToast();
return useMutation({
mutationFn: async (pdfFile) => {
const formData = new FormData();
formData.append("pdf", pdfFile);
const res = await apiRequest("POST", "/api/patientDataExtraction/patientdataextract", formData);
if (!res.ok)
throw new Error("Failed to extract PDF");
return res.json();
},
onError: (error) => {
toast({
title: "Error",
description: `Failed to extract PDF: ${error.message}`,
variant: "destructive",
});
},
});
}

View File

@@ -0,0 +1,55 @@
/**
* useJobStatus — tracks a BullMQ job via WebSocket `job:update` events.
*
* Usage:
* const { status, result, error } = useJobStatus(jobId);
*
* The hook listens for `job:update` events emitted by the backend workers.
* When the jobId changes, the previous listener is removed and a fresh one
* is registered for the new job.
*/
import { useEffect, useState } from "react";
import { socket } from "@/lib/socket";
export function useJobStatus(jobId) {
const [status, setStatus] = useState(jobId ? "queued" : null);
const [message, setMessage] = useState("");
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [socketId, setSocketId] = useState(socket.id ?? null);
// Keep socketId in sync with the socket connection
useEffect(() => {
const onConnect = () => setSocketId(socket.id ?? null);
socket.on("connect", onConnect);
if (socket.connected)
setSocketId(socket.id ?? null);
return () => { socket.off("connect", onConnect); };
}, []);
// Reset state when the jobId changes
useEffect(() => {
if (!jobId) {
setStatus(null);
setMessage("");
setResult(null);
setError(null);
return;
}
setStatus("queued");
setMessage("");
setResult(null);
setError(null);
const handler = (payload) => {
if (payload.jobId !== jobId)
return;
setStatus(payload.status);
if (payload.message)
setMessage(payload.message);
if (payload.result !== undefined)
setResult(payload.result);
if (payload.error)
setError(payload.error);
};
socket.on("job:update", handler);
return () => { socket.off("job:update", handler); };
}, [jobId]);
return { status, message, result, error, socketId };
}

View File

@@ -0,0 +1,144 @@
import * as React from "react";
const TOAST_LIMIT = 10;
const TOAST_REMOVE_DELAY = 10000;
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
};
let count = 0;
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}
const toastTimeouts = new Map();
const addToRemoveQueue = (toastId) => {
if (toastTimeouts.has(toastId))
return;
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
});
// Show next toast in the queue
const next = memoryState.toasts[1]; // [0] was just removed
if (next) {
addToRemoveQueue(next.id);
}
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
export const reducer = (state, action) => {
switch (action.type) {
case "ADD_TOAST":
const newToasts = [...state.toasts, action.toast].slice(0, TOAST_LIMIT);
addToRemoveQueue(action.toast.id);
return {
...state,
toasts: newToasts,
};
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t),
};
case "DISMISS_TOAST": {
const { toastId } = action;
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId);
}
else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id);
});
}
return {
...state,
toasts: state.toasts.map((t) => t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
}
};
const listeners = [];
let memoryState = { toasts: [] };
function dispatch(action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}
const shownMessages = new Set();
function toast({ ...props }) {
const id = genId();
// Prevent same message from being shown repeatedly
const messageKey = `${props.title}-${props.description}`;
if (shownMessages.has(messageKey)) {
return {
id,
dismiss: () => { },
update: () => { },
};
}
shownMessages.add(messageKey);
setTimeout(() => shownMessages.delete(messageKey), 3000); // allow to re-show after 3s
const update = (props) => dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open)
dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}
function useToast() {
const [state, setState] = React.useState(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}
export { useToast, toast };