fix(odin): gate Uložit on invoices.create + correct invoice invalidation key

Final review caught two minor issues:
- saveInvoice invalidated ["received-invoices"], which matches no query (the
  received list/stats are keyed ["invoices","received",…]); use ["invoices"]
  so the list/stats actually refresh after an import.
- The Save button hit /received-invoices (requires invoices.create) while Odin
  is gated only on ai.use; disable Uložit (with a hint) when the user lacks
  invoices.create so the UI never offers a 403 action. Dormant today (ai.use is
  admin-only and admin bypasses checks) but correct going forward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-08 19:46:49 +02:00
parent b7ebf04001
commit ac7c220bb1
2 changed files with 16 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ import type { ReviewInvoice, EditableField } from "./types";
interface InvoiceReviewCardProps { interface InvoiceReviewCardProps {
inv: ReviewInvoice; inv: ReviewInvoice;
busy: boolean; busy: boolean;
canSave: boolean;
onChange: (uid: string, field: EditableField, value: string) => void; onChange: (uid: string, field: EditableField, value: string) => void;
onSave: (inv: ReviewInvoice) => void; onSave: (inv: ReviewInvoice) => void;
onDiscard: (uid: string) => void; onDiscard: (uid: string) => void;
@@ -17,6 +18,7 @@ const fieldCols = { xs: "1fr", sm: "1fr 1fr" };
export default function InvoiceReviewCard({ export default function InvoiceReviewCard({
inv, inv,
busy, busy,
canSave,
onChange, onChange,
onSave, onSave,
onDiscard, onDiscard,
@@ -96,7 +98,13 @@ export default function InvoiceReviewCard({
{/* Actions */} {/* Actions */}
<Box sx={{ display: "flex", gap: 1, mt: 1.5 }}> <Box sx={{ display: "flex", gap: 1, mt: 1.5 }}>
<Button onClick={() => onSave(inv)} disabled={busy}> <Button
onClick={() => onSave(inv)}
disabled={busy || !canSave}
title={
!canSave ? "Nemáte oprávnění k uložení přijatých faktur" : undefined
}
>
Uložit Uložit
</Button> </Button>
<Button <Button

View File

@@ -4,6 +4,7 @@ import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import apiFetch from "../../utils/api"; import apiFetch from "../../utils/api";
import { useAlert } from "../../context/AlertContext"; import { useAlert } from "../../context/AlertContext";
import { useAuth } from "../../context/AuthContext";
import { import {
aiUsageOptions, aiUsageOptions,
aiConversationsOptions, aiConversationsOptions,
@@ -61,6 +62,10 @@ function summaryNote(reviews: ReviewInvoice[]): string {
export default function OdinChat() { export default function OdinChat() {
const alert = useAlert(); const alert = useAlert();
const qc = useQueryClient(); const qc = useQueryClient();
const { hasPermission } = useAuth();
// Saving an extracted invoice hits /received-invoices (needs invoices.create);
// Odin itself only needs ai.use, so the Save affordance is gated separately.
const canSave = hasPermission("invoices.create");
const { data: usage } = useQuery(aiUsageOptions()); const { data: usage } = useQuery(aiUsageOptions());
const { data: convData, isPending: convPending } = useQuery( const { data: convData, isPending: convPending } = useQuery(
aiConversationsOptions(), aiConversationsOptions(),
@@ -304,7 +309,7 @@ export default function OdinChat() {
if (!res.ok) throw new Error(body?.error || "Uložení selhalo"); if (!res.ok) throw new Error(body?.error || "Uložení selhalo");
alert.success("Faktura uložena"); alert.success("Faktura uložena");
setReview((r) => r.filter((x) => x.uid !== inv.uid)); setReview((r) => r.filter((x) => x.uid !== inv.uid));
qc.invalidateQueries({ queryKey: ["received-invoices"] }); qc.invalidateQueries({ queryKey: ["invoices"] });
} catch (e) { } catch (e) {
alert.error(e instanceof Error ? e.message : "Uložení selhalo"); alert.error(e instanceof Error ? e.message : "Uložení selhalo");
} finally { } finally {
@@ -445,6 +450,7 @@ export default function OdinChat() {
key={inv.uid} key={inv.uid}
inv={inv} inv={inv}
busy={busy} busy={busy}
canSave={canSave}
onChange={patch} onChange={patch}
onSave={saveInvoice} onSave={saveInvoice}
onDiscard={(uid) => onDiscard={(uid) =>