feat(feedback): דף מרכזי /feedback להערות יו"ר + תיקון קישורי מרכז אישורים
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s

- דף /feedback חדש: מאגד את כל הערות chair_feedback מכל התיקים, סינון
  טרם-יושמו/הכל + לפי קטגוריה, כפתור "סמן כיושם" לכל הערה
- מרכז אישורים: כרטיס "הערות יו"ר" קישר ל-/ (חסר תועלת) → עכשיו /feedback
- מרכז אישורים: כרטיס "תיקים שנכשלו ב-QA" — כל תיק במדגם קליקבילי לדף
  התיק, והכרטיס מקשר ישירות לתיק כשיש רק אחד
- ApprovalSample.href אופציונלי; פריטי מדגם נהפכים ל-Link כשיש href
- ניווט: הוספת "הערות יו"ר" לקבוצת work ב-app-shell

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:38:04 +00:00
parent 59ff4e31cf
commit e3e3da09e5
5 changed files with 282 additions and 12 deletions

View File

@@ -72,14 +72,27 @@ function ApprovalCard({ cat }: { cat: ApprovalCategory }) {
<p className="text-[0.85rem] text-emerald-700 mb-0">אין פריטים ממתינים </p>
) : cat.sample && cat.sample.length > 0 ? (
<ul className="space-y-1.5 mt-1">
{cat.sample.map((s, i) => (
<li key={i} className="text-[0.82rem] text-ink leading-snug border-s-2 border-rule ps-2.5">
<span className="line-clamp-2">{s.text || "—"}</span>
{s.source ? (
<span className="text-ink-muted text-[0.72rem]"> · {s.source}</span>
) : null}
</li>
))}
{cat.sample.map((s, i) => {
const body = (
<>
<span className="line-clamp-2">{s.text || "—"}</span>
{s.source ? (
<span className="text-ink-muted text-[0.72rem]"> · {s.source}</span>
) : null}
</>
);
return (
<li key={i} className="text-[0.82rem] text-ink leading-snug border-s-2 border-rule ps-2.5">
{s.href ? (
<Link href={s.href} className="hover:text-gold-deep hover:underline block">
{body}
</Link>
) : (
body
)}
</li>
);
})}
</ul>
) : null}

View File

@@ -0,0 +1,252 @@
"use client";
import { useMemo, useState } from "react";
import Link from "next/link";
import { Check, CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
useFeedbackList,
useResolveFeedback,
CATEGORY_LABELS,
CATEGORY_COLORS,
BLOCK_LABELS,
type ChairFeedback,
type FeedbackCategory,
} from "@/lib/api/feedback";
/**
* מרכז הערות יו"ר — הדף המרכזי לטיפול בכל הערות דפנה שנרשמו על טיוטות
* (chair_feedback). מאגד הערות מכל התיקים במקום אחד, מאפשר סינון לפי
* "טרם יושמו" וקטגוריה, וסימון כל הערה כיושמה. מוזן מ-/api/feedback.
*/
function formatDate(iso?: string | null): string {
if (!iso) return "";
try {
return new Date(iso).toLocaleDateString("he-IL", {
day: "numeric",
month: "long",
year: "numeric",
});
} catch {
return "";
}
}
function FeedbackCard({ fb }: { fb: ChairFeedback }) {
const resolve = useResolveFeedback();
const onResolve = () => {
resolve.mutate(
{ feedbackId: fb.id, applied_to: [] },
{
onSuccess: () => toast.success("ההערה סומנה כיושמה"),
onError: (e) =>
toast.error(e instanceof Error ? e.message : "שגיאה בסימון"),
},
);
};
return (
<Card className={`bg-surface border-rule shadow-sm ${fb.resolved ? "opacity-60" : ""}`}>
<CardContent className="px-5 py-4 space-y-2.5">
<div className="flex items-start gap-2 flex-wrap">
<Badge variant="outline" className={`text-[0.7rem] ${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/${encodeURIComponent(fb.case_number)}`}
className="text-[0.72rem] text-gold-deep hover:underline"
>
תיק {fb.case_number}
</Link>
) : (
<span className="text-[0.72rem] text-ink-muted">ללא תיק</span>
)}
<span className="ms-auto text-[0.72rem] text-ink-muted">
{formatDate(fb.created_at)}
</span>
</div>
<p className="text-navy text-sm leading-relaxed m-0 whitespace-pre-wrap" dir="rtl">
{fb.feedback_text}
</p>
{fb.lesson_extracted ? (
<div className="rounded-md bg-gold-wash/40 border border-gold/30 px-3 py-2">
<div className="text-[0.68rem] text-gold-deep mb-0.5">לקח שהופק</div>
<p className="text-ink-soft text-[0.82rem] leading-relaxed m-0 whitespace-pre-wrap" dir="rtl">
{fb.lesson_extracted}
</p>
</div>
) : null}
<div className="flex items-center justify-end pt-1 border-t border-rule-soft">
{fb.resolved ? (
<span className="flex items-center gap-1 text-[0.78rem] text-emerald-700">
<CheckCircle2 className="w-3.5 h-3.5" /> יושמה
</span>
) : (
<Button
size="sm"
onClick={onResolve}
disabled={resolve.isPending}
className="bg-gold text-navy hover:bg-gold-deep"
>
<Check className="w-3.5 h-3.5 me-1" />
סמן כיושם
</Button>
)}
</div>
</CardContent>
</Card>
);
}
type CatFilter = "all" | FeedbackCategory;
export default function FeedbackPage() {
const [unresolvedOnly, setUnresolvedOnly] = useState(true);
const [category, setCategory] = useState<CatFilter>("all");
const { data, isPending, error } = useFeedbackList({
unresolved_only: unresolvedOnly,
category: category === "all" ? undefined : category,
});
const items = useMemo(() => data ?? [], [data]);
const unresolvedCount = useMemo(
() => items.filter((f) => !f.resolved).length,
[items],
);
const categories: { key: CatFilter; label: string }[] = [
{ key: "all", label: "הכל" },
{ key: "missing_content", label: CATEGORY_LABELS.missing_content },
{ key: "wrong_tone", label: CATEGORY_LABELS.wrong_tone },
{ key: "wrong_structure", label: CATEGORY_LABELS.wrong_structure },
{ key: "factual_error", label: CATEGORY_LABELS.factual_error },
{ key: "style", label: CATEGORY_LABELS.style },
{ key: "other", label: CATEGORY_LABELS.other },
];
return (
<AppShell>
<section className="space-y-6">
<header className="space-y-1.5">
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span>
<Link href="/approvals" className="hover:text-gold-deep">מרכז אישורים</Link>
<span aria-hidden> · </span>
<span className="text-navy">הערות יו״ר</span>
</nav>
<div className="flex items-end justify-between gap-4 flex-wrap">
<div className="space-y-1">
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
שערים אנושיים · יו״ר הוועדה
</div>
<h1 className="text-navy mb-0">הערות יו״ר</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl leading-relaxed">
כל ההערות שנרשמו על טיוטות מכל התיקים. סמן כל הערה כיושמה
לאחר שהלקח הוטמע.
</p>
</div>
<div className="text-end">
<div className="text-3xl font-semibold text-navy leading-none">
{unresolvedCount}
</div>
<div className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted mt-1">
טרם יושמו
</div>
</div>
</div>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{/* Filters */}
<div className="flex items-center gap-3 flex-wrap">
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => setUnresolvedOnly(true)}
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
unresolvedOnly
? "bg-navy text-parchment border-navy"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`}
>
טרם יושמו
</button>
<button
type="button"
onClick={() => setUnresolvedOnly(false)}
className={`text-[0.78rem] px-3 py-1.5 rounded border transition-colors ${
!unresolvedOnly
? "bg-navy text-parchment border-navy"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`}
>
הכל
</button>
</div>
<div className="flex items-center gap-1 flex-wrap ms-auto">
{categories.map((c) => (
<button
key={c.key}
type="button"
onClick={() => setCategory(c.key)}
className={`text-[0.74rem] px-2.5 py-1 rounded border transition-colors ${
category === c.key
? "bg-gold-wash text-gold-deep border-gold/40"
: "bg-surface text-ink-muted border-rule hover:bg-rule-soft"
}`}
>
{c.label}
</button>
))}
</div>
</div>
{error ? (
<Card className="bg-surface border-rule">
<CardContent className="px-6 py-5 text-ink-muted text-sm">
שגיאה בטעינת ההערות. נסה לרענן.
</CardContent>
</Card>
) : isPending ? (
<div className="space-y-3">
{[0, 1, 2].map((i) => (
<Card key={i} className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4 h-28 animate-pulse" />
</Card>
))}
</div>
) : items.length === 0 ? (
<div className="text-center text-ink-muted py-16">
<p className="text-lg">אין הערות בקטגוריה זו.</p>
{unresolvedOnly && (
<p className="text-sm mt-2">כל ההערות יושמו </p>
)}
</div>
) : (
<div className="space-y-3">
{items.map((fb) => (
<FeedbackCard key={fb.id} fb={fb} />
))}
</div>
)}
</section>
</AppShell>
);
}