Add drafts & feedback tab to case page, remove global feedback page
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 32s

Move draft management (export DOCX, download, upload revised version, mark
final) and chair feedback into a new "טיוטות והערות" tab on the case detail
page. Remove the standalone /feedback page and its nav link since feedback
is now case-scoped.

Also fix /api/admin/skills 500 error when Paperclip DB is unreachable by
adding a connection timeout and graceful fallback to disk-only skills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 05:55:46 +00:00
parent ce61b88438
commit 140a2e442d
7 changed files with 612 additions and 339 deletions

View File

@@ -0,0 +1,87 @@
/**
* Exports domain hooks — draft DOCX files for a case.
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "./client";
import { casesKeys } from "./cases";
export type ExportFile = {
filename: string;
size: number;
created_at: number;
is_final: boolean;
};
export const exportsKeys = {
all: ["exports"] as const,
list: (caseNumber: string) =>
[...exportsKeys.all, "list", caseNumber] as const,
};
export function useExports(caseNumber: string | undefined) {
return useQuery({
queryKey: exportsKeys.list(caseNumber ?? ""),
queryFn: ({ signal }) =>
apiRequest<ExportFile[]>(`/api/cases/${caseNumber}/exports`, { signal }),
enabled: Boolean(caseNumber),
staleTime: 5_000,
refetchInterval: 5_000,
});
}
export function useExportDocx(caseNumber: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: () =>
apiRequest<{ status: string; path: string; message: string }>(
`/api/cases/${caseNumber}/export-docx`,
{ method: "POST" },
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
},
});
}
export function useUploadDraft(caseNumber: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (file: File) => {
const form = new FormData();
form.append("file", file);
const res = await fetch(`/api/cases/${caseNumber}/exports/upload`, {
method: "POST",
body: form,
});
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: "שגיאה בהעלאה" }));
throw new Error(err.detail ?? "שגיאה בהעלאה");
}
return res.json() as Promise<{
filename: string;
size: number;
version: number;
}>;
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
},
});
}
export function useMarkFinal(caseNumber: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (filename: string) =>
apiRequest<{ final_filename: string; status: string }>(
`/api/cases/${caseNumber}/exports/${filename}/mark-final`,
{ method: "POST" },
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
qc.invalidateQueries({ queryKey: casesKeys.detail(caseNumber) });
},
});
}

View File

@@ -56,6 +56,19 @@ export function useFeedbackList(filters: {
});
}
/** Feedback filtered by case number */
export function useCaseFeedback(caseNumber: string | undefined) {
const params = caseNumber ? `?case_number=${caseNumber}` : "";
return useQuery({
queryKey: [...feedbackKeys.all, "case", caseNumber ?? ""] as const,
queryFn: ({ signal }) =>
apiRequest<ChairFeedback[]>(`/api/feedback${params}`, { signal }),
enabled: Boolean(caseNumber),
staleTime: 5_000,
refetchInterval: 5_000,
});
}
export function useCreateFeedback() {
const qc = useQueryClient();
return useMutation({
@@ -100,6 +113,16 @@ export const CATEGORY_LABELS: Record<FeedbackCategory, string> = {
other: "אחר",
};
/** Tailwind color classes per category */
export const CATEGORY_COLORS: Record<FeedbackCategory, string> = {
missing_content: "bg-amber-100 text-amber-800 border-amber-200",
wrong_tone: "bg-purple-100 text-purple-800 border-purple-200",
wrong_structure: "bg-blue-100 text-blue-800 border-blue-200",
factual_error: "bg-red-100 text-red-800 border-red-200",
style: "bg-emerald-100 text-emerald-800 border-emerald-200",
other: "bg-gray-100 text-gray-800 border-gray-200",
};
/** Block ID labels */
export const BLOCK_LABELS: Record<string, string> = {
"block-he": "ה — פתיחה",