From 1133272e343aa9e51f7028c4313bebcd3c912593 Mon Sep 17 00:00:00 2001 From: Chaim Date: Tue, 14 Apr 2026 14:09:08 +0000 Subject: [PATCH] =?UTF-8?q?Fix=20Paperclip=20integration=20(identifier?= =?UTF-8?q?=E2=86=92issue=5Fprefix)=20+=20add=20settings=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix column name mismatch in paperclip_client.py and app.py: Paperclip's companies table uses `issue_prefix`, not `identifier` - Fix _LEGAL_DB_URL to read from POSTGRES_URL env var (used in container) - Add settings page (/settings) for managing tag → Paperclip company mappings - Replace "תיק חדש" nav item with "הגדרות" (new case is on home page) Co-Authored-By: Claude Opus 4.6 (1M context) --- web-ui/src/app/settings/page.tsx | 250 ++++++++++++++++++++++++++++ web-ui/src/components/app-shell.tsx | 2 +- web-ui/src/lib/api/settings.ts | 57 +++++++ web/app.py | 4 +- web/paperclip_client.py | 12 +- 5 files changed, 316 insertions(+), 9 deletions(-) create mode 100644 web-ui/src/app/settings/page.tsx create mode 100644 web-ui/src/lib/api/settings.ts diff --git a/web-ui/src/app/settings/page.tsx b/web-ui/src/app/settings/page.tsx new file mode 100644 index 0000000..67b32fe --- /dev/null +++ b/web-ui/src/app/settings/page.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Plus, Trash2, Tags, Building2 } from "lucide-react"; +import { AppShell } from "@/components/app-shell"; +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"; + +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 handleTagChange(value: string) { + setTag(value); + const match = APPEAL_SUBTYPES.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 ( + +
+
+ +

הגדרות

+

+ ניהול מיפוי תגיות ערר לחברות ב-Paperclip. כל תיק חדש ישויך + אוטומטית לפרויקט בחברה הנכונה לפי סוג הערר. +

+
+ +
+ + {/* Companies overview */} + + +

+ + חברות ב-Paperclip +

+ {loadingCompanies ? ( + + ) : !companies?.length ? ( +

לא נמצאו חברות

+ ) : ( +
+ {companies.map((c) => ( +
+ {c.name} + + {c.prefix} + +
+ ))} +
+ )} +
+
+ + {/* Tag mappings */} + + +

+ + מיפוי תגיות + + {mappings?.length ?? 0} + +

+ + {/* Add form */} +
+
+ + +
+ +
+ + setTagLabel(e.target.value)} + placeholder="שם לתצוגה" + className="w-[160px]" + /> +
+ +
+ + +
+ + +
+ + {/* Table */} + {loadingMappings ? ( + + ) : !mappings?.length ? ( +

+ אין מיפויים. הוסף מיפוי כדי שתיקים חדשים ישויכו אוטומטית + לפרויקט בחברה הנכונה. +

+ ) : ( +
+ + + + + + + + + + {mappings.map((m) => ( + + + + + + + ))} + +
TagLabelCompany +
+ + {m.tag} + + {m.tag_label}{m.company_name} + +
+
+ )} +
+
+
+
+ ); +} diff --git a/web-ui/src/components/app-shell.tsx b/web-ui/src/components/app-shell.tsx index bfd5187..102d0b4 100644 --- a/web-ui/src/components/app-shell.tsx +++ b/web-ui/src/components/app-shell.tsx @@ -24,10 +24,10 @@ type NavItem = { const NAV_ITEMS: NavItem[] = [ { href: "/", label: "בית" }, - { href: "/cases/new", label: "תיק חדש" }, { href: "/training", label: "אימון סגנון" }, { href: "/skills", label: "מיומנויות" }, { href: "/diagnostics", label: "אבחון" }, + { href: "/settings", label: "הגדרות" }, ]; function isActive(pathname: string, href: string): boolean { diff --git a/web-ui/src/lib/api/settings.ts b/web-ui/src/lib/api/settings.ts new file mode 100644 index 0000000..87c708c --- /dev/null +++ b/web-ui/src/lib/api/settings.ts @@ -0,0 +1,57 @@ +/** + * Settings hooks: tag → Paperclip company mappings. + */ + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { apiRequest } from "./client"; + +export type PaperclipCompany = { + id: string; + name: string; + prefix: string; +}; + +export type TagMapping = { + id: string; + tag: string; + tag_label: string; + company_id: string; + company_name: string; + created_at: string; +}; + +export function usePaperclipCompanies() { + return useQuery({ + queryKey: ["settings", "paperclip-companies"] as const, + queryFn: ({ signal }) => + apiRequest("/api/settings/paperclip-companies", { signal }), + staleTime: 60_000, + }); +} + +export function useTagMappings() { + return useQuery({ + queryKey: ["settings", "tag-mappings"] as const, + queryFn: ({ signal }) => + apiRequest("/api/settings/tag-mappings", { signal }), + staleTime: 10_000, + }); +} + +export function useAddTagMapping() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { tag: string; tag_label: string; company_id: string; company_name: string }) => + apiRequest("/api/settings/tag-mappings", { method: "POST", body }), + onSuccess: () => qc.invalidateQueries({ queryKey: ["settings", "tag-mappings"] }), + }); +} + +export function useDeleteTagMapping() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => + apiRequest<{ ok: boolean }>(`/api/settings/tag-mappings/${id}`, { method: "DELETE" }), + onSuccess: () => qc.invalidateQueries({ queryKey: ["settings", "tag-mappings"] }), + }); +} diff --git a/web/app.py b/web/app.py index 5e0e0a7..da2dfbb 100644 --- a/web/app.py +++ b/web/app.py @@ -2113,9 +2113,9 @@ async def api_paperclip_companies(): conn = await asyncpg.connect(pc_url) try: rows = await conn.fetch( - "SELECT id, name, identifier FROM companies ORDER BY name" + "SELECT id, name, issue_prefix FROM companies ORDER BY name" ) - return [{"id": str(r["id"]), "name": r["name"], "identifier": r.get("identifier", "")} for r in rows] + return [{"id": str(r["id"]), "name": r["name"], "prefix": r.get("issue_prefix", "")} for r in rows] finally: await conn.close() except Exception as e: diff --git a/web/paperclip_client.py b/web/paperclip_client.py index 1654f7e..cead8fa 100644 --- a/web/paperclip_client.py +++ b/web/paperclip_client.py @@ -40,7 +40,7 @@ _FALLBACK_APPEAL_TYPE_TO_COMPANY = { } # Legal-AI DB URL for reading tag_company_mappings -_LEGAL_DB_URL = os.environ.get( +_LEGAL_DB_URL = os.environ.get("POSTGRES_URL") or os.environ.get( "DATABASE_URL", "postgresql://legal:legal@127.0.0.1:5432/legal_ai" ) @@ -76,11 +76,11 @@ async def create_project( conn = await asyncpg.connect(PAPERCLIP_DB_URL) try: - # Resolve prefix from company identifier in Paperclip DB + # Resolve prefix from company issue_prefix in Paperclip DB comp_row = await conn.fetchrow( - "SELECT identifier FROM companies WHERE id = $1::uuid", company_id, + "SELECT issue_prefix FROM companies WHERE id = $1::uuid", company_id, ) - prefix = comp_row["identifier"] if comp_row and comp_row["identifier"] else "CMP" + prefix = comp_row["issue_prefix"] if comp_row and comp_row["issue_prefix"] else "CMP" # Check for existing project with this case number existing = await conn.fetchrow( @@ -244,9 +244,9 @@ async def get_project_url(case_number: str) -> str | None: ) if row: comp_row = await conn.fetchrow( - "SELECT identifier FROM companies WHERE id = $1::uuid", str(row["company_id"]), + "SELECT issue_prefix FROM companies WHERE id = $1::uuid", str(row["company_id"]), ) - prefix = comp_row["identifier"] if comp_row and comp_row["identifier"] else "CMP" + prefix = comp_row["issue_prefix"] if comp_row and comp_row["issue_prefix"] else "CMP" return f"https://pc.nautilus.marcusgroup.org/{prefix}/projects/{row['id']}/issues" return None finally: