Adds a new "ההחלטה" tab to the case detail page showing all 12 decision
blocks with rendered markdown content and inline editing that saves back
to the DB via two new FastAPI endpoints.
Backend (web/app.py):
- GET /api/cases/{n}/decision-blocks — returns all 12 blocks (empty
ones included) merged from BLOCK_CONFIG + decision_blocks table.
Exposes source_of_truth ("docx"|"blocks") and active_draft_path.
- PUT /api/cases/{n}/decision-blocks/{block_id} — inline save via
block_writer.save_block_content; warns (does not block) when an
active DOCX draft exists.
Frontend:
- src/lib/api/decision-blocks.ts — typed hooks (useDecisionBlocks,
useSaveBlock) following the cases.ts hand-written-module pattern.
- src/components/cases/decision-blocks-panel.tsx — accordion of 12
blocks; view mode renders Markdown component; edit mode is a textarea
with on-blur save (derived from ChairEditor pattern, setState-during-
render for re-sync to avoid effect cascade).
- BLOCK_LABELS in feedback.ts extended from 7 → 12 blocks.
- cases/[caseNumber]/page.tsx — new "ההחלטה" tab wired to the panel.
No DB migration required — decision_blocks + active_draft_path exist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
/**
|
||
* Chair feedback hooks — recording and managing Dafna's feedback on drafts.
|
||
*/
|
||
|
||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { apiRequest } from "./client";
|
||
|
||
export type FeedbackCategory =
|
||
| "missing_content"
|
||
| "wrong_tone"
|
||
| "wrong_structure"
|
||
| "factual_error"
|
||
| "style"
|
||
| "other";
|
||
|
||
export type ChairFeedback = {
|
||
id: string;
|
||
case_id: string | null;
|
||
case_number: string;
|
||
block_id: string;
|
||
category: FeedbackCategory;
|
||
feedback_text: string;
|
||
lesson_extracted: string;
|
||
resolved: boolean;
|
||
applied_to: string[];
|
||
created_at: string | null;
|
||
};
|
||
|
||
export type CreateFeedbackInput = {
|
||
case_number?: string;
|
||
block_id?: string;
|
||
feedback_text: string;
|
||
category?: FeedbackCategory;
|
||
lesson_extracted?: string;
|
||
};
|
||
|
||
const feedbackKeys = {
|
||
all: ["feedback"] as const,
|
||
list: (filters: { category?: string; unresolved_only?: boolean }) =>
|
||
[...feedbackKeys.all, "list", filters] as const,
|
||
};
|
||
|
||
export function useFeedbackList(filters: {
|
||
category?: string;
|
||
unresolved_only?: boolean;
|
||
} = {}) {
|
||
const params = new URLSearchParams();
|
||
if (filters.category) params.set("category", filters.category);
|
||
if (filters.unresolved_only) params.set("unresolved_only", "true");
|
||
const qs = params.toString();
|
||
|
||
return useQuery({
|
||
queryKey: feedbackKeys.list(filters),
|
||
queryFn: ({ signal }) =>
|
||
apiRequest<ChairFeedback[]>(`/api/feedback${qs ? `?${qs}` : ""}`, { signal }),
|
||
});
|
||
}
|
||
|
||
/** 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({
|
||
mutationFn: (data: CreateFeedbackInput) =>
|
||
apiRequest<{ id: string; status: string }>("/api/feedback/json", {
|
||
method: "POST",
|
||
body: data,
|
||
}),
|
||
onSuccess: () => {
|
||
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
||
},
|
||
});
|
||
}
|
||
|
||
export function useResolveFeedback() {
|
||
const qc = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: ({
|
||
feedbackId,
|
||
applied_to,
|
||
}: {
|
||
feedbackId: string;
|
||
applied_to: string[];
|
||
}) =>
|
||
apiRequest<{ status: string }>(
|
||
`/api/feedback/${feedbackId}/resolve`,
|
||
{ method: "PATCH", body: { applied_to } },
|
||
),
|
||
onSuccess: () => {
|
||
qc.invalidateQueries({ queryKey: feedbackKeys.all });
|
||
},
|
||
});
|
||
}
|
||
|
||
/** Hebrew labels for feedback categories */
|
||
export const CATEGORY_LABELS: Record<FeedbackCategory, string> = {
|
||
missing_content: "תוכן חסר",
|
||
wrong_tone: "טון שגוי",
|
||
wrong_structure: "מבנה שגוי",
|
||
factual_error: "שגיאה עובדתית",
|
||
style: "סגנון",
|
||
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-alef": "א — כותרת מוסדית",
|
||
"block-bet": "ב — הרכב הוועדה",
|
||
"block-gimel": "ג — צדדים",
|
||
"block-dalet": "ד — החלטה",
|
||
"block-he": "ה — פתיחה",
|
||
"block-vav": "ו — רקע עובדתי",
|
||
"block-zayin": "ז — טענות הצדדים",
|
||
"block-chet": "ח — הליכים",
|
||
"block-tet": "ט — תכניות חלות",
|
||
"block-yod": "י — דיון והכרעה",
|
||
"block-yod-alef": "יא — סיכום",
|
||
"block-yod-bet": "יב — חתימות",
|
||
};
|