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>
|
<p className="text-[0.85rem] text-emerald-700 mb-0">אין פריטים ממתינים ✓</p>
|
||||||
) : cat.sample && cat.sample.length > 0 ? (
|
) : cat.sample && cat.sample.length > 0 ? (
|
||||||
<ul className="space-y-1.5 mt-1">
|
<ul className="space-y-1.5 mt-1">
|
||||||
{cat.sample.map((s, i) => (
|
{cat.sample.map((s, i) => {
|
||||||
<li key={i} className="text-[0.82rem] text-ink leading-snug border-s-2 border-rule ps-2.5">
|
const body = (
|
||||||
<span className="line-clamp-2">{s.text || "—"}</span>
|
<>
|
||||||
{s.source ? (
|
<span className="line-clamp-2">{s.text || "—"}</span>
|
||||||
<span className="text-ink-muted text-[0.72rem]"> · {s.source}</span>
|
{s.source ? (
|
||||||
) : null}
|
<span className="text-ink-muted text-[0.72rem]"> · {s.source}</span>
|
||||||
</li>
|
) : 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>
|
</ul>
|
||||||
) : null}
|
) : 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: [
|
items: [
|
||||||
{ href: "/", label: "בית" },
|
{ href: "/", label: "בית" },
|
||||||
{ href: "/approvals", label: "מרכז אישורים" },
|
{ href: "/approvals", label: "מרכז אישורים" },
|
||||||
|
{ href: "/feedback", label: "הערות יו״ר" },
|
||||||
{ href: "/archive", label: "ארכיון" },
|
{ href: "/archive", label: "ארכיון" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { apiRequest } from "./client";
|
|||||||
*/
|
*/
|
||||||
export type ApprovalSeverity = "high" | "medium" | "low" | "ok";
|
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 = {
|
export type ApprovalCategory = {
|
||||||
key: string;
|
key: string;
|
||||||
|
|||||||
10
web/app.py
10
web/app.py
@@ -5058,17 +5058,21 @@ async def api_chair_pending():
|
|||||||
"key": "chair_feedback", "label": "הערות יו\"ר שטרם יושמו",
|
"key": "chair_feedback", "label": "הערות יו\"ר שטרם יושמו",
|
||||||
"description": "הערות שרשמת על טיוטות וטרם הופקו מהן לקחים/תיקונים.",
|
"description": "הערות שרשמת על טיוטות וטרם הופקו מהן לקחים/תיקונים.",
|
||||||
"count": cf_count, "severity": "medium" if cf_count else "ok",
|
"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
|
# 4) תיקים שנכשלו ב-QA
|
||||||
qa_rows = await conn.fetch(
|
qa_rows = await conn.fetch(
|
||||||
"SELECT case_number, coalesce(title,'') AS title FROM cases WHERE status='qa_failed' ORDER BY updated_at DESC")
|
"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({
|
categories.append({
|
||||||
"key": "qa_failed", "label": "תיקים שנכשלו ב-QA",
|
"key": "qa_failed", "label": "תיקים שנכשלו ב-QA",
|
||||||
"description": "תיקים שבדיקת-האיכות חסמה — דורשים התייחסותך לפני המשך.",
|
"description": "תיקים שבדיקת-האיכות חסמה — דורשים התייחסותך לפני המשך.",
|
||||||
"count": len(qa_rows), "severity": "high" if qa_rows else "ok", "href": "/",
|
"count": len(qa_rows), "severity": "high" if qa_rows else "ok", "href": qa_href,
|
||||||
"sample": [{"text": r["case_number"], "source": r["title"]} for r in qa_rows[:5]],
|
"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)
|
total_pending = sum(c["count"] for c in categories)
|
||||||
|
|||||||
Reference in New Issue
Block a user