feat(policies): full CRUD + child editors + insurance lookups (plan phase 3)
Policy header, all five child collections, and the insurance reference catalogs become create/edit/delete-able on the RBAC foundation. API: - Policy gains archivedAt (soft-delete); list/browser default to archivedAt=null with ?includeArchived opt-in. - PoliciesService: header create/update/archive/restore (customer FK validated for a clean 404); add/update/remove for installments, vehicles, drivers, beneficiaries, claims — each scoped to its policy so one policy's id can't touch another's rows; lookups CRUD for providers, policy types, adjusters. - PoliciesController write routes: header create/update need STAFF+ (policy:create/update), archive/restore need MANAGER+ (policy:delete), every child route needs policy:update. New LookupsController at /lookups (read open; mutate needs lookup:manage / MANAGER+). Mutations audited. - DTOs (policy header, children, lookups); dates coerced; shared coerce.ts. Web: - Generic ChildCollection editor (config-driven add/edit/remove table), reused by both the policy detail child editors and the catalogs screen. - PolicyForm (header) with type/provider selects and a debounced CustomerPicker; /polizas/nuevo (accepts ?customerId prefill) and /polizas/[id]/editar. Policy detail: gated action bar (Editar/Archivar) + "Administrar detalles" child editors for all five collections. - /catalogos admin screen (aseguradoras/tipos/ajustadores), nav-gated on lookup:manage. "Nueva póliza" buttons on the list and on the customer detail (prefilled). api.ts + types for all of the above. Verified against dev: policy create (dates coerced, archivedAt null), installment/vehicle add, VIEWER child-add 403, cross-policy child guard 404, lookups CRUD with VIEWER 403 / MANAGER 201, archive drops from the default list and includeArchived surfaces it. Both apps compile clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ const NAV: { href: string; label: string; ability?: Ability }[] = [
|
||||
{ href: "/polizas", label: "Pólizas" },
|
||||
{ href: "/estado-cuenta", label: "Estado de cuenta" },
|
||||
{ href: "/banco", label: "Chequera" },
|
||||
{ href: "/catalogos", label: "Catálogos", ability: "lookup:manage" },
|
||||
{ href: "/usuarios", label: "Usuarios", ability: "user:manage" },
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
/** A single editable field in a child row. */
|
||||
export type FieldDef = {
|
||||
key: string;
|
||||
label: string;
|
||||
type?: "text" | "number" | "date" | "checkbox" | "select";
|
||||
options?: { value: string; label: string }[];
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export type ChildConfig = {
|
||||
/** URL segment: installments | vehicles | drivers | beneficiaries | claims */
|
||||
apiKind: string;
|
||||
title: string;
|
||||
fields: FieldDef[];
|
||||
};
|
||||
|
||||
type RowValues = Record<string, string | boolean>;
|
||||
|
||||
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<string, unknown>): 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<string, unknown> {
|
||||
const out: Record<string, unknown> = {};
|
||||
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<string, unknown>[];
|
||||
canEdit: boolean;
|
||||
onAdd: (payload: Record<string, unknown>) => Promise<void>;
|
||||
onSave: (id: string, payload: Record<string, unknown>) => Promise<void>;
|
||||
onRemove: (id: string) => Promise<void>;
|
||||
}) {
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [values, setValues] = useState<RowValues>({});
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
function startAdd() {
|
||||
setEditingId(null);
|
||||
setAdding(true);
|
||||
setValues(rowToValues(config.fields));
|
||||
}
|
||||
function startEdit(row: Record<string, unknown>) {
|
||||
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 (
|
||||
<div className="child-editor">
|
||||
<div className="form-grid">
|
||||
{config.fields.map((f) => (
|
||||
<label className="field" key={f.key}>
|
||||
<span className="field-label">{f.label}</span>
|
||||
{f.type === "checkbox" ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!values[f.key]}
|
||||
onChange={(e) => setValues({ ...values, [f.key]: e.target.checked })}
|
||||
/>
|
||||
) : f.type === "select" ? (
|
||||
<select
|
||||
className="select"
|
||||
value={String(values[f.key] ?? "")}
|
||||
onChange={(e) => setValues({ ...values, [f.key]: e.target.value })}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{f.options?.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
className="input"
|
||||
type={f.type === "number" ? "number" : f.type === "date" ? "date" : "text"}
|
||||
step={f.type === "number" ? "0.01" : undefined}
|
||||
value={String(values[f.key] ?? "")}
|
||||
onChange={(e) => setValues({ ...values, [f.key]: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<button type="button" className="btn btn-primary" onClick={submit} disabled={busy}>
|
||||
{busy ? "Guardando…" : editingId ? "Guardar" : "Agregar"}
|
||||
</button>
|
||||
<button type="button" className="btn btn-ghost" onClick={cancel}>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 14 }}>
|
||||
<div className="child-head">
|
||||
<h3 className="section-title" style={{ margin: 0 }}>
|
||||
{config.title}
|
||||
<span className="section-count"> {rows.length}</span>
|
||||
</h3>
|
||||
{canEdit && !adding && editingId === null && (
|
||||
<button type="button" className="btn btn-outline" onClick={startAdd}>
|
||||
+ Agregar
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{rows.length === 0 && !adding ? (
|
||||
<div className="empty-inline">Sin registros.</div>
|
||||
) : (
|
||||
<div className="tx-scroll">
|
||||
<table className="tx-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{config.fields.map((f) => (
|
||||
<th key={f.key}>{f.label}</th>
|
||||
))}
|
||||
{canEdit && <th className="num">Acciones</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr key={String(row.id)}>
|
||||
{config.fields.map((f) => (
|
||||
<td key={f.key}>{cellText(f, row[f.key])}</td>
|
||||
))}
|
||||
{canEdit && (
|
||||
<td>
|
||||
<div className="row-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost"
|
||||
onClick={() => startEdit(row)}
|
||||
>
|
||||
Editar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost"
|
||||
onClick={() => remove(String(row.id))}
|
||||
>
|
||||
Eliminar
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(adding || editingId !== null) && editor()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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<CustomerListItem[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const debounce = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
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 (
|
||||
<div className="picker">
|
||||
{value ? (
|
||||
<div className="picker-selected">
|
||||
<span>{valueName ?? "Cliente seleccionado"}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost"
|
||||
onClick={() => onPick("", "")}
|
||||
>
|
||||
Cambiar
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Buscar cliente por nombre…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => results.length && setOpen(true)}
|
||||
/>
|
||||
{open && results.length > 0 && (
|
||||
<ul className="picker-list">
|
||||
{results.map((c) => (
|
||||
<li key={c.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="picker-item"
|
||||
onClick={() => {
|
||||
onPick(c.id, c.name);
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
}}
|
||||
>
|
||||
{c.name}
|
||||
{c.city && <span className="muted"> · {c.city}</span>}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<V>(() => initial(policy));
|
||||
const [lookups, setLookups] = useState<LookupsResponse | null>(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<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getLookups().then(setLookups).catch(() => setLookups(null));
|
||||
}, []);
|
||||
|
||||
function set<K extends keyof V>(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 (
|
||||
<form onSubmit={submit}>
|
||||
{error && <div className="state-box state-error">{error}</div>}
|
||||
|
||||
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
|
||||
<h2 className="section-title" style={{ marginBottom: 14 }}>Datos de la póliza</h2>
|
||||
<div className="form-grid">
|
||||
<label className="field">
|
||||
<span className="field-label">Cliente *</span>
|
||||
{editing ? (
|
||||
<input className="input" value={customerName} disabled />
|
||||
) : (
|
||||
<CustomerPicker
|
||||
value={customerId}
|
||||
valueName={customerName}
|
||||
onPick={(id, name) => {
|
||||
setCustomerId(id);
|
||||
setCustomerName(name);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Número de póliza *</span>
|
||||
<input className="input" required value={v.policyNumber}
|
||||
onChange={(e) => set("policyNumber", e.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Tipo</span>
|
||||
<select className="select" value={v.policyTypeId}
|
||||
onChange={(e) => set("policyTypeId", e.target.value)}>
|
||||
<option value="">—</option>
|
||||
{lookups?.types.map((t) => (
|
||||
<option key={t.id} value={t.id}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Aseguradora</span>
|
||||
<select className="select" value={v.insuranceProviderId}
|
||||
onChange={(e) => set("insuranceProviderId", e.target.value)}>
|
||||
<option value="">—</option>
|
||||
{lookups?.providers.map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Agente</span>
|
||||
<input className="input" value={v.agentName}
|
||||
onChange={(e) => set("agentName", e.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Moneda</span>
|
||||
<select className="select" value={v.currency}
|
||||
onChange={(e) => set("currency", e.target.value as Currency)}>
|
||||
<option value="MXN">MXN</option>
|
||||
<option value="USD">USD</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
|
||||
<h2 className="section-title" style={{ marginBottom: 14 }}>Vigencia y prima</h2>
|
||||
<div className="form-grid">
|
||||
<label className="field">
|
||||
<span className="field-label">Emisión</span>
|
||||
<input className="input" type="date" value={v.policyDate}
|
||||
onChange={(e) => set("policyDate", e.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Desde</span>
|
||||
<input className="input" type="date" value={v.policyFrom}
|
||||
onChange={(e) => set("policyFrom", e.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Hasta</span>
|
||||
<input className="input" type="date" value={v.policyTo}
|
||||
onChange={(e) => set("policyTo", e.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Prima neta</span>
|
||||
<input className="input" type="number" step="0.01" value={v.netPremium}
|
||||
onChange={(e) => set("netPremium", e.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Derecho de póliza</span>
|
||||
<input className="input" type="number" step="0.01" value={v.policyFee}
|
||||
onChange={(e) => set("policyFee", e.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Comisión</span>
|
||||
<input className="input" type="number" step="0.01" value={v.commission}
|
||||
onChange={(e) => set("commission", e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
|
||||
<h2 className="section-title" style={{ marginBottom: 14 }}>Liquidación</h2>
|
||||
<div className="form-grid">
|
||||
<label className="field">
|
||||
<span className="field-label">Liquidada</span>
|
||||
<input type="checkbox" checked={v.liquidated}
|
||||
onChange={(e) => set("liquidated", e.target.checked)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Endoso</span>
|
||||
<input type="checkbox" checked={v.endorsement}
|
||||
onChange={(e) => set("endorsement", e.target.checked)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">No. liquidación</span>
|
||||
<input className="input" value={v.liquidationNumber}
|
||||
onChange={(e) => set("liquidationNumber", e.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span className="field-label">Fecha liquidación</span>
|
||||
<input className="input" type="date" value={v.liquidationDate}
|
||||
onChange={(e) => set("liquidationDate", e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
<label className="field" style={{ marginTop: 14 }}>
|
||||
<span className="field-label">Observaciones</span>
|
||||
<textarea className="input" rows={2} value={v.observations}
|
||||
onChange={(e) => set("observations", e.target.value)} />
|
||||
</label>
|
||||
<label className="field" style={{ marginTop: 12 }}>
|
||||
<span className="field-label">Notas</span>
|
||||
<textarea className="input" rows={2} value={v.notes}
|
||||
onChange={(e) => set("notes", e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<button type="submit" className="btn btn-primary" disabled={saving}>
|
||||
{saving ? "Guardando…" : editing ? "Guardar cambios" : "Crear póliza"}
|
||||
</button>
|
||||
<button type="button" className="btn btn-outline" onClick={() => router.back()}>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user