Files
legal-ai/web-ui/src/lib/api/plans.ts
Chaim 2b1fb18dfd
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
feat(plans): כפתור "משוך מ-מנהל-התכנון" בטופס-התכנית (Phase C טריגר 1)
טריגר 1 הידני: בטופס PlanForm, כפתור "משוך מ-מנהל-התכנון" — היו"ר
מקליד מספר-תכנית, לוחץ, והשדות (שם/תאריך-רשומות/י"פ/סוג/ייעוד)
מתמלאים מ-mavat דרך POST /api/plans/fetch (#292). היו"ר בודק ושומר —
שער-היו"ר נשמר (שום שמירה אוטומטית).

- plans.ts: useFetchPlan + PlanFetchResult.
- PlanForm: כפתור עם spinner (~דקה, דפדפן חי), מילוי-שדות (מחליף
  בערך-mavat היכן שקיים, שומר ערך-יו"ר היכן ש-mavat ריק), קישור-מקור
  "מקור: מנהל-התכנון" בתצוגה-המקדימה (פרובננס INV-AH).

עבר שער-עיצוב (מוקאפ 22-plans-review מאושר). ההוק ידני (לא תלוי
types שנוצרים). tsc  lint  (0 errors).

INV-AH: source_url מוצג; שדה-חסר ריק לא מומצא. G10: מילוי-טופס בלבד,
שמירה דרך plan_upsert הקיים. G2: צורך את /api/plans/fetch (#292).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:32:32 +00:00

185 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Planning-schemes registry (מרשם-התכניות, V38) — typed API hooks.
*
* SSOT for a plan's identity + validity, reused across cases. LLM-extracted rows
* arrive pending_review; only approved validity feeds block-tet (INV-DM5/G10).
* Variant duplicates are surfaced (usePlanDuplicates), never auto-merged — the
* chair merges manually. Mirrors the precedent-library hook conventions.
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "./client";
export type PlanReviewStatus = "pending_review" | "approved" | "rejected";
export type PlanDiscrepancy = {
field: string;
old: string;
new: string;
source_case_number?: string;
via?: string;
};
export type Plan = {
id: string;
plan_number: string;
display_name: string;
aliases: string[];
plan_type: string;
gazette_date: string | null; // ISO YYYY-MM-DD
yalkut_number: string;
purpose: string;
citation_formatted: string; // the deterministic block-tet sentence
review_status: PlanReviewStatus;
source_case_number: string;
source_document_id: string | null;
model_used: string;
discrepancies: PlanDiscrepancy[];
created_at: string;
updated_at: string;
/** Only present on a duplicate-candidate hit. */
match_reason?: string;
};
export const planKeys = {
all: ["plans"] as const,
list: () => [...planKeys.all, "list"] as const,
duplicates: (id: string) => [...planKeys.all, "duplicates", id] as const,
};
/**
* The whole registry in one fetch (every review_status). The panel filters
* client-side by status segment (ממתינים/מאושרות/כולן) and search — the
* registry is small, so one load gives accurate segment counts + instant
* filtering without per-keystroke round-trips. `limit` is a safety ceiling;
* if it is ever hit the panel surfaces a truncation note (no silent cap).
*/
export function usePlansAll(limit = 1000) {
return useQuery({
queryKey: planKeys.list(),
queryFn: ({ signal }) =>
apiRequest<{ items: Plan[]; count: number }>(
`/api/plans?limit=${limit}`,
{ signal },
),
staleTime: 5_000,
refetchOnMount: "always",
});
}
/** Near-duplicate candidates for a plan — surfaced for manual merge. */
export function usePlanDuplicates(planId: string, enabled = true) {
return useQuery({
queryKey: planKeys.duplicates(planId),
queryFn: ({ signal }) =>
apiRequest<{ items: Plan[]; count: number }>(
`/api/plans/${encodeURIComponent(planId)}/duplicates`,
{ signal },
),
enabled: enabled && !!planId,
staleTime: 10_000,
});
}
/** The candidate mavat returns for the form (INV-AH: every value carries source_url). */
export type PlanFetchResult = {
plan_number: string;
display_name: string;
plan_type: string;
purpose: string;
gazette_date: string; // ISO YYYY-MM-DD, "" if mavat doesn't expose it
yalkut_number: string;
yalkut_page: string;
source_url: string; // the mavat plan page
};
/**
* Pull a plan's identity + validity from mavat (מנהל התכנון) to prefill the form.
* Slow — drives a real browser on the host bridge (~½1 min); surface a spinner.
*/
export function useFetchPlan() {
return useMutation({
mutationFn: (planNumber: string) =>
apiRequest<PlanFetchResult>(`/api/plans/fetch`, {
method: "POST",
body: { plan_number: planNumber },
}),
});
}
export type PlanUpsert = {
plan_number: string;
display_name?: string;
plan_type?: string;
gazette_date?: string; // ISO; "" = unknown
yalkut_number?: string;
purpose?: string;
review_status?: PlanReviewStatus;
aliases?: string[];
};
export type PlanEdit = {
plan_number: string;
display_name?: string;
plan_type?: string;
gazette_date?: string;
yalkut_number?: string;
purpose?: string;
aliases?: string[] | null;
};
function invalidate(qc: ReturnType<typeof useQueryClient>) {
qc.invalidateQueries({ queryKey: planKeys.all });
// the chair-pending aggregate (/approvals) carries a plans count (INV-IA2)
qc.invalidateQueries({ queryKey: ["chair", "pending"] });
}
/** Manual chair add/upsert (idempotent on normalized plan_number). */
export function useUpsertPlan() {
const qc = useQueryClient();
return useMutation({
mutationFn: (body: PlanUpsert) =>
apiRequest<Plan>(`/api/plans`, { method: "POST", body }),
onSuccess: () => invalidate(qc),
});
}
/** Edit/fix a plan by id (refuses a number collision → use merge). */
export function useUpdatePlan() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, patch }: { id: string; patch: PlanEdit }) =>
apiRequest<Plan>(
`/api/plans/${encodeURIComponent(id)}`,
{ method: "PATCH", body: patch },
),
onSuccess: () => invalidate(qc),
});
}
/** Chair gate (G10): approve / reject / reset. */
export function useReviewPlan() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, status }: { id: string; status: PlanReviewStatus }) =>
apiRequest<Plan>(
`/api/plans/${encodeURIComponent(id)}/review`,
{ method: "POST", body: { review_status: status } },
),
onSuccess: () => invalidate(qc),
});
}
/** Fold a source plan into a target (manual dedup); source is deleted. */
export function useMergePlans() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ sourceId, targetId }: { sourceId: string; targetId: string }) =>
apiRequest<Plan>(
`/api/plans/merge`,
{ method: "POST", body: { source_id: sourceId, target_id: targetId } },
),
onSuccess: () => invalidate(qc),
});
}