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

@@ -13,6 +13,7 @@ import { WorkflowTimeline } from "@/components/cases/workflow-timeline";
import { StatusGuide } from "@/components/cases/status-guide";
import { StatusChanger } from "@/components/cases/status-changer";
import { DocumentsPanel } from "@/components/cases/documents-panel";
import { DraftsPanel } from "@/components/cases/drafts-panel";
import { UploadSheet } from "@/components/documents/upload-sheet";
import { expectedOutcomes } from "@/lib/schemas/case";
import { useCase } from "@/lib/api/cases";
@@ -78,6 +79,9 @@ export default function CaseDetailPage({
</span>
)}
</TabsTrigger>
<TabsTrigger value="drafts">
טיוטות והערות
</TabsTrigger>
</TabsList>
<UploadSheet caseNumber={caseNumber} />
</div>
@@ -115,6 +119,13 @@ export default function CaseDetailPage({
<TabsContent value="documents" className="mt-5">
<DocumentsPanel data={data} />
</TabsContent>
<TabsContent value="drafts" className="mt-5">
<DraftsPanel
caseNumber={caseNumber}
status={data?.status}
/>
</TabsContent>
</Tabs>
</CardContent>
</Card>

View File

@@ -1,329 +0,0 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
useFeedbackList,
useCreateFeedback,
useResolveFeedback,
CATEGORY_LABELS,
BLOCK_LABELS,
type FeedbackCategory,
} from "@/lib/api/feedback";
import { toast } from "sonner";
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",
};
export default function FeedbackPage() {
const [showResolved, setShowResolved] = useState(false);
const [filterCategory, setFilterCategory] = useState<string>("");
const { data: feedbacks, isLoading } = useFeedbackList({
category: filterCategory || undefined,
unresolved_only: !showResolved,
});
const resolveMutation = useResolveFeedback();
function handleResolve(id: string) {
resolveMutation.mutate(
{ feedbackId: id, applied_to: [] },
{
onSuccess: () => toast.success("ההערה סומנה כמטופלת"),
onError: () => toast.error("שגיאה בעדכון"),
},
);
}
return (
<AppShell>
<section className="space-y-6">
<header>
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">
בית
</Link>
<span aria-hidden> &middot; </span>
<span className="text-navy">הערות יו״ר</span>
</nav>
<h1 className="text-navy mb-0">הערות יו״ר על טיוטות</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl">
תיעוד הערות דפנה על טיוטות החלטות. כל הערה מנותחת ומשפיעה על שיפור
כתיבת ההחלטות העתידיות.
</p>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{/* Toolbar */}
<div className="flex items-center gap-3 flex-wrap">
<NewFeedbackDialog />
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
className="rounded-md border border-rule bg-surface px-3 py-1.5 text-sm"
>
<option value="">כל הקטגוריות</option>
{Object.entries(CATEGORY_LABELS).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
<label className="flex items-center gap-2 text-sm text-ink-muted cursor-pointer">
<input
type="checkbox"
checked={showResolved}
onChange={(e) => setShowResolved(e.target.checked)}
className="rounded border-rule"
/>
הצג גם מטופלות
</label>
{feedbacks && (
<span className="text-sm text-ink-muted me-auto">
{feedbacks.length} הערות
</span>
)}
</div>
{/* Feedback list */}
{isLoading ? (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-8 text-center text-ink-muted">
טוען...
</CardContent>
</Card>
) : !feedbacks?.length ? (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-8 text-center text-ink-muted">
אין הערות{!showResolved ? " פתוחות" : ""}
{filterCategory ? ` בקטגוריה ${CATEGORY_LABELS[filterCategory as FeedbackCategory]}` : ""}
</CardContent>
</Card>
) : (
<div className="space-y-3">
{feedbacks.map((fb) => (
<Card
key={fb.id}
className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-60" : ""}`}
>
<CardHeader className="border-b pb-3">
<div className="flex items-center gap-2 flex-wrap">
<Badge
className={`text-[0.7rem] border ${CATEGORY_COLORS[fb.category]}`}
>
{CATEGORY_LABELS[fb.category]}
</Badge>
<Badge variant="outline" className="text-[0.7rem]">
{BLOCK_LABELS[fb.block_id] ?? fb.block_id}
</Badge>
{fb.case_number && (
<Link
href={`/cases/${fb.case_number}`}
className="text-[0.7rem] text-gold-deep hover:underline"
>
תיק {fb.case_number}
</Link>
)}
{fb.resolved && (
<Badge className="bg-emerald-100 text-emerald-700 text-[0.7rem] border border-emerald-200">
טופל
</Badge>
)}
<span className="text-[0.7rem] text-ink-muted me-auto">
{fb.created_at
? new Date(fb.created_at).toLocaleDateString("he-IL")
: ""}
</span>
</div>
</CardHeader>
<CardContent className="px-6 py-4 space-y-3">
<p className="text-sm leading-relaxed">{fb.feedback_text}</p>
{fb.lesson_extracted && (
<div className="bg-gold/5 border border-gold/20 rounded-md px-4 py-3">
<p className="text-[0.7rem] font-semibold text-gold-deep mb-1">
לקח שהופק:
</p>
<p className="text-sm text-ink-muted leading-relaxed">
{fb.lesson_extracted}
</p>
</div>
)}
{!fb.resolved && (
<div className="flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => handleResolve(fb.id)}
disabled={resolveMutation.isPending}
>
סמן כמטופל
</Button>
</div>
)}
</CardContent>
</Card>
))}
</div>
)}
</section>
</AppShell>
);
}
/* ── New feedback dialog ─────────────────────────────────── */
function NewFeedbackDialog() {
const [open, setOpen] = useState(false);
const createMutation = useCreateFeedback();
const [caseNumber, setCaseNumber] = useState("");
const [blockId, setBlockId] = useState("block-yod");
const [category, setCategory] = useState<FeedbackCategory>("missing_content");
const [feedbackText, setFeedbackText] = useState("");
const [lesson, setLesson] = useState("");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!feedbackText.trim()) return;
createMutation.mutate(
{
case_number: caseNumber || undefined,
block_id: blockId,
feedback_text: feedbackText,
category,
lesson_extracted: lesson || undefined,
},
{
onSuccess: () => {
toast.success("ההערה נרשמה בהצלחה");
setOpen(false);
setCaseNumber("");
setFeedbackText("");
setLesson("");
},
onError: () => toast.error("שגיאה ברישום ההערה"),
},
);
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>+ הערה חדשה</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg" dir="rtl">
<DialogHeader>
<DialogTitle>רישום הערת יו״ר</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-2">
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="fb-case">מספר תיק (אופציונלי)</Label>
<Input
id="fb-case"
value={caseNumber}
onChange={(e) => setCaseNumber(e.target.value)}
placeholder="1130-25"
/>
</div>
<div>
<Label htmlFor="fb-block">בלוק</Label>
<select
id="fb-block"
value={blockId}
onChange={(e) => setBlockId(e.target.value)}
className="w-full rounded-md border border-rule bg-surface px-3 py-2 text-sm"
>
{Object.entries(BLOCK_LABELS).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
</div>
</div>
<div>
<Label htmlFor="fb-category">קטגוריה</Label>
<select
id="fb-category"
value={category}
onChange={(e) => setCategory(e.target.value as FeedbackCategory)}
className="w-full rounded-md border border-rule bg-surface px-3 py-2 text-sm"
>
{Object.entries(CATEGORY_LABELS).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
</div>
<div>
<Label htmlFor="fb-text">ההערה</Label>
<Textarea
id="fb-text"
value={feedbackText}
onChange={(e) => setFeedbackText(e.target.value)}
placeholder="מה דפנה אמרה? מה חסר, מה לא נכון, מה צריך לשנות..."
rows={4}
required
/>
</div>
<div>
<Label htmlFor="fb-lesson">לקח שהופק (אופציונלי)</Label>
<Textarea
id="fb-lesson"
value={lesson}
onChange={(e) => setLesson(e.target.value)}
placeholder="מה למדנו מההערה? מה צריך לשנות במערכת?"
rows={2}
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
>
ביטול
</Button>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? "שומר..." : "שמור הערה"}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}