refactor(settings): split into tabs (paperclip + 3 stubs)

Extracts Paperclip companies + tag-mappings UI into PaperclipTab component,
adds stub tabs for Environment / Tools / Registrations, and replaces the flat
page.tsx with a shadcn Tabs layout to make room for Tasks 8-10.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 06:44:27 +00:00
parent 6c129a1350
commit 8289b4d643
5 changed files with 259 additions and 223 deletions

View File

@@ -0,0 +1 @@
export function EnvironmentTab() { return <div>Environment tab coming soon</div>; }

View File

@@ -0,0 +1,225 @@
"use client";
import { useState } from "react";
import { Plus, Trash2, Tags, Building2 } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
useTagMappings,
usePaperclipCompanies,
useAddTagMapping,
useDeleteTagMapping,
} from "@/lib/api/settings";
import { APPEAL_SUBTYPES } from "@/lib/practice-area";
import { toast } from "sonner";
const TAG_SUGGESTIONS = APPEAL_SUBTYPES.filter((s) => s.value !== "unknown");
export function PaperclipTab() {
const { data: mappings, isPending: loadingMappings } = useTagMappings();
const { data: companies, isPending: loadingCompanies } = usePaperclipCompanies();
const addMapping = useAddTagMapping();
const deleteMapping = useDeleteTagMapping();
const [tag, setTag] = useState("");
const [tagLabel, setTagLabel] = useState("");
const [companyId, setCompanyId] = useState("");
function handleTagInput(value: string) {
setTag(value);
const match = TAG_SUGGESTIONS.find((s) => s.value === value);
if (match) setTagLabel(match.label);
}
function handleAdd() {
if (!tag || !companyId) {
toast.error("יש לבחור תגית וחברה");
return;
}
const company = companies?.find((c) => c.id === companyId);
addMapping.mutate(
{
tag,
tag_label: tagLabel,
company_id: companyId,
company_name: company?.name ?? "",
},
{
onSuccess: () => {
toast.success("מיפוי נוסף בהצלחה");
setTag("");
setTagLabel("");
setCompanyId("");
},
onError: (err) => toast.error(`שגיאה: ${err.message}`),
},
);
}
function handleDelete(id: string, tag: string) {
deleteMapping.mutate(id, {
onSuccess: () => toast.success(`מיפוי "${tag}" נמחק`),
onError: (err) => toast.error(`שגיאה: ${err.message}`),
});
}
return (
<div className="space-y-6">
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h2 className="text-navy text-lg mb-3 flex items-center gap-2">
<Building2 className="w-4 h-4" />
חברות ב-Paperclip
</h2>
{loadingCompanies ? (
<Skeleton className="h-12 w-full" />
) : !companies?.length ? (
<p className="text-ink-muted text-sm">לא נמצאו חברות</p>
) : (
<div className="flex flex-wrap gap-3">
{companies.map((c) => (
<div
key={c.id}
className="flex items-center gap-2 rounded-md bg-rule-soft/60 border border-rule px-4 py-2.5"
>
<span className="text-sm font-medium text-ink">{c.name}</span>
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{c.prefix}
</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<h2 className="text-navy text-lg mb-4 flex items-center gap-2">
<Tags className="w-4 h-4" />
מיפוי תגיות
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{mappings?.length ?? 0}
</Badge>
</h2>
<div className="flex flex-wrap items-end gap-3 mb-5 p-4 rounded-md bg-rule-soft/40 border border-rule">
<div className="flex flex-col gap-1.5 min-w-[180px]">
<label className="text-[0.72rem] text-ink-muted">תגית</label>
<Input
list="tag-suggestions"
value={tag}
onChange={(e) => handleTagInput(e.target.value)}
placeholder="סוג ערר או תגית חופשית"
className="w-[220px]"
/>
<datalist id="tag-suggestions">
{TAG_SUGGESTIONS.map((s) => (
<option key={s.value} value={s.value}>
{s.label}
</option>
))}
</datalist>
</div>
<div className="flex flex-col gap-1.5 min-w-[140px]">
<label className="text-[0.72rem] text-ink-muted">תווית</label>
<Input
value={tagLabel}
onChange={(e) => setTagLabel(e.target.value)}
placeholder="שם לתצוגה"
className="w-[160px]"
/>
</div>
<div className="flex flex-col gap-1.5 min-w-[200px]">
<label className="text-[0.72rem] text-ink-muted">
חברה ב-Paperclip
</label>
<Select value={companyId} onValueChange={setCompanyId}>
<SelectTrigger className="w-[240px]">
<SelectValue placeholder="בחר חברה" />
</SelectTrigger>
<SelectContent>
{companies?.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name} ({c.prefix})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
onClick={handleAdd}
disabled={addMapping.isPending || !tag || !companyId}
size="default"
>
<Plus className="w-4 h-4" data-icon="inline-start" />
{addMapping.isPending ? "שומר..." : "הוסף מיפוי"}
</Button>
</div>
{loadingMappings ? (
<Skeleton className="h-32 w-full" />
) : !mappings?.length ? (
<p className="text-ink-muted text-sm">
אין מיפויים. הוסף מיפוי כדי שתיקים חדשים ישויכו אוטומטית
לפרויקט בחברה הנכונה.
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-rule text-ink-muted text-[0.72rem] uppercase tracking-wider">
<th className="text-start py-2 px-3 font-medium">Tag</th>
<th className="text-start py-2 px-3 font-medium">Label</th>
<th className="text-start py-2 px-3 font-medium">Company</th>
<th className="py-2 px-3 w-12" />
</tr>
</thead>
<tbody>
{mappings.map((m) => (
<tr
key={m.id}
className="border-b border-rule/60 hover:bg-rule-soft/40 transition-colors"
>
<td className="py-2.5 px-3">
<Badge variant="outline" className="text-[0.75rem] font-mono">
{m.tag}
</Badge>
</td>
<td className="py-2.5 px-3 text-ink">{m.tag_label}</td>
<td className="py-2.5 px-3 text-ink">{m.company_name}</td>
<td className="py-2.5 px-3">
<Button
variant="ghost"
size="icon-xs"
onClick={() => handleDelete(m.id, m.tag)}
disabled={deleteMapping.isPending}
title="מחק מיפוי"
>
<Trash2 className="w-3.5 h-3.5 text-danger" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1 @@
export function RegistrationsTab() { return <div>Registrations tab coming soon</div>; }

View File

@@ -0,0 +1 @@
export function ToolsTab() { return <div>Tools tab coming soon</div>; }

View File

@@ -1,80 +1,15 @@
"use client"; "use client";
import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { Plus, Trash2, Tags, Building2 } from "lucide-react"; import { Server, Wrench, Plug, Building2 } from "lucide-react";
import { AppShell } from "@/components/app-shell"; import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge"; import { PaperclipTab } from "./_components/paperclip-tab";
import { Button } from "@/components/ui/button"; import { EnvironmentTab } from "./_components/environment-tab";
import { Input } from "@/components/ui/input"; import { ToolsTab } from "./_components/tools-tab";
import { Skeleton } from "@/components/ui/skeleton"; import { RegistrationsTab } from "./_components/registrations-tab";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
useTagMappings,
usePaperclipCompanies,
useAddTagMapping,
useDeleteTagMapping,
} from "@/lib/api/settings";
import { APPEAL_SUBTYPES } from "@/lib/practice-area";
import { toast } from "sonner";
const TAG_SUGGESTIONS = APPEAL_SUBTYPES.filter((s) => s.value !== "unknown");
export default function SettingsPage() { export default function SettingsPage() {
const { data: mappings, isPending: loadingMappings } = useTagMappings();
const { data: companies, isPending: loadingCompanies } = usePaperclipCompanies();
const addMapping = useAddTagMapping();
const deleteMapping = useDeleteTagMapping();
const [tag, setTag] = useState("");
const [tagLabel, setTagLabel] = useState("");
const [companyId, setCompanyId] = useState("");
function handleTagInput(value: string) {
setTag(value);
const match = TAG_SUGGESTIONS.find((s) => s.value === value);
if (match) setTagLabel(match.label);
}
function handleAdd() {
if (!tag || !companyId) {
toast.error("יש לבחור תגית וחברה");
return;
}
const company = companies?.find((c) => c.id === companyId);
addMapping.mutate(
{
tag,
tag_label: tagLabel,
company_id: companyId,
company_name: company?.name ?? "",
},
{
onSuccess: () => {
toast.success("מיפוי נוסף בהצלחה");
setTag("");
setTagLabel("");
setCompanyId("");
},
onError: (err) => toast.error(`שגיאה: ${err.message}`),
},
);
}
function handleDelete(id: string, tag: string) {
deleteMapping.mutate(id, {
onSuccess: () => toast.success(`מיפוי "${tag}" נמחק`),
onError: (err) => toast.error(`שגיאה: ${err.message}`),
});
}
return ( return (
<AppShell> <AppShell>
<section className="space-y-6"> <section className="space-y-6">
@@ -88,164 +23,37 @@ export default function SettingsPage() {
</nav> </nav>
<h1 className="text-navy mb-0">הגדרות</h1> <h1 className="text-navy mb-0">הגדרות</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl"> <p className="text-ink-muted text-sm mt-1 max-w-2xl">
ניהול מיפוי תגיות ערר לחברות ב-Paperclip. כל תיק חדש ישויך תצורת המערכת, MCP server, ו-Paperclip integration.
אוטומטית לפרויקט בחברה הנכונה לפי סוג הערר.
</p> </p>
</header> </header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" /> <div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{/* Companies overview */} <Tabs defaultValue="paperclip" className="space-y-4">
<Card className="bg-surface border-rule shadow-sm"> <TabsList>
<CardContent className="px-6 py-5"> <TabsTrigger value="paperclip">
<h2 className="text-navy text-lg mb-3 flex items-center gap-2"> <Building2 className="w-4 h-4" data-icon="inline-start" />
<Building2 className="w-4 h-4" /> Paperclip
חברות ב-Paperclip </TabsTrigger>
</h2> <TabsTrigger value="environment">
{loadingCompanies ? ( <Server className="w-4 h-4" data-icon="inline-start" />
<Skeleton className="h-12 w-full" /> Environment
) : !companies?.length ? ( </TabsTrigger>
<p className="text-ink-muted text-sm">לא נמצאו חברות</p> <TabsTrigger value="tools">
) : ( <Wrench className="w-4 h-4" data-icon="inline-start" />
<div className="flex flex-wrap gap-3"> Tools
{companies.map((c) => ( </TabsTrigger>
<div <TabsTrigger value="registrations">
key={c.id} <Plug className="w-4 h-4" data-icon="inline-start" />
className="flex items-center gap-2 rounded-md bg-rule-soft/60 border border-rule px-4 py-2.5" Registrations
> </TabsTrigger>
<span className="text-sm font-medium text-ink">{c.name}</span> </TabsList>
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{c.prefix}
</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Tag mappings */} <TabsContent value="paperclip"><PaperclipTab /></TabsContent>
<Card className="bg-surface border-rule shadow-sm"> <TabsContent value="environment"><EnvironmentTab /></TabsContent>
<CardContent className="px-6 py-5"> <TabsContent value="tools"><ToolsTab /></TabsContent>
<h2 className="text-navy text-lg mb-4 flex items-center gap-2"> <TabsContent value="registrations"><RegistrationsTab /></TabsContent>
<Tags className="w-4 h-4" /> </Tabs>
מיפוי תגיות
<Badge variant="outline" className="text-[0.7rem] tabular-nums">
{mappings?.length ?? 0}
</Badge>
</h2>
{/* Add form */}
<div className="flex flex-wrap items-end gap-3 mb-5 p-4 rounded-md bg-rule-soft/40 border border-rule">
<div className="flex flex-col gap-1.5 min-w-[180px]">
<label className="text-[0.72rem] text-ink-muted">
תגית
</label>
<Input
list="tag-suggestions"
value={tag}
onChange={(e) => handleTagInput(e.target.value)}
placeholder="סוג ערר או תגית חופשית"
className="w-[220px]"
/>
<datalist id="tag-suggestions">
{TAG_SUGGESTIONS.map((s) => (
<option key={s.value} value={s.value}>
{s.label}
</option>
))}
</datalist>
</div>
<div className="flex flex-col gap-1.5 min-w-[140px]">
<label className="text-[0.72rem] text-ink-muted">תווית</label>
<Input
value={tagLabel}
onChange={(e) => setTagLabel(e.target.value)}
placeholder="שם לתצוגה"
className="w-[160px]"
/>
</div>
<div className="flex flex-col gap-1.5 min-w-[200px]">
<label className="text-[0.72rem] text-ink-muted">
חברה ב-Paperclip
</label>
<Select value={companyId} onValueChange={setCompanyId}>
<SelectTrigger className="w-[240px]">
<SelectValue placeholder="בחר חברה" />
</SelectTrigger>
<SelectContent>
{companies?.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name} ({c.prefix})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
onClick={handleAdd}
disabled={addMapping.isPending || !tag || !companyId}
size="default"
>
<Plus className="w-4 h-4" data-icon="inline-start" />
{addMapping.isPending ? "שומר..." : "הוסף מיפוי"}
</Button>
</div>
{/* Table */}
{loadingMappings ? (
<Skeleton className="h-32 w-full" />
) : !mappings?.length ? (
<p className="text-ink-muted text-sm">
אין מיפויים. הוסף מיפוי כדי שתיקים חדשים ישויכו אוטומטית
לפרויקט בחברה הנכונה.
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-rule text-ink-muted text-[0.72rem] uppercase tracking-wider">
<th className="text-start py-2 px-3 font-medium">Tag</th>
<th className="text-start py-2 px-3 font-medium">Label</th>
<th className="text-start py-2 px-3 font-medium">Company</th>
<th className="py-2 px-3 w-12" />
</tr>
</thead>
<tbody>
{mappings.map((m) => (
<tr
key={m.id}
className="border-b border-rule/60 hover:bg-rule-soft/40 transition-colors"
>
<td className="py-2.5 px-3">
<Badge variant="outline" className="text-[0.75rem] font-mono">
{m.tag}
</Badge>
</td>
<td className="py-2.5 px-3 text-ink">{m.tag_label}</td>
<td className="py-2.5 px-3 text-ink">{m.company_name}</td>
<td className="py-2.5 px-3">
<Button
variant="ghost"
size="icon-xs"
onClick={() => handleDelete(m.id, m.tag)}
disabled={deleteMapping.isPending}
title="מחק מיפוי"
>
<Trash2 className="w-3.5 h-3.5 text-danger" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</section> </section>
</AppShell> </AppShell>
); );