mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 19:22:06 +00:00
Dispatchers see a one-line banner and a one-time toast while a Reactive/Emergency work order is past half of its SEV response window; the banner opens the board on exactly those work orders and dismisses for the session. Missed deadlines stay in the notification center until acknowledged, and Clear all skips them.
90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
import { useEffect, useMemo } from "react";
|
|
import { AlertTriangle, ChevronRight, X } from "lucide-react";
|
|
import { useNavigate } from "react-router";
|
|
import { toast } from "react-toastify";
|
|
import type { SlaAtRiskWorkOrder } from "@/domain/notifications/types/notification-feed";
|
|
import { useNotificationSession } from "@/domain/notifications/session/notification-session-context";
|
|
import { useNotificationFeed } from "@/domain/notifications/use-cases/use-notification-feed";
|
|
import { slaBannerText, slaHalfwayToastText } from "@/domain/notifications/utils/sla-alert-copy";
|
|
import { buildWorkOrderIdsHref } from "@/domain/work-orders/utils/work-order-ids-filter";
|
|
|
|
const NO_WORK_ORDERS: SlaAtRiskWorkOrder[] = [];
|
|
|
|
/** Toasts each at-risk work order once per session, the first time the feed reports it. */
|
|
function useSlaHalfwayToasts(atRisk: SlaAtRiskWorkOrder[]) {
|
|
const { claimSlaToasts } = useNotificationSession();
|
|
|
|
useEffect(() => {
|
|
const claimed = new Set(claimSlaToasts(atRisk.map((workOrder) => workOrder.id)));
|
|
atRisk
|
|
.filter((workOrder) => claimed.has(workOrder.id))
|
|
.forEach((workOrder) =>
|
|
toast.warn(slaHalfwayToastText(workOrder), { toastId: `sla-at-risk-${workOrder.id}` }),
|
|
);
|
|
}, [atRisk, claimSlaToasts]);
|
|
}
|
|
|
|
/**
|
|
* One line at the top of every page while any of the user's Reactive/Emergency work orders is past
|
|
* half of its response window. Clicking it opens Work Orders on exactly those work orders; dismissing
|
|
* hides them for this session, and a work order that becomes at risk later brings the banner back.
|
|
*/
|
|
export function SlaAtRiskBanner() {
|
|
const navigate = useNavigate();
|
|
const { data } = useNotificationFeed();
|
|
const { slaBannerDismissedIds, dismissSlaBanner } = useNotificationSession();
|
|
const atRisk = data?.slaAtRisk ?? NO_WORK_ORDERS;
|
|
useSlaHalfwayToasts(atRisk);
|
|
|
|
const shown = useMemo(
|
|
() => atRisk.filter((workOrder) => !slaBannerDismissedIds.has(workOrder.id)),
|
|
[atRisk, slaBannerDismissedIds],
|
|
);
|
|
if (shown.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const ids = shown.map((workOrder) => workOrder.id);
|
|
return (
|
|
<div
|
|
role="region"
|
|
aria-label="Work orders at risk of missing their response deadline"
|
|
className="flex shrink-0 items-center"
|
|
style={{
|
|
gap: 10,
|
|
padding: "6px 16px",
|
|
background: "var(--destructive)",
|
|
color: "var(--destructive-foreground)",
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate(buildWorkOrderIdsHref(ids))}
|
|
title="View in Work Orders"
|
|
className="flex min-w-0 flex-1 cursor-pointer items-center text-left"
|
|
style={{ color: "inherit" }}
|
|
>
|
|
<span
|
|
className="inline-flex min-w-0 items-center transition-colors hover:bg-white/20"
|
|
style={{ gap: 8, padding: "3px 8px", marginLeft: -8, borderRadius: 4 }}
|
|
>
|
|
<AlertTriangle size={14} className="shrink-0" aria-hidden />
|
|
<span className="truncate" style={{ fontSize: 12.5, fontWeight: 600 }}>
|
|
{slaBannerText(shown)}
|
|
</span>
|
|
<ChevronRight size={14} className="shrink-0" aria-hidden />
|
|
</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => dismissSlaBanner(ids)}
|
|
title="Dismiss"
|
|
aria-label="Dismiss SLA alert"
|
|
className="flex shrink-0 cursor-pointer items-center justify-center rounded transition-colors hover:bg-white/20"
|
|
style={{ width: 22, height: 22, color: "inherit" }}
|
|
>
|
|
<X size={14} aria-hidden />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|