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:
23
apps/Frontend/src/hooks/use-extractPdfData.js
Normal file
23
apps/Frontend/src/hooks/use-extractPdfData.js
Normal 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",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
55
apps/Frontend/src/hooks/use-job-status.js
Normal file
55
apps/Frontend/src/hooks/use-job-status.js
Normal 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 };
|
||||
}
|
||||
144
apps/Frontend/src/hooks/use-toast.js
Normal file
144
apps/Frontend/src/hooks/use-toast.js
Normal 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 };
|
||||
Reference in New Issue
Block a user