feat: add attachment upload to Edit Appointment form (disk, scanned, screenshot with crop)
Lets staff attach files to an appointment directly from the edit form via local disk upload, picking a previously-saved scanned attachment, or an inline screenshot with crop-before-save (matching the Copy Agent page's crop UX), instead of detouring through the Copy Agent page. Also fixes a latent data-loss bug in claim-form's save-for-appointment call, which replaced all AppointmentFile rows instead of merging with existing ones. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
// Minimal single-shot screen capture, extracted from the batching/cropping logic in
|
||||
// ai-copy-agent-page.tsx — this just needs "take a screenshot, attach it."
|
||||
export function useScreenCapture() {
|
||||
const [isCapturing, setIsCapturing] = useState(false);
|
||||
const [countdown, setCountdown] = useState<number | null>(null);
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
|
||||
const stopStream = () => {
|
||||
streamRef.current?.getTracks().forEach((track) => track.stop());
|
||||
streamRef.current = null;
|
||||
};
|
||||
|
||||
// Resolves with a PNG data URL of the full screenshot (caller crops before converting to a File).
|
||||
async function capture(): Promise<string> {
|
||||
setIsCapturing(true);
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: { displaySurface: "monitor" },
|
||||
});
|
||||
streamRef.current = stream;
|
||||
|
||||
const video = document.createElement("video");
|
||||
video.srcObject = stream;
|
||||
video.muted = true;
|
||||
await video.play();
|
||||
await new Promise<void>((resolve) => {
|
||||
if (video.readyState >= 2) resolve();
|
||||
else video.onloadeddata = () => resolve();
|
||||
});
|
||||
|
||||
// Short countdown so the user has time to alt-tab to the window they want to capture.
|
||||
for (let secondsLeft = 2; secondsLeft > 0; secondsLeft--) {
|
||||
setCountdown(secondsLeft);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
setCountdown(null);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas.getContext("2d")?.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
stopStream();
|
||||
|
||||
return canvas.toDataURL("image/png");
|
||||
} catch (error) {
|
||||
stopStream();
|
||||
const name = error instanceof Error ? error.name : "";
|
||||
const message =
|
||||
name === "NotAllowedError"
|
||||
? "Screen share permission was denied or the picker was cancelled."
|
||||
: name === "NotFoundError"
|
||||
? "No screen/window source was available to capture."
|
||||
: !navigator.mediaDevices?.getDisplayMedia
|
||||
? "Screen capture isn't supported in this browser."
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "Screen capture failed.";
|
||||
throw new Error(message);
|
||||
} finally {
|
||||
setIsCapturing(false);
|
||||
setCountdown(null);
|
||||
}
|
||||
}
|
||||
|
||||
return { capture, isCapturing, countdown };
|
||||
}
|
||||
Reference in New Issue
Block a user