סוגר את לולאת פידבק-יו"ר→ידע-סוכנים. עד כה resolve רק עדכן את ה-DB; עכשיו לחיצה ב-/feedback מעירה את ה-CEO שמקפל את הלקח לקובץ לפי הקטגוריה. - paperclip_client.py: wake_ceo_for_feedback_fold() — יוצר issue ב-Paperclip עם הלקח + rubric ניתוב (style→SKILL.md, wrong_structure→block-schema, אחר→lessons.md), מעיר CEO. משכפל את דפוס wake_for_precedent_extraction - db.py: get_chair_feedback(id) — שליפת הערה בודדת עם case_number/appeal_type - app.py: resolve endpoint מקבל fold (ברירת מחדל true); BackgroundTask fire-and-forget; guard — רק עם lesson_extracted. מחזיר fold_queued - legal-ceo.md: dispatch ל-feedback_fold_ + סעיף "קיפול הערת יו"ר" עם rubric - frontend: useResolveFeedback מקבל fold; /feedback שולח fold=true עם toast; drafts-panel שולח fold=false (bookkeeping per-case, בלי קיפול כפול) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
258 lines
9.2 KiB
TypeScript
258 lines
9.2 KiB
TypeScript
"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: [], fold: true },
|
||
{
|
||
onSuccess: (res) =>
|
||
toast.success(
|
||
res.fold_queued
|
||
? "סומנה כיושמה — הלקח נשלח ל-CEO לקיפול לקובץ הידע"
|
||
: "ההערה סומנה כיושמה",
|
||
),
|
||
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>
|
||
);
|
||
}
|