Files
medassist-ng/frontend/src/hooks/useShare.ts
T
2026-03-14 20:26:17 +01:00

192 lines
5.8 KiB
TypeScript

// =============================================================================
// useShare Hook - Share dialog state and operations
// =============================================================================
import { useCallback, useState } from "react";
import type { Medication } from "../types";
import { withCorrelation } from "../utils/correlation";
import { log } from "../utils/logger";
const SHARE_ALL_VALUE = "all";
export interface UseShareReturn {
showShareDialog: boolean;
sharePeople: string[];
shareSelectedPerson: string;
setShareSelectedPerson: React.Dispatch<React.SetStateAction<string>>;
shareSelectedDays: number;
setShareSelectedDays: React.Dispatch<React.SetStateAction<number>>;
shareGenerating: boolean;
shareLink: string | null;
setShareLink: React.Dispatch<React.SetStateAction<string | null>>;
shareCopied: boolean;
setShareCopied: React.Dispatch<React.SetStateAction<boolean>>;
openShareDialog: (meds: Medication[]) => void;
generateShareLink: () => Promise<void>;
copyShareLink: () => void;
closeShareDialog: () => void;
resetShareDialogState: () => void;
}
export function useShare(): UseShareReturn {
const [showShareDialog, setShowShareDialog] = useState(false);
const [sharePeople, setSharePeople] = useState<string[]>([]);
const [shareSelectedPerson, setShareSelectedPerson] = useState<string>("");
const [shareSelectedDays, setShareSelectedDays] = useState<number>(30);
const [shareGenerating, setShareGenerating] = useState(false);
const [shareLink, setShareLink] = useState<string | null>(null);
const [shareCopied, setShareCopied] = useState(false);
const openShareDialog = useCallback((meds: Medication[]) => {
setShowShareDialog(true);
window.history.pushState({ modal: "share" }, "");
setShareLink(null);
setShareCopied(false);
setShareSelectedPerson("");
setShareSelectedDays(30);
// Include both per-intake assignments and legacy medication-level assignments.
const uniquePeople = [
...new Set(
meds.flatMap((medication) => [
...(medication.intakes
?.map((intake) => intake.takenBy)
.filter((person): person is string => Boolean(person)) ?? []),
...(medication.takenBy || []),
])
),
]
.filter(Boolean)
.sort();
setSharePeople(uniquePeople.length > 0 ? [SHARE_ALL_VALUE, ...uniquePeople] : []);
log.info("[ShareDialog] Opened", { medicationCount: meds.length, personCount: uniquePeople.length });
if (uniquePeople.length > 0) {
setShareSelectedPerson(uniquePeople[0]);
}
}, []);
const generateShareLink = useCallback(async () => {
if (!shareSelectedPerson) {
log.warn("[ShareDialog] Attempted to generate link without selected person");
return;
}
setShareGenerating(true);
setShareCopied(false);
try {
const { correlationId, init } = withCorrelation(
{
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
takenBy: shareSelectedPerson,
scheduleDays: shareSelectedDays,
}),
},
"fe-share"
);
const res = await fetch("/api/share", init);
if (res.ok) {
const data = await res.json();
const fullUrl = `${window.location.origin}/share/${data.token}`;
setShareLink(fullUrl);
log.info("[ShareDialog] Share link ready", {
person: shareSelectedPerson,
days: shareSelectedDays,
reused: Boolean(data.reused),
correlationId,
});
} else {
const err = await res.json();
log.error("[ShareDialog] Failed to generate share link", {
status: res.status,
person: shareSelectedPerson,
error: err.error,
correlationId,
});
alert(err.error || "Failed to generate share link");
}
} catch (error) {
log.error("[ShareDialog] Share link request threw error", { person: shareSelectedPerson, error });
alert("Failed to generate share link");
} finally {
setShareGenerating(false);
}
}, [shareSelectedPerson, shareSelectedDays]);
const copyShareLink = useCallback(() => {
if (shareLink) {
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(shareLink).then(
() => {
setShareCopied(true);
log.debug("[ShareDialog] Share link copied to clipboard");
setTimeout(() => setShareCopied(false), 2000);
},
() => {
// Clipboard API blocked (non-secure context / permissions)
fallbackCopyToClipboard(shareLink);
}
);
} else {
fallbackCopyToClipboard(shareLink);
}
}
function fallbackCopyToClipboard(text: string) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
setShareCopied(true);
log.debug("[ShareDialog] Share link copied via fallback");
setTimeout(() => setShareCopied(false), 2000);
} catch {
log.warn("[ShareDialog] Clipboard copy failed — not in secure context");
} finally {
document.body.removeChild(textarea);
}
}
}, [shareLink]);
const closeShareDialog = useCallback(() => {
if (showShareDialog) {
log.debug("[ShareDialog] Closing dialog");
window.history.back();
}
}, [showShareDialog]);
// Internal function to reset share dialog state (called by popstate handler)
const resetShareDialogState = useCallback(() => {
log.debug("[ShareDialog] Reset dialog state");
setShowShareDialog(false);
setShareLink(null);
setShareCopied(false);
}, []);
return {
showShareDialog,
sharePeople,
shareSelectedPerson,
setShareSelectedPerson,
shareSelectedDays,
setShareSelectedDays,
shareGenerating,
shareLink,
setShareLink,
shareCopied,
setShareCopied,
openShareDialog,
generateShareLink,
copyShareLink,
closeShareDialog,
resetShareDialogState,
};
}