Use NCR business number in URLs (/ncrs/NCR-2026-0021)

Detail routes, queue navigation, and notification email links now use the
business identifier instead of the database id — friendlier for end users
quoting NCR numbers. The API resolves both forms (case-insensitive number,
or legacy numeric id) so existing bookmarks and email links keep working.
Adds regression tests incl. a guard that /ncrs/export.csv isn't shadowed
by the path parameter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ang3l12
2026-07-28 11:15:16 -06:00
parent 12383b2225
commit 8d48f774bb
7 changed files with 102 additions and 44 deletions

View File

@@ -54,11 +54,13 @@ export function useQueue(queue: string, filters: QueueFilters, page: number, pag
});
}
export function useNcr(id: number | undefined) {
/** `ref` is the NCR business number (NCR-2026-0021) or, for older links,
* the numeric database id — the API resolves both. */
export function useNcr(ref: string | undefined) {
return useQuery({
queryKey: ["ncr", id],
queryFn: () => api<NcrDetail>(`/api/ncrs/${id}`),
enabled: id !== undefined,
queryKey: ["ncr", ref],
queryFn: () => api<NcrDetail>(`/api/ncrs/${encodeURIComponent(ref!)}`),
enabled: !!ref,
});
}
@@ -97,7 +99,9 @@ export function useNcrMutation<TVars>(
return useMutation({
mutationFn,
onSuccess: (data) => {
qc.setQueryData(["ncr", data.ncr.id], data.ncr);
// The detail page may be keyed by business number or legacy id.
qc.setQueryData(["ncr", data.ncr.ncr_number], data.ncr);
qc.setQueryData(["ncr", String(data.ncr.id)], data.ncr);
qc.invalidateQueries({ queryKey: ["ncrs"] });
qc.invalidateQueries({ queryKey: ["ncr-audit", data.ncr.id] });
if (data.warnings.length && onWarnings) onWarnings(data.warnings);

View File

@@ -72,7 +72,7 @@ export function QueueTable({ items, total, page, pageSize, onPageChange, loading
<Stack spacing={1}>
{items.map((n) => (
<Card key={n.id} variant="outlined">
<CardActionArea onClick={() => navigate(`/ncrs/${n.id}`)}>
<CardActionArea onClick={() => navigate(`/ncrs/${n.ncr_number}`)}>
<CardContent sx={{ py: 1.5 }}>
<Stack
direction="row"
@@ -121,7 +121,7 @@ export function QueueTable({ items, total, page, pageSize, onPageChange, loading
key={n.id}
hover
sx={{ cursor: "pointer" }}
onClick={() => navigate(`/ncrs/${n.id}`)}
onClick={() => navigate(`/ncrs/${n.ncr_number}`)}
>
<TableCell sx={{ fontWeight: 700 }}>{n.ncr_number}</TableCell>
<TableCell>{n.job_number}</TableCell>

View File

@@ -325,9 +325,10 @@ function DetailBody({ ncr }: { ncr: NcrDetail }) {
}
export function NcrDetailPage() {
// URL carries the NCR business number (e.g. /ncrs/NCR-2026-0021); numeric
// ids from older links still resolve server-side.
const { id } = useParams();
const ncrId = Number(id);
const ncrQuery = useNcr(Number.isFinite(ncrId) ? ncrId : undefined);
const ncrQuery = useNcr(id);
const me = useMe();
const { toast } = useToast();
const [tab, setTab] = useState(0);

View File

@@ -98,7 +98,7 @@ export function NewNcrPage() {
</Box>
<Stack direction="row" spacing={1} justifyContent="center" sx={{ mt: 3 }}>
<Button variant="contained" onClick={() => navigate(`/ncrs/${created.id}`)}>
<Button variant="contained" onClick={() => navigate(`/ncrs/${created.ncr_number}`)}>
View NCR
</Button>
<Button