10 שגיאות (כולן קיימות-מראש, לא מהפיצ'רים האחרונים): - react/no-unescaped-entities (3): legal-arguments-panel, precedent-edit-sheet — escaping של מרכאות ב-JSX (“/") - react-hooks/set-state-in-effect (6): documents-panel, chair-editor, content-checklists, discussion-rules, golden-ratios, documents.ts — disable-comment לדפוסי sync/reset לגיטימיים (false-positive ידוע) - React Compiler reassign (1): subject-donut — refactor לחישוב prefix-sums ללא mutable accumulator ניקוי: הסרת 5 eslint-disable directives מיותרים (halacha-review-panel, precedent-upload-sheet). תוצאה: 0 errors (היה 10), 24→ warnings (היה 29). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
179 lines
5.8 KiB
TypeScript
179 lines
5.8 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { Card, CardContent } from "@/components/ui/card";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import {
|
||
useMethodology,
|
||
useUpdateMethodology,
|
||
useResetMethodology,
|
||
type GoldenRatios,
|
||
} from "@/lib/api/methodology";
|
||
import { toast } from "sonner";
|
||
import { Save, RotateCcw, Loader2 } from "lucide-react";
|
||
|
||
const OUTCOME_LABELS: Record<string, string> = {
|
||
rejection: "דחייה",
|
||
full_acceptance: "קבלה מלאה",
|
||
partial_acceptance: "קבלה חלקית",
|
||
betterment_levy: "היטל השבחה",
|
||
};
|
||
|
||
const SECTION_LABELS: Record<string, string> = {
|
||
background: "רקע",
|
||
claims: "טענות",
|
||
discussion: "דיון",
|
||
summary: "סיכום",
|
||
};
|
||
|
||
type RatioCard = {
|
||
key: string;
|
||
label: string;
|
||
original: GoldenRatios;
|
||
draft: GoldenRatios;
|
||
isOverride: boolean;
|
||
dirty: boolean;
|
||
};
|
||
|
||
export function GoldenRatiosPanel() {
|
||
const { data, isLoading } = useMethodology<GoldenRatios>("golden_ratios");
|
||
const update = useUpdateMethodology("golden_ratios");
|
||
const reset = useResetMethodology("golden_ratios");
|
||
const [cards, setCards] = useState<RatioCard[]>([]);
|
||
|
||
// Sync from server
|
||
useEffect(() => {
|
||
if (!data?.items) return;
|
||
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync from server
|
||
setCards(
|
||
Object.entries(data.items).map(([key, item]) => ({
|
||
key,
|
||
label: OUTCOME_LABELS[key] ?? key,
|
||
original: item.value,
|
||
draft: structuredClone(item.value),
|
||
isOverride: item.is_override,
|
||
dirty: false,
|
||
})),
|
||
);
|
||
}, [data]);
|
||
|
||
const setRange = (cardIdx: number, section: string, idx: 0 | 1, val: number) => {
|
||
setCards((prev) =>
|
||
prev.map((c, i) => {
|
||
if (i !== cardIdx) return c;
|
||
const next = { ...c, draft: { ...c.draft, [section]: [...c.draft[section]] as [number, number] } };
|
||
next.draft[section][idx] = val;
|
||
next.dirty = JSON.stringify(next.draft) !== JSON.stringify(next.original);
|
||
return next;
|
||
}),
|
||
);
|
||
};
|
||
|
||
const handleSave = (card: RatioCard) => {
|
||
update.mutate(
|
||
{ key: card.key, value: card.draft },
|
||
{
|
||
onSuccess: () => toast.success(`${card.label} נשמר`),
|
||
onError: () => toast.error("שגיאה בשמירה"),
|
||
},
|
||
);
|
||
};
|
||
|
||
const handleReset = (card: RatioCard) => {
|
||
reset.mutate(card.key, {
|
||
onSuccess: () => toast.success(`${card.label} אופס לברירת מחדל`),
|
||
onError: () => toast.error("שגיאה באיפוס"),
|
||
});
|
||
};
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="flex items-center justify-center py-12 text-ink-faint">
|
||
<Loader2 className="w-5 h-5 animate-spin ml-2" />
|
||
טוען...
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="grid gap-4 md:grid-cols-2">
|
||
{cards.map((card, ci) => (
|
||
<Card key={card.key} className="border-rule">
|
||
<CardContent className="px-5 py-4 space-y-3">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-sm font-semibold text-navy">{card.label}</h3>
|
||
<Badge variant={card.isOverride ? "default" : "secondary"} className="text-[10px]">
|
||
{card.isOverride ? "מותאם" : "ברירת מחדל"}
|
||
</Badge>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="text-ink-faint text-[11px]">
|
||
<th className="text-right py-1">Section</th>
|
||
<th className="text-center py-1">Min %</th>
|
||
<th className="text-center py-1">Max %</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{Object.entries(SECTION_LABELS).map(([sec, label]) => (
|
||
<tr key={sec} className="border-t border-rule/50">
|
||
<td className="py-1.5 text-ink">{label}</td>
|
||
<td className="py-1.5 px-1">
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
value={card.draft[sec]?.[0] ?? 0}
|
||
onChange={(e) => setRange(ci, sec, 0, Number(e.target.value))}
|
||
className="h-7 w-16 text-center text-xs mx-auto"
|
||
/>
|
||
</td>
|
||
<td className="py-1.5 px-1">
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
value={card.draft[sec]?.[1] ?? 0}
|
||
onChange={(e) => setRange(ci, sec, 1, Number(e.target.value))}
|
||
className="h-7 w-16 text-center text-xs mx-auto"
|
||
/>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center gap-2 pt-1">
|
||
<Button
|
||
size="sm"
|
||
disabled={!card.dirty || update.isPending}
|
||
onClick={() => handleSave(card)}
|
||
>
|
||
{update.isPending ? <Loader2 className="w-3 h-3 animate-spin ml-1" /> : <Save className="w-3 h-3 ml-1" />}
|
||
שמור
|
||
</Button>
|
||
{card.isOverride && (
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
disabled={reset.isPending}
|
||
onClick={() => handleReset(card)}
|
||
>
|
||
<RotateCcw className="w-3 h-3 ml-1" />
|
||
איפוס
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|