Files
Assclaw f852e7c941
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped
chore: standardize security and quality foundations
2026-09-17 20:03:47 +12:00

122 lines
4.0 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { authFetch, getApiBase, clearToken } from "../../../lib/auth";
import ResolutionChoice from "../../../ui/ResolutionChoice";
type Issue = {
id: number;
kind: string;
title: string;
status: string;
permissions?: { can_confirm_resolution?: boolean };
};
export default function ConfirmIssuePage() {
const { id } = useParams<{ id: string }>();
const router = useRouter();
const [item, setItem] = useState<Issue | null>(null);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [result, setResult] = useState("");
const login = useCallback(() => {
clearToken();
router.replace(`/login?next=${encodeURIComponent(`/issues/confirm/${id}`)}`);
}, [id, router]);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setItem(null);
setError("");
setResult("");
const load = async () => {
try {
const response = await authFetch(`${getApiBase()}/portal/items/${id}`, {
signal: controller.signal,
cache: "no-store",
});
if (response.status === 401) {
login();
return;
}
if (!response.ok)
throw new Error("This issue is unavailable. Please sign in with the account that reported it.");
const data = await response.json();
if (data.item?.kind !== "issue") throw new Error("This link does not belong to an issue.");
setItem(data.item);
} catch (err) {
if (!controller.signal.aborted)
setError(err instanceof Error ? err.message : "Could not load this issue. Please try again.");
} finally {
if (!controller.signal.aborted) setLoading(false);
}
};
void load();
return () => controller.abort();
// The confirmation link identifies one issue. Never submit an answer on GET.
}, [id, login]);
const answer = async (resolved: boolean) => {
if (busy) return;
setBusy(true);
setError("");
try {
const response = await authFetch(`${getApiBase()}/portal/issues/${id}/resolution-response`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ resolved }),
});
if (response.status === 401) {
login();
return;
}
if (!response.ok)
throw new Error(
"Your answer could not be saved. This issue may already have been answered. Refresh to check before trying again.",
);
setResult(
resolved
? "Thanks! Your issue is now closed."
: "Thanks for letting us know. Your issue stays open for another look.",
);
} catch (err) {
setError(err instanceof Error ? err.message : "Could not save your answer. Please try again.");
} finally {
setBusy(false);
}
};
return (
<main className="resolution-response-page">
{error && (
<p role="alert" className="status-banner">
{error}
</p>
)}
{loading ? (
<p role="status">Loading your issue</p>
) : result ? (
<section className="resolution-choice" role="status">
<h2>{result}</h2>
<a href="/portal/issues">Back to issues</a>
</section>
) : item ? (
item.status === "awaiting_confirmation" && item.permissions?.can_confirm_resolution ? (
<ResolutionChoice title={item.title} busy={busy} onAnswer={(value) => void answer(value)} />
) : (
<section className="resolution-choice">
<h2>
{item.status === "awaiting_confirmation"
? "This question is for the person who reported the issue."
: "No answer is needed right now."}
</h2>
<p>{item.title}</p>
<a href={`/portal/issues?item=${item.id}`}>View issue</a>
</section>
)
) : null}
</main>
);
}