diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index b3ca463..14f4caa 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -315,6 +315,59 @@ button {
color: var(--muted, #6b7280);
margin: 4px 0 14px;
}
+.child-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+.child-editor {
+ border-top: 1px solid rgba(0, 0, 0, 0.08);
+ margin-top: 12px;
+ padding-top: 14px;
+}
+.picker {
+ position: relative;
+}
+.picker-selected {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 8px 12px;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 8px;
+}
+.picker-list {
+ position: absolute;
+ z-index: 20;
+ top: 100%;
+ left: 0;
+ right: 0;
+ margin: 4px 0 0;
+ padding: 4px;
+ list-style: none;
+ background: var(--card-bg, #fff);
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
+ max-height: 280px;
+ overflow-y: auto;
+}
+.picker-item {
+ display: block;
+ width: 100%;
+ text-align: left;
+ padding: 8px 10px;
+ border: 0;
+ background: transparent;
+ border-radius: 6px;
+ cursor: pointer;
+ font: inherit;
+}
+.picker-item:hover {
+ background: rgba(0, 0, 0, 0.05);
+}
/* ============================================================================
Buttons
diff --git a/apps/web/src/app/polizas/[id]/editar/page.tsx b/apps/web/src/app/polizas/[id]/editar/page.tsx
new file mode 100644
index 0000000..cf50252
--- /dev/null
+++ b/apps/web/src/app/polizas/[id]/editar/page.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Link from "next/link";
+import { AppShell } from "@/components/AppShell";
+import { PolicyForm } from "@/components/PolicyForm";
+import { useCan } from "@/lib/abilities";
+import { getPolicy } from "@/lib/api";
+import type { PolicyDetail } from "@/lib/types";
+
+export default function EditarPolizaPage({
+ params,
+}: {
+ params: { id: string };
+}) {
+ return (
+
+
+
+ );
+}
+
+function EditarPoliza({ id }: { id: string }) {
+ const allowed = useCan("policy:update");
+ const [policy, setPolicy] = useState
(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!allowed) return;
+ getPolicy(id)
+ .then(setPolicy)
+ .catch((e) => setError(e?.message ?? "No se pudo cargar la póliza."));
+ }, [id, allowed]);
+
+ return (
+ <>
+
+ ← Póliza
+
Editar póliza
+
+ {!allowed ? (
+
+ No tiene permisos para editar pólizas.
+
+ ) : error ? (
+ {error}
+ ) : !policy ? (
+
+ ) : (
+
+ )}
+ >
+ );
+}
diff --git a/apps/web/src/app/polizas/[id]/page.tsx b/apps/web/src/app/polizas/[id]/page.tsx
index e9a6ea4..7e5a1d9 100644
--- a/apps/web/src/app/polizas/[id]/page.tsx
+++ b/apps/web/src/app/polizas/[id]/page.tsx
@@ -3,7 +3,17 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import { AppShell } from "@/components/AppShell";
-import { getPolicy } from "@/lib/api";
+import {
+ addPolicyChild,
+ archivePolicy,
+ getLookups,
+ getPolicy,
+ removePolicyChild,
+ restorePolicy,
+ updatePolicyChild,
+} from "@/lib/api";
+import { useCan } from "@/lib/abilities";
+import { ChildCollection, type ChildConfig } from "@/components/ChildCollection";
import {
expiryPhrase,
formatDate,
@@ -12,7 +22,7 @@ import {
premiumHeadline,
SIN_NOMBRE,
} from "@/lib/labels";
-import type { Installment, PolicyDetail } from "@/lib/types";
+import type { AdjusterRow, Installment, PolicyDetail } from "@/lib/types";
export default function PolizaDetailPage({
params,
@@ -73,9 +83,14 @@ function Detail({ id }: { id: string }) {
if (!data) return null;
+ const reload = () => getPolicy(id).then(setData).catch(() => {});
+
return (
-
+
@@ -87,10 +102,163 @@ function Detail({ id }: { id: string }) {
{data.claims.length > 0 &&
}
+
);
}
+/** Edit / archive controls for the policy header. */
+function PolicyActions({
+ data,
+ onChange,
+}: {
+ data: PolicyDetail;
+ onChange: () => void;
+}) {
+ const canEdit = useCan("policy:update");
+ const canDelete = useCan("policy:delete");
+ const [busy, setBusy] = useState(false);
+ const archived = data.archivedAt != null;
+
+ async function toggle() {
+ const verb = archived ? "restaurar" : "archivar";
+ if (!window.confirm(`¿Seguro que desea ${verb} esta póliza?`)) return;
+ setBusy(true);
+ try {
+ if (archived) await restorePolicy(data.id);
+ else await archivePolicy(data.id);
+ onChange();
+ } catch (e) {
+ window.alert((e as Error)?.message ?? "No se pudo completar la acción.");
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ if (!canEdit && !canDelete) return null;
+ return (
+
+ {archived && Archivada}
+ {canEdit && (
+
+ Editar
+
+ )}
+ {canDelete && (
+
+ )}
+
+ );
+}
+
+/** Editable child collections — only shown to users who can edit the policy. */
+function ChildrenEditor({
+ data,
+ onChange,
+}: {
+ data: PolicyDetail;
+ onChange: () => void;
+}) {
+ const canEdit = useCan("policy:update");
+ const [adjusters, setAdjusters] = useState([]);
+
+ useEffect(() => {
+ if (canEdit) getLookups().then((l) => setAdjusters(l.adjusters)).catch(() => {});
+ }, [canEdit]);
+
+ if (!canEdit) return null;
+
+ const INSTALLMENTS: ChildConfig = {
+ apiKind: "installments",
+ title: "Pagos",
+ fields: [
+ { key: "sequence", label: "Sec.", type: "number" },
+ { key: "amount", label: "Monto", type: "number" },
+ { key: "currency", label: "Moneda", type: "select",
+ options: [{ value: "MXN", label: "MXN" }, { value: "USD", label: "USD" }] },
+ { key: "dueDate", label: "Vence", type: "date" },
+ { key: "paidDate", label: "Pagado", type: "date" },
+ { key: "checkNumber", label: "Cheque" },
+ { key: "isCash", label: "Efectivo", type: "checkbox" },
+ ],
+ };
+ const VEHICLES: ChildConfig = {
+ apiKind: "vehicles",
+ title: "Vehículos",
+ fields: [
+ { key: "make", label: "Marca" },
+ { key: "model", label: "Modelo" },
+ { key: "modelYear", label: "Año" },
+ { key: "licensePlate", label: "Placa" },
+ { key: "vinNumber", label: "VIN" },
+ { key: "stateCode", label: "Estado" },
+ ],
+ };
+ const DRIVERS: ChildConfig = {
+ apiKind: "drivers",
+ title: "Conductores",
+ fields: [
+ { key: "fullName", label: "Nombre" },
+ { key: "birthDate", label: "Nacimiento", type: "date" },
+ { key: "sex", label: "Sexo" },
+ { key: "occupation", label: "Ocupación" },
+ { key: "licenseNumber", label: "Licencia" },
+ { key: "licenseState", label: "Estado" },
+ ],
+ };
+ const BENEFICIARIES: ChildConfig = {
+ apiKind: "beneficiaries",
+ title: "Beneficiarios",
+ fields: [
+ { key: "name", label: "Nombre" },
+ { key: "phone", label: "Teléfono" },
+ { key: "email", label: "Correo" },
+ { key: "address", label: "Dirección" },
+ ],
+ };
+ const CLAIMS: ChildConfig = {
+ apiKind: "claims",
+ title: "Siniestros",
+ fields: [
+ { key: "claimType", label: "Tipo" },
+ { key: "incidentDate", label: "Fecha", type: "date" },
+ { key: "description", label: "Descripción" },
+ { key: "adjusterId", label: "Ajustador", type: "select",
+ options: adjusters.map((a) => ({ value: a.id, label: a.name ?? a.company ?? a.id })) },
+ { key: "claimedAmount", label: "Reclamado", type: "number" },
+ { key: "settledAmount", label: "Pagado", type: "number" },
+ { key: "resolved", label: "Resuelto", type: "checkbox" },
+ ],
+ };
+
+ const bind = (cfg: ChildConfig, rows: Record[]) => (
+ { await addPolicyChild(data.id, cfg.apiKind, p); onChange(); }}
+ onSave={async (cid, p) => { await updatePolicyChild(data.id, cfg.apiKind, cid, p); onChange(); }}
+ onRemove={async (cid) => { await removePolicyChild(data.id, cfg.apiKind, cid); onChange(); }}
+ />
+ );
+
+ return (
+
+
+
+
Administrar detalles
+
+ {bind(INSTALLMENTS, data.installments as unknown as Record[])}
+ {bind(VEHICLES, data.vehicles as unknown as Record[])}
+ {bind(DRIVERS, data.insuredDrivers as unknown as Record[])}
+ {bind(BENEFICIARIES, data.beneficiaries as unknown as Record[])}
+ {bind(CLAIMS, data.claims as unknown as Record[])}
+
+ );
+}
+
function BackLink() {
return (
diff --git a/apps/web/src/app/polizas/nuevo/page.tsx b/apps/web/src/app/polizas/nuevo/page.tsx
new file mode 100644
index 0000000..b607d79
--- /dev/null
+++ b/apps/web/src/app/polizas/nuevo/page.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import { Suspense } from "react";
+import Link from "next/link";
+import { useSearchParams } from "next/navigation";
+import { AppShell } from "@/components/AppShell";
+import { PolicyForm } from "@/components/PolicyForm";
+import { useCan } from "@/lib/abilities";
+
+export default function NuevaPolizaPage() {
+ return (
+
+
+
+
+
+ );
+}
+
+function NuevaPoliza() {
+ const allowed = useCan("policy:create");
+ const params = useSearchParams();
+ const customerId = params.get("customerId") ?? undefined;
+ const customerName = params.get("customerName") ?? undefined;
+
+ return (
+ <>
+
+ ← Pólizas
+
Nueva póliza
+
+ {allowed ? (
+
+ ) : (
+
+ No tiene permisos para crear pólizas.
+
+ )}
+ >
+ );
+}
diff --git a/apps/web/src/app/polizas/page.tsx b/apps/web/src/app/polizas/page.tsx
index b03a2de..e480f05 100644
--- a/apps/web/src/app/polizas/page.tsx
+++ b/apps/web/src/app/polizas/page.tsx
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { AppShell } from "@/components/AppShell";
+import { useCan } from "@/lib/abilities";
import {
EXPIRY_WINDOW_DAYS,
getPolicyFacets,
@@ -54,6 +55,7 @@ export default function PolizasPage() {
}
function PolizasBrowser() {
+ const canCreate = useCan("policy:create");
const [stats, setStats] = useState(null);
const [facets, setFacets] = useState(null);
@@ -122,7 +124,13 @@ function PolizasBrowser() {
<>
Cartera de seguros
-
Pólizas
+
+
Pólizas
+
+ {canCreate && (
+ + Nueva póliza
+ )}
+
;
+
+function toDateInput(v: unknown): string {
+ if (!v || typeof v !== "string") return "";
+ const d = new Date(v);
+ return isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
+}
+
+/** Build editable values for a field from an existing row (or blank). */
+function rowToValues(fields: FieldDef[], row?: Record): RowValues {
+ const v: RowValues = {};
+ for (const f of fields) {
+ const raw = row?.[f.key];
+ if (f.type === "checkbox") v[f.key] = !!raw;
+ else if (f.type === "date") v[f.key] = toDateInput(raw);
+ else v[f.key] = raw == null ? "" : String(raw);
+ }
+ return v;
+}
+
+/** Coerce editable values into an API payload (numbers/blanks handled). */
+function valuesToPayload(fields: FieldDef[], v: RowValues): Record {
+ const out: Record = {};
+ for (const f of fields) {
+ const val = v[f.key];
+ if (f.type === "checkbox") out[f.key] = !!val;
+ else if (f.type === "number") {
+ const s = String(val).trim();
+ out[f.key] = s === "" ? undefined : Number(s);
+ } else {
+ const s = String(val).trim();
+ out[f.key] = s === "" ? undefined : s;
+ }
+ }
+ return out;
+}
+
+/**
+ * Generic add/edit/remove editor for a policy's child collection. The parent
+ * owns the API calls (so it can reload the policy afterward); this component is
+ * pure UI over `rows` plus add/save/remove callbacks.
+ */
+export function ChildCollection({
+ config,
+ rows,
+ canEdit,
+ onAdd,
+ onSave,
+ onRemove,
+}: {
+ config: ChildConfig;
+ rows: Record[];
+ canEdit: boolean;
+ onAdd: (payload: Record) => Promise;
+ onSave: (id: string, payload: Record) => Promise;
+ onRemove: (id: string) => Promise;
+}) {
+ const [editingId, setEditingId] = useState(null);
+ const [adding, setAdding] = useState(false);
+ const [values, setValues] = useState({});
+ const [busy, setBusy] = useState(false);
+
+ function startAdd() {
+ setEditingId(null);
+ setAdding(true);
+ setValues(rowToValues(config.fields));
+ }
+ function startEdit(row: Record) {
+ setAdding(false);
+ setEditingId(String(row.id));
+ setValues(rowToValues(config.fields, row));
+ }
+ function cancel() {
+ setAdding(false);
+ setEditingId(null);
+ }
+
+ async function submit() {
+ setBusy(true);
+ try {
+ const payload = valuesToPayload(config.fields, values);
+ if (editingId) await onSave(editingId, payload);
+ else await onAdd(payload);
+ cancel();
+ } catch (e) {
+ window.alert((e as Error)?.message ?? "No se pudo guardar.");
+ } finally {
+ setBusy(false);
+ }
+ }
+
+ async function remove(id: string) {
+ if (!window.confirm("¿Eliminar este registro?")) return;
+ try {
+ await onRemove(id);
+ } catch (e) {
+ window.alert((e as Error)?.message ?? "No se pudo eliminar.");
+ }
+ }
+
+ function editor() {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {config.title}
+ {rows.length}
+
+ {canEdit && !adding && editingId === null && (
+
+ )}
+
+
+ {rows.length === 0 && !adding ? (
+
Sin registros.
+ ) : (
+
+
+
+
+ {config.fields.map((f) => (
+ | {f.label} |
+ ))}
+ {canEdit && Acciones | }
+
+
+
+ {rows.map((row) => (
+
+ {config.fields.map((f) => (
+ | {cellText(f, row[f.key])} |
+ ))}
+ {canEdit && (
+
+
+
+
+
+ |
+ )}
+
+ ))}
+
+
+
+ )}
+
+ {(adding || editingId !== null) && editor()}
+
+ );
+}
+
+function cellText(f: FieldDef, raw: unknown): string {
+ if (f.type === "checkbox") return raw ? "Sí" : "No";
+ if (f.type === "date") return toDateInput(raw) || "—";
+ if (f.type === "select") {
+ const opt = f.options?.find((o) => o.value === String(raw));
+ return opt ? opt.label : "—";
+ }
+ return raw == null || raw === "" ? "—" : String(raw);
+}
diff --git a/apps/web/src/components/CustomerPicker.tsx b/apps/web/src/components/CustomerPicker.tsx
new file mode 100644
index 0000000..b54e262
--- /dev/null
+++ b/apps/web/src/components/CustomerPicker.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { listCustomers } from "@/lib/api";
+import type { CustomerListItem } from "@/lib/types";
+
+/**
+ * Debounced customer search + select. Reports the chosen customer's id and name
+ * upward. Used when creating a policy that isn't started from a customer page.
+ */
+export function CustomerPicker({
+ value,
+ valueName,
+ onPick,
+}: {
+ value: string;
+ valueName?: string;
+ onPick: (id: string, name: string) => void;
+}) {
+ const [query, setQuery] = useState("");
+ const [results, setResults] = useState([]);
+ const [open, setOpen] = useState(false);
+ const debounce = useRef>();
+
+ useEffect(() => {
+ if (debounce.current) clearTimeout(debounce.current);
+ if (query.trim().length < 2) {
+ setResults([]);
+ return;
+ }
+ debounce.current = setTimeout(() => {
+ listCustomers({ query, pageSize: 8 })
+ .then((r) => {
+ setResults(r.items);
+ setOpen(true);
+ })
+ .catch(() => setResults([]));
+ }, 260);
+ return () => {
+ if (debounce.current) clearTimeout(debounce.current);
+ };
+ }, [query]);
+
+ return (
+
+ {value ? (
+
+ {valueName ?? "Cliente seleccionado"}
+
+
+ ) : (
+ <>
+
setQuery(e.target.value)}
+ onFocus={() => results.length && setOpen(true)}
+ />
+ {open && results.length > 0 && (
+
+ {results.map((c) => (
+ -
+
+
+ ))}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/apps/web/src/components/PolicyForm.tsx b/apps/web/src/components/PolicyForm.tsx
new file mode 100644
index 0000000..2880db8
--- /dev/null
+++ b/apps/web/src/components/PolicyForm.tsx
@@ -0,0 +1,292 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { CustomerPicker } from "@/components/CustomerPicker";
+import { createPolicy, getLookups, updatePolicy } from "@/lib/api";
+import type {
+ Currency,
+ LookupsResponse,
+ PolicyDetail,
+ PolicyInput,
+} from "@/lib/types";
+
+function toDateInput(v: string | null | undefined): string {
+ if (!v) return "";
+ const d = new Date(v);
+ return isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
+}
+function numOrUndef(v: string): number | undefined {
+ const t = v.trim();
+ if (t === "") return undefined;
+ const n = Number(t);
+ return isNaN(n) ? undefined : n;
+}
+function s(v: string): string | undefined {
+ const t = v.trim();
+ return t === "" ? undefined : t;
+}
+
+type V = {
+ policyNumber: string;
+ policyTypeId: string;
+ insuranceProviderId: string;
+ agentName: string;
+ policyDate: string;
+ policyFrom: string;
+ policyTo: string;
+ netPremium: string;
+ policyFee: string;
+ brokerFee: string;
+ commission: string;
+ currency: Currency;
+ liquidated: boolean;
+ liquidationNumber: string;
+ liquidationDate: string;
+ endorsement: boolean;
+ observations: string;
+ notes: string;
+};
+
+function initial(p?: PolicyDetail): V {
+ return {
+ policyNumber: p?.policyNumber ?? "",
+ policyTypeId: p?.policyType?.id ?? "",
+ insuranceProviderId: p?.insuranceProvider?.id ?? "",
+ agentName: p?.agentName ?? "",
+ policyDate: toDateInput(p?.policyDate),
+ policyFrom: toDateInput(p?.policyFrom),
+ policyTo: toDateInput(p?.policyTo),
+ netPremium: p?.netPremium != null ? String(p.netPremium) : "",
+ policyFee: p?.policyFee != null ? String(p.policyFee) : "",
+ brokerFee: p?.brokerFee != null ? String(p.brokerFee) : "",
+ commission: p?.commission != null ? String(p.commission) : "",
+ currency: (p?.currency as Currency) ?? "MXN",
+ liquidated: p?.liquidated ?? false,
+ liquidationNumber: p?.liquidationNumber ?? "",
+ liquidationDate: toDateInput(p?.liquidationDate),
+ endorsement: p?.endorsement ?? false,
+ observations: p?.observations ?? "",
+ notes: p?.notes ?? "",
+ };
+}
+
+export function PolicyForm({
+ policy,
+ fixedCustomerId,
+ fixedCustomerName,
+}: {
+ policy?: PolicyDetail;
+ fixedCustomerId?: string;
+ fixedCustomerName?: string;
+}) {
+ const router = useRouter();
+ const editing = !!policy;
+ const [v, setV] = useState(() => initial(policy));
+ const [lookups, setLookups] = useState(null);
+ const [customerId, setCustomerId] = useState(
+ policy?.customer.id ?? fixedCustomerId ?? "",
+ );
+ const [customerName, setCustomerName] = useState(
+ policy?.customer.name ?? fixedCustomerName ?? "",
+ );
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ getLookups().then(setLookups).catch(() => setLookups(null));
+ }, []);
+
+ function set(k: K, val: V[K]) {
+ setV((p) => ({ ...p, [k]: val }));
+ }
+
+ async function submit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!customerId) {
+ setError("Seleccione un cliente.");
+ return;
+ }
+ setSaving(true);
+ setError(null);
+ const base = {
+ policyNumber: v.policyNumber.trim(),
+ policyTypeId: s(v.policyTypeId),
+ insuranceProviderId: s(v.insuranceProviderId),
+ agentName: s(v.agentName),
+ policyDate: s(v.policyDate),
+ policyFrom: s(v.policyFrom),
+ policyTo: s(v.policyTo),
+ netPremium: numOrUndef(v.netPremium),
+ policyFee: numOrUndef(v.policyFee),
+ brokerFee: numOrUndef(v.brokerFee),
+ commission: numOrUndef(v.commission),
+ currency: v.currency,
+ liquidated: v.liquidated,
+ liquidationNumber: s(v.liquidationNumber),
+ liquidationDate: s(v.liquidationDate),
+ endorsement: v.endorsement,
+ observations: s(v.observations),
+ notes: s(v.notes),
+ };
+ try {
+ if (editing) {
+ const saved = await updatePolicy(policy!.id, base);
+ router.push(`/polizas/${saved.id}`);
+ } else {
+ const payload: PolicyInput = { ...base, customerId };
+ const saved = await createPolicy(payload);
+ router.push(`/polizas/${saved.id}`);
+ }
+ } catch (e2) {
+ setError((e2 as Error)?.message ?? "No se pudo guardar la póliza.");
+ setSaving(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
index ef38d6a..dcb43e6 100644
--- a/apps/web/src/lib/api.ts
+++ b/apps/web/src/lib/api.ts
@@ -26,10 +26,12 @@ import type {
MovementSort,
PolicyDetail,
PolicyFacets,
+ PolicyInput,
PolicyListResponse,
PolicySort,
PolicyStats,
PolicyStatus,
+ LookupsResponse,
PropertyDetail,
PropertyFacets,
PropertyListResponse,
@@ -208,6 +210,83 @@ export function getPolicy(
return apiFetch(`/policies/${id}?days=${days}`);
}
+export function createPolicy(input: PolicyInput): Promise {
+ return apiFetch("/policies", {
+ method: "POST",
+ body: JSON.stringify(input),
+ });
+}
+export function updatePolicy(
+ id: string,
+ input: Partial,
+): Promise {
+ return apiFetch(`/policies/${id}`, {
+ method: "PATCH",
+ body: JSON.stringify(input),
+ });
+}
+export function archivePolicy(id: string): Promise {
+ return apiFetch(`/policies/${id}`, { method: "DELETE" });
+}
+export function restorePolicy(id: string): Promise {
+ return apiFetch(`/policies/${id}/restore`, { method: "POST" });
+}
+
+// Generic policy-child CRUD. `kind` is the URL segment
+// (installments|vehicles|drivers|beneficiaries|claims).
+export function addPolicyChild(
+ policyId: string,
+ kind: string,
+ input: T,
+): Promise {
+ return apiFetch(`/policies/${policyId}/${kind}`, {
+ method: "POST",
+ body: JSON.stringify(input),
+ });
+}
+export function updatePolicyChild(
+ policyId: string,
+ kind: string,
+ childId: string,
+ input: T,
+): Promise {
+ return apiFetch(`/policies/${policyId}/${kind}/${childId}`, {
+ method: "PATCH",
+ body: JSON.stringify(input),
+ });
+}
+export function removePolicyChild(
+ policyId: string,
+ kind: string,
+ childId: string,
+): Promise {
+ return apiFetch(`/policies/${policyId}/${kind}/${childId}`, {
+ method: "DELETE",
+ });
+}
+
+/* ------------------------------------------------- Lookups (insurance ref) */
+
+export function getLookups(): Promise {
+ return apiFetch("/lookups");
+}
+export function createLookup(kind: string, input: unknown): Promise {
+ return apiFetch(`/lookups/${kind}`, { method: "POST", body: JSON.stringify(input) });
+}
+export function updateLookup(
+ kind: string,
+ id: string,
+ input: unknown,
+): Promise {
+ return apiFetch(`/lookups/${kind}/${id}`, {
+ method: "PATCH",
+ body: JSON.stringify(input),
+ });
+}
+export function removeLookup(kind: string, id: string): Promise {
+ return apiFetch(`/lookups/${kind}/${id}`, { method: "DELETE" });
+}
+
/* ----------------------------------------------------- Utilities module */
export interface PropertyQuery {
diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts
index ea85e95..3036a3d 100644
--- a/apps/web/src/lib/types.ts
+++ b/apps/web/src/lib/types.ts
@@ -155,17 +155,23 @@ export interface Vehicle {
id: string;
make: string | null;
model: string | null;
- modelYear: number | null;
+ modelYear: number | string | null;
licensePlate: string | null;
bodyType: string | null;
engineNumber?: string | null;
vinNumber?: string | null;
+ stateCode?: string | null;
+ notes?: string | null;
}
export interface InsuredDriver {
id: string;
fullName: string | null;
licenseNumber: string | null;
+ birthDate?: string | null;
+ sex?: string | null;
+ occupation?: string | null;
+ licenseState?: string | null;
}
export interface Beneficiary {
@@ -237,6 +243,106 @@ export interface PolicyListItem {
vehicleCount: number;
installmentCount: number;
documentCount: number;
+ archived: boolean;
+}
+
+/** Editable policy-header fields — shared by the create/edit form and API. */
+export interface PolicyInput {
+ policyNumber: string;
+ customerId: string;
+ policyTypeId?: string;
+ insuranceProviderId?: string;
+ agentName?: string;
+ policyDate?: string;
+ policyFrom?: string;
+ policyTo?: string;
+ coveragePeriodDays?: number;
+ netPremium?: number;
+ policyFee?: number;
+ brokerFee?: number;
+ commission?: number;
+ total?: number;
+ currency?: Currency;
+ observations?: string;
+ notes?: string;
+ endorsement?: boolean;
+ liquidated?: boolean;
+ liquidationNumber?: string;
+ liquidationDate?: string;
+}
+
+export interface InstallmentInput {
+ sequence: number;
+ amount?: number;
+ currency?: Currency;
+ dueDate?: string;
+ paidDate?: string;
+ checkNumber?: string;
+ isCash?: boolean;
+}
+export interface VehicleInput {
+ make?: string;
+ model?: string;
+ modelYear?: string;
+ bodyType?: string;
+ engineNumber?: string;
+ licensePlate?: string;
+ vinNumber?: string;
+ stateCode?: string;
+ notes?: string;
+}
+export interface DriverInput {
+ fullName?: string;
+ birthDate?: string;
+ sex?: string;
+ occupation?: string;
+ licenseNumber?: string;
+ licenseState?: string;
+}
+export interface BeneficiaryInput {
+ name?: string;
+ address?: string;
+ phone?: string;
+ email?: string;
+}
+export interface ClaimInput {
+ claimType?: string;
+ incidentDate?: string;
+ reportedDate?: string;
+ description?: string;
+ adjusterId?: string;
+ claimedAmount?: number;
+ settledAmount?: number;
+ settlementDate?: string;
+ checkNumber?: string;
+ resolved?: boolean;
+ resolutionNotes?: string;
+}
+
+/* Lookups (insurance reference data) */
+export interface ProviderRow {
+ id: string;
+ name: string;
+ _count?: { policies: number };
+}
+export interface PolicyTypeRow {
+ id: string;
+ name: string;
+ shortDescription: string | null;
+ _count?: { policies: number };
+}
+export interface AdjusterRow {
+ id: string;
+ company: string | null;
+ city: string | null;
+ name: string | null;
+ phone: string | null;
+ beeper: string | null;
+}
+export interface LookupsResponse {
+ providers: ProviderRow[];
+ types: PolicyTypeRow[];
+ adjusters: AdjusterRow[];
}
export interface PolicyListResponse {
@@ -295,6 +401,10 @@ export interface Claim {
settlementDate: string | null;
status?: string | null;
adjuster: Adjuster | null;
+ adjusterId?: string | null;
+ checkNumber?: string | null;
+ resolved?: boolean;
+ resolutionNotes?: string | null;
}
export interface PolicyCustomerRef {
@@ -333,6 +443,7 @@ export interface PolicyDetail {
legacySourceDb: string | null;
legacySourceTable: string | null;
legacyId: string | null;
+ archivedAt: string | null;
status: PolicyStatus;
daysToExpiry: number | null;
customer: PolicyCustomerRef;
diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma
index 2d21165..35e8917 100644
--- a/packages/database/prisma/schema.prisma
+++ b/packages/database/prisma/schema.prisma
@@ -165,6 +165,9 @@ model Policy {
liquidated Boolean @default(false)
liquidationNumber String?
liquidationDate DateTime?
+ // Soft-delete marker (see Customer.archivedAt). Never hard-delete migrated
+ // policy data; archiving hides it from default lists.
+ archivedAt DateTime?
legacySourceDb String?
legacySourceTable String?
legacyId String?