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>
|
||||
);
|
||||
}
|
||||
@@ -42,6 +42,7 @@ const NAV_GROUPS: NavGroup[] = [
|
||||
items: [
|
||||
{ href: "/", label: "בית" },
|
||||
{ href: "/approvals", label: "מרכז אישורים" },
|
||||
{ href: "/feedback", label: "הערות יו״ר" },
|
||||
{ href: "/archive", label: "ארכיון" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ import { apiRequest } from "./client";
|
||||
*/
|
||||
export type ApprovalSeverity = "high" | "medium" | "low" | "ok";
|
||||
|
||||
export type ApprovalSample = { text: string; source: string };
|
||||
export type ApprovalSample = { text: string; source: string; href?: string | null };
|
||||
|
||||
export type ApprovalCategory = {
|
||||
key: string;
|
||||
|
||||
10
web/app.py
10
web/app.py
@@ -5058,17 +5058,21 @@ async def api_chair_pending():
|
||||
"key": "chair_feedback", "label": "הערות יו\"ר שטרם יושמו",
|
||||
"description": "הערות שרשמת על טיוטות וטרם הופקו מהן לקחים/תיקונים.",
|
||||
"count": cf_count, "severity": "medium" if cf_count else "ok",
|
||||
"href": "/", "sample": [{"text": (r["feedback_text"] or "")[:120], "source": r["case_number"]} for r in cf_sample],
|
||||
"href": "/feedback", "sample": [{"text": (r["feedback_text"] or "")[:120], "source": r["case_number"]} for r in cf_sample],
|
||||
})
|
||||
|
||||
# 4) תיקים שנכשלו ב-QA
|
||||
qa_rows = await conn.fetch(
|
||||
"SELECT case_number, coalesce(title,'') AS title FROM cases WHERE status='qa_failed' ORDER BY updated_at DESC")
|
||||
# Single failed case → link straight to it; multiple → home dashboard
|
||||
# (the donut/table surface them). Each sample row links to its own case.
|
||||
qa_href = f"/cases/{qa_rows[0]['case_number']}" if len(qa_rows) == 1 else "/"
|
||||
categories.append({
|
||||
"key": "qa_failed", "label": "תיקים שנכשלו ב-QA",
|
||||
"description": "תיקים שבדיקת-האיכות חסמה — דורשים התייחסותך לפני המשך.",
|
||||
"count": len(qa_rows), "severity": "high" if qa_rows else "ok", "href": "/",
|
||||
"sample": [{"text": r["case_number"], "source": r["title"]} for r in qa_rows[:5]],
|
||||
"count": len(qa_rows), "severity": "high" if qa_rows else "ok", "href": qa_href,
|
||||
"sample": [{"text": r["case_number"], "source": r["title"],
|
||||
"href": f"/cases/{r['case_number']}"} for r in qa_rows[:5]],
|
||||
})
|
||||
|
||||
total_pending = sum(c["count"] for c in categories)
|
||||
|
||||
Reference in New Issue
Block a user