feat(feedback): דף מרכזי /feedback להערות יו"ר + תיקון קישורי מרכז אישורים
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s
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:
@@ -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}
|
||||
|
||||
|
||||
252
web-ui/src/app/feedback/page.tsx
Normal file
252
web-ui/src/app/feedback/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user