/* ============================================
Admin Modules — Empleados, Cheques
============================================ */
/* ============================================
EMPLEADOS
============================================ */
function Empleados() {
const D = window.DATA;
const [empleados, setEmpleados] = useState([]);
const [sel, setSel] = useState(null);
const [maquinariaOptions, setMaquinariaOptions] = useState(["—"]);
const [loteOptions, setLoteOptions] = useState(["—", "Galpón A", "Galpón B", "Galpón C", "Galpón D", "Taller propio", "Oficina", "Ruta — entregas"]);
useEffect(() => {
window.API.get("/empleados").then(r => {
if (r.data && r.data.length > 0) {
const mapped = r.data.map(window.API_MAP.empleado);
setEmpleados(mapped);
setSel(mapped[0].id);
}
});
window.API.get("/maquinas").then(r => {
if (r.data) setMaquinariaOptions(["—", ...r.data.map(m => m.nombre)]);
});
window.API.get("/lotes").then(r => {
if (r.data) setLoteOptions(["—", ...r.data.map(l => "Lote " + l.nombre), "Galpón A", "Galpón B", "Galpón C", "Galpón D", "Taller propio", "Oficina", "Ruta — entregas"]);
});
}, []);
const [payFor, setPayFor] = useState(null);
const [addTaskFor, setAddTaskFor] = useState(null);
const [editTask, setEditTask] = useState(null);
const [showNuevo, setShowNuevo] = useState(false);
const cur = empleados.find(e => e.id === sel) || null;
const ultimoPagoTotal = empleados.reduce((s, e) => s + (e.ultimoPago || 0), 0);
// maquinariaOptions y loteOptions se cargan desde la API en useEffect
/* Acciones */
const registrarPago = (empId, monto, fecha, concepto) => {
setEmpleados(emps => emps.map(e => e.id === empId
? { ...e, ultimoPago: monto, ultimoPagoFecha: fecha, ultimoConcepto: concepto }
: e
));
};
const agregarTarea = (empId, tarea) => {
setEmpleados(emps => emps.map(e => e.id === empId
? { ...e, tareas: [...e.tareas, tarea] }
: e
));
};
const editarTarea = (empId, idx, tarea) => {
setEmpleados(emps => emps.map(e => e.id === empId
? { ...e, tareas: e.tareas.map((t, i) => i === idx ? tarea : t) }
: e
));
};
const eliminarTarea = (empId, idx) => {
setEmpleados(emps => emps.map(e => e.id === empId
? { ...e, tareas: e.tareas.filter((_, i) => i !== idx) }
: e
));
};
if (empleados.length === 0 && sel === null) {
return (
Empleados
Sin empleados cargados
setShowNuevo(true)}> Agregar primer empleado
No hay empleados cargados. Usá "Agregar primer empleado" para comenzar.
{showNuevo &&
setShowNuevo(false)} onSave={(emp) => { setEmpleados(emps => [...emps, emp]); setSel(emp.id); setShowNuevo(false); }} />}
);
}
return (
Empleados
{empleados.length} empleados activos · pagos según modalidad de cada uno
window.toast("Generando recibos de sueldo del último período...")}> Recibos
cur && setPayFor(cur)}>
Registrar pago
setShowNuevo(true)}>
Agregar empleado
s + e.tareas.length, 0))} delta="esta semana" deltaDir="info" hint="distribuidas entre los 3" />
{/* Lista de empleados */}
Plantilla
Tocá un empleado para ver detalle
{empleados.map(e => (
setSel(e.id)}
style={{
display: "flex", gap: 12, alignItems: "center", textAlign: "left",
padding: 12, borderRadius: 10,
background: sel === e.id ? "var(--green-50)" : "transparent",
border: sel === e.id ? "1px solid var(--green-400)" : "1px solid transparent",
width: "100%", cursor: "pointer",
transition: "background 140ms",
}}>
{e.avatar}
último pago
{D.fmtARS(e.ultimoPago || 0)}
))}
{/* Detalle */}
{cur.avatar}
{cur.nombre}
{cur.rol}
Ingreso {cur.ingreso}
{cur.ciudad}
{cur.edad} años
{/* Pago block — sólo último pago + modalidad */}
Último pago registrado
{D.fmtARS(cur.ultimoPago || 0)}
setPayFor(cur)} style={{ background: "rgba(255,255,255,0.14)", color: "#fff", flexShrink: 0 }}>
Registrar pago
Modalidad
{cur.modalidadPago}
Fecha último pago
{cur.ultimoPagoFecha}
Cuenta de depósito
{cur.cuenta}
{/* Tareas + add */}
Tareas asignadas · {cur.tareas.length}
setAddTaskFor(cur)}>
Agregar tarea
{cur.tareas.map((t, i) => (
{t.descripcion}
{t.maquinaria && t.maquinaria !== "—" && (
{t.maquinaria}
)}
{t.lote && t.lote !== "—" && (
{t.lote}
)}
setEditTask({ empId: cur.id, taskIdx: i })} title="Editar">
eliminarTarea(cur.id, i)} title="Eliminar">
))}
{cur.tareas.length === 0 && (
Sin tareas asignadas. Tocá "Agregar tarea" para crear una.
)}
{payFor && (
setPayFor(null)}
onSave={(monto, fecha, concepto) => {
registrarPago(payFor.id, monto, fecha, concepto);
setPayFor(null);
}}
/>
)}
{addTaskFor && (
setAddTaskFor(null)}
onSave={(tarea) => {
agregarTarea(addTaskFor.id, tarea);
setAddTaskFor(null);
}}
/>
)}
{editTask && (
e.id === editTask.empId)}
tarea={empleados.find(e => e.id === editTask.empId).tareas[editTask.taskIdx]}
maquinariaOptions={maquinariaOptions}
loteOptions={loteOptions}
onClose={() => setEditTask(null)}
onSave={(tarea) => {
editarTarea(editTask.empId, editTask.taskIdx, tarea);
setEditTask(null);
}}
/>
)}
{showNuevo && (
setShowNuevo(false)}
onSave={(emp) => {
setEmpleados(emps => [...emps, emp]);
setSel(emp.id);
setShowNuevo(false);
}}
/>
)}
);
}
/* Modal: registrar pago — abierto, sin "sueldo fijo" */
function PagoModal({ empleado, onClose, onSave }) {
const D = window.DATA;
const [monto, setMonto] = useState(empleado.ultimoPago || 0);
const [fecha, setFecha] = useState("15/05/2026");
const [concepto, setConcepto] = useState("");
const [done, setDone] = useState(false);
const submit = (e) => {
e.preventDefault();
onSave(+monto, fecha, concepto);
setDone(true);
};
return (
e.stopPropagation()}>
Registrar pago
{empleado.nombre}
Modalidad de pago acordada
{empleado.modalidadPago}
);
}
/* Modal: agregar / editar tarea */
function TareaModal({ empleado, tarea, maquinariaOptions, loteOptions, onClose, onSave }) {
const [descripcion, setDescripcion] = useState(tarea?.descripcion || "");
const [maquinaria, setMaquinaria] = useState(tarea?.maquinaria || "—");
const [lote, setLote] = useState(tarea?.lote || "—");
const submit = (e) => {
e.preventDefault();
if (!descripcion.trim()) return;
onSave({ descripcion: descripcion.trim(), maquinaria, lote });
};
return (
e.stopPropagation()}>
{tarea ? "Editar tarea" : "Nueva tarea"}
{empleado.nombre}
);
}
/* Modal: agregar nuevo empleado */
function NuevoEmpleadoModal({ onClose, onSave }) {
const [nombre, setNombre] = useState("");
const [rol, setRol] = useState("");
const [telefono, setTelefono] = useState("");
const [ciudad, setCiudad] = useState("");
const [edad, setEdad] = useState("");
const [ingreso, setIngreso] = useState("20/05/2026");
const [modalidad, setModalidad] = useState("Mensual fijo");
const [cuenta, setCuenta] = useState("");
const [done, setDone] = useState(false);
const submit = (e) => {
e.preventDefault();
const avatar = nombre.trim().split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase();
const emp = {
id: "e" + Date.now(),
nombre: nombre.trim(),
rol: rol.trim(),
avatar,
ingreso,
ciudad: ciudad.trim() || "—",
edad: +edad || 0,
ultimoPago: 0,
ultimoPagoFecha: "—",
ultimoConcepto: "",
cuenta: cuenta.trim() || "Sin cuenta registrada",
modalidadPago: modalidad,
telefono: telefono.trim() || "—",
tareas: [],
};
setDone(true);
setTimeout(() => onSave(emp), 800);
};
return (
e.stopPropagation()}>
Nuevo empleado
Agregar a la plantilla
{done ? (
Empleado agregado correctamente.
) : (
Modalidad de pago
setModalidad(e.target.value)}>
Mensual fijo
Quincenal fijo
Por porcentaje de producción
Por día trabajado
Por tarea / comisión
Cuenta de depósito
setCuenta(e.target.value)} placeholder="Ej: CVU Mercado Pago · 000-XXX" />
Cancelar
Agregar empleado
)}
);
}
/* ============================================
CHEQUES
============================================ */
function Cheques() {
const D = window.DATA;
const [tab, setTab] = useState("recibidos");
const [filter, setFilter] = useState("todos");
const [showNew, setShowNew] = useState(false);
const [detail, setDetail] = useState(null);
const [cheques, setCheques] = useState([]);
const recargar = () => {
window.API.get("/cheques").then(r => {
if (r.data) setCheques(r.data);
});
};
useEffect(() => { recargar(); }, []);
const recibidos = cheques.filter(c => c.tipo === "recibido");
const emitidos = cheques.filter(c => c.tipo === "emitido");
// Stats
const totalRecibidos = recibidos.filter(c => c.monto).reduce((s, c) => s + c.monto, 0);
const enCarteraRecibidos = recibidos.filter(c => c.estado === "en_cartera" && c.monto).reduce((s, c) => s + c.monto, 0);
const totalEmitidos = emitidos.filter(c => c.monto).reduce((s, c) => s + c.monto, 0);
const pendientesEmitidos = emitidos.filter(c => c.estado === "pendiente" && c.monto).reduce((s, c) => s + c.monto, 0);
const dataCur = tab === "recibidos" ? recibidos : emitidos;
const filtered = dataCur.filter(c => filter === "todos" ? true : c.estado === filter);
// Próximos vencimientos ordenados por fecha de cobro
const proximos = [...emitidos.filter(e => e.estado === "pendiente"), ...recibidos.filter(r => r.estado === "en_cartera")]
.filter(c => c.fecha_cobro)
.sort((a, b) => a.fecha_cobro.split("/").reverse().join("").localeCompare(b.fecha_cobro.split("/").reverse().join("")))
.slice(0, 5);
return (
Cheques
Gestión de cheques emitidos y recibidos
window.toast("Listado de cheques exportado")}> Exportar
setShowNew(true)}>
{tab === "recibidos" ? "Registrar cheque recibido" : "Emitir cheque"}
c.estado==='en_cartera').length} sin depositar`} deltaDir="info" hint="cobro pendiente" />
c.estado==='pendiente').length} sin cobrar`} deltaDir="flat" hint="próx. vencimientos" />
{/* Tabs */}
{ setTab("recibidos"); setFilter("todos"); }} label="Recibidos" count={recibidos.length} />
{ setTab("emitidos"); setFilter("todos"); }} label="Emitidos" count={emitidos.length} />
{(tab === "recibidos"
? [["todos","Todos"], ["en cartera","En cartera"], ["depositado","Depositado"]]
: [["todos","Todos"], ["pendiente","Pendientes"], ["cobrado","Cobrados"]]
).map(([k, l]) => (
setFilter(k)}>{l}
))}
Nº
Banco / Sucursal
{tab === "recibidos" ? "Librador" : "Beneficiario"}
CUIT
Concepto
Monto
Cobro / Vto.
Estado
{filtered.length === 0 && (
Sin cheques. Usá el botón para registrar uno.
)}
{filtered.map(c => {
const estadoCls =
c.estado === "cobrado" || c.estado === "depositado" ? "b-ok" :
c.estado === "en_cartera" ? "b-info" :
c.estado === "endosado" ? "b-gold" :
"b-warn";
const estadoLabel = {
en_cartera: "En cartera", depositado: "Depositado",
endosado: "Endosado", cobrado: "Cobrado",
pendiente: "Pendiente", rechazado: "Rechazado",
}[c.estado] || c.estado;
return (
setDetail({ ...c, _tipo: tab })}
onMouseEnter={e => e.currentTarget.style.background = "var(--green-50)"}
onMouseLeave={e => e.currentTarget.style.background = ""}>
{c.numero ? `#${c.numero}` : "—"}
{c.banco && {c.banco} }
{c.sucursal && {c.sucursal}
}
{!c.banco && "—"}
{tab === "recibidos" ? (c.origen || "—") : (c.destino || "—")}
{c.cuit || "—"}
{c.concepto || "—"}
{c.monto != null ? D.fmtARS(c.monto) : "—"}
{c.fecha_cobro || "—"}
{estadoLabel}
);
})}
Próximos vencimientos
Cobros y pagos por orden de fecha
{proximos.length}
{proximos.length === 0 && (
Sin vencimientos próximos.
)}
{proximos.map((c, i) => {
const isEmitido = c.tipo === "emitido";
return (
setDetail({ ...c, _tipo: isEmitido ? "emitidos" : "recibidos" })}>
{isEmitido ? "Pago a " + (c.destino || "—") : "Cobro de " + (c.origen || "—")}
{c.numero ? `#${c.numero}` : "S/N"} · {c.banco || "—"} · vence {c.fecha_cobro}
{c.monto != null ? `${isEmitido ? "−" : "+"}${D.fmtARS(c.monto)}` : "—"}
);
})}
Balance proyectado
{D.fmtARS(enCarteraRecibidos - pendientesEmitidos)}
= 0 ? "var(--green-400)" : "var(--bad)" }}>
Cobros en cartera − Pagos pendientes
{detail &&
setDetail(null)} onSuccess={recargar} />}
{showNew && setShowNew(false)} onSuccess={recargar} />}
);
}
function ChequeTab({ active, onClick, label, count }) {
return (
{label}
{count}
);
}
function ChequeDetail({ cheque, onClose }) {
const D = window.DATA;
const isEmitido = cheque._tipo === "emitidos";
return (
e.stopPropagation()} style={{ maxWidth: 480 }}>
Cheque {isEmitido ? "emitido" : "recibido"}
#{cheque.numero}
Páguese a la orden de
{isEmitido ? cheque.destino : cheque.origen}
Monto
{D.fmtARS(cheque.monto)}
{cheque.estado}} />
window.toast(`Imprimiendo cheque #${cheque.numero}`)}> Imprimir
{!isEmitido && cheque.estado === "en cartera" && (
{ window.toast(`Cheque #${cheque.numero} marcado como depositado`); }}> Depositar
)}
{isEmitido && cheque.estado === "pendiente" && (
{ window.toast(`Cheque #${cheque.numero} marcado como cobrado`); }}> Marcar cobrado
)}
);
}
function DetailField({ label, value, highlight, full }) {
return (
);
}
function ChequeForm({ tipo, onClose, onSuccess }) {
const isRec = tipo === "recibidos";
const D = window.DATA;
const [numero, setNumero] = useState("");
const [banco, setBanco] = useState("");
const [sucursal, setSucursal] = useState("");
const [cuit, setCuit] = useState("");
const [origen, setOrigen] = useState(""); // librador (recibido) o destino (emitido)
const [monto, setMonto] = useState("");
const [fechaCobro,setFechaCobro]= useState(new Date().toLocaleDateString("es-AR"));
const [concepto, setConcepto] = useState("");
const [loading, setLoading] = useState(false);
const [done, setDone] = useState(false);
const [err, setErr] = useState("");
const toISO = (str) => {
const [d, m, y] = (str || "").split("/");
return y ? `${y}-${m}-${d}` : null;
};
const submit = async (e) => {
e.preventDefault();
setLoading(true); setErr("");
const res = await window.API.post("/cheques", {
tipo: isRec ? "recibido" : "emitido",
numero: numero || null,
banco: banco || null,
sucursal: sucursal || null,
cuit: cuit || null,
origen: isRec ? (origen || null) : null,
destino: isRec ? null : (origen || null),
concepto: concepto || null,
monto: monto ? parseFloat(monto) : null,
fecha_cobro: toISO(fechaCobro) || null,
estado: isRec ? "en_cartera" : "pendiente",
});
setLoading(false);
if (res.id) {
setDone(true);
onSuccess && onSuccess();
setTimeout(onClose, 1400);
} else {
const msgs = res.errors ? Object.values(res.errors).flat().join(" ") : res.message;
setErr(msgs || "Error al guardar.");
}
};
return (
e.stopPropagation()}>
{isRec ? "Registrar cheque recibido" : "Emitir nuevo cheque"}
{done ? (
Cheque guardado correctamente.
) : (
{/* Fila 1: N° cheque + Banco */}
{/* Fila 2: Sucursal + CUIT */}
{/* Librador / Beneficiario */}
{isRec ? "Librador (quién lo emitió)" : "Beneficiario (a quién va)"}
setOrigen(e.target.value)}
placeholder={isRec ? "Ej: Tambo La Esperanza" : "Ej: YPF Sacanta"} />
{/* Monto + Fecha de cobro */}
{/* Concepto */}
Concepto
setConcepto(e.target.value)}
placeholder="Ej: Venta fardos mayo, pago combustible..." />
{err && {err}
}
Cancelar
{loading ? "Guardando..." : <> Guardar cheque>}
)}
);
}
Object.assign(window, { Empleados, Cheques });
/* ============================================
ASOCIACIÓN — lotes en sociedad, fardos compartidos y contabilidad
============================================ */
function Asociacion() {
const D = window.DATA;
// ── datos desde API ──────────────────────────────────────────────
const [lotesSociedad, setLotesSociedad] = useState([]);
const [fardos, setFardos] = useState([]);
const [sociosDB, setSociosDB] = useState([]); // empresas socias registradas en /api/proveedores
const [socioSel, setSocioSel] = useState(null);
const [showNuevoSocio,setShowNuevoSocio]= useState(false);
// movimientos locales hasta que se defina el módulo de precios
const [compras, setCompras] = useState([]);
const [ventas, setVentas] = useState([]);
const [showCompra, setShowCompra]= useState(false);
const [showVenta, setShowVenta] = useState(false);
const [tabMov, setTabMov] = useState("compras");
const recargar = () => {
window.API.get("/lotes").then(r => {
if (r.data) {
const soc = r.data.filter(l => l.tenencia === "sociedad").map(window.API_MAP.lote);
setLotesSociedad(soc);
if (!socioSel && soc.length > 0) setSocioSel(soc[0].socio);
}
});
window.API.get("/proveedores").then(r => {
if (r.data) setSociosDB(r.data.map(window.API_MAP.proveedor));
});
window.API.get("/fardos").then(r => {
if (r.data) setFardos(r.data.map(window.API_MAP.fardo));
});
};
useEffect(() => { recargar(); }, []);
// socios = unión de los registrados en DB + los que aparecen en lotes
const nombresDB = sociosDB.map(s => s.nombre);
const nombresLotes = lotesSociedad.map(l => l.socio).filter(Boolean);
const todosNombres = [...new Set([...nombresDB, ...nombresLotes])];
const socioActual = socioSel || todosNombres[0] || null;
const lotesSocio = lotesSociedad.filter(l => l.socio === socioActual);
const hasSocio = lotesSocio.reduce((s, l) => s + l.hectareas, 0);
// fardos cuyo origen coincide con algún lote de sociedad
const nombresSoc = lotesSociedad.map(l => l.nombre);
const fardosSoc = fardos.filter(f => nombresSoc.some(n => (f.origen || "").includes(n)));
const totalFardosSoc = fardosSoc.reduce((s, f) => s + f.stock, 0);
// movimientos del socio actual
const comprasSocio = compras.filter(c => c.socio === socioActual);
const ventasSocio = ventas.filter(v => v.socio === socioActual);
return (
Asociación
{todosNombres.length} {todosNombres.length === 1 ? "empresa socia" : "empresas socias"} ·
{lotesSociedad.length} lotes en sociedad ·
{lotesSociedad.reduce((s, l) => s + l.hectareas, 0).toFixed(0)} ha compartidas
window.toast("Historial de asociación exportado")}> Exportar
setShowNuevoSocio(true)}> Nuevo socio
setShowVenta(true)}> Registrar venta
setShowCompra(true)}> Compra a socio
{/* KPIs */}
s+l.hectareas,0).toFixed(0)+" ha"} deltaDir="info" hint="superficie total compartida" />
{/* ── Selector de socio ── */}
{todosNombres.length > 0 && (
{todosNombres.map(s => {
const tieneLotes = lotesSociedad.some(l => l.socio === s);
return (
setSocioSel(s)}>
{s}
{!tieneLotes && sin lotes }
);
})}
)}
{/* ── Lotes del socio seleccionado ── */}
{socioActual || "Sin socios"}
{lotesSocio.length} lotes · {hasSocio.toFixed(0)} ha totales
Sociedad
{/* Datos de contacto del socio si está registrado en DB */}
{(() => {
const info = sociosDB.find(s => s.nombre === socioActual);
if (!info) return null;
return (
{info.contacto && {info.contacto} }
{info.telefono && {info.telefono} }
{info.email && {info.email} }
{info.ciudad && {info.ciudad} }
{info.cuit && CUIT {info.cuit} }
);
})()}
{lotesSocio.length === 0 ? (
Sin lotes en sociedad cargados. Agregá lotes con tenencia "Sociedad" en la sección Producción.
) : (
Lote Has Participación Estado
{lotesSocio.map((l, i) => (
{l.hectareas}
{l.participacion
? {l.participacion}
: — }
))}
)}
{/* Fardos de esos lotes */}
Fardos de lotes en sociedad
{fardosSoc.length} tipos · {D.fmtNum(totalFardosSoc)} unidades
{fardosSoc.length === 0 ? (
No hay fardos cargados con origen en lotes de sociedad.
) : (
Fardo Tipo Origen Stock
{fardosSoc.map(f => (
{f.nombre}
{f.tipo}
{f.origen}
{D.fmtNum(f.stock)}
))}
)}
{/* ── Movimientos (compras y ventas) ── */}
{[["compras", "Compras al socio", comprasSocio.length], ["ventas", "Ventas a terceros", ventasSocio.length]].map(([k, l, cnt]) => (
setTabMov(k)} style={{
padding: "16px 14px",
borderBottom: tabMov === k ? "2px solid var(--green-800)" : "2px solid transparent",
color: tabMov === k ? "var(--admin-ink)" : "var(--admin-mute)",
fontWeight: tabMov === k ? 600 : 500,
fontSize: 14,
display: "flex", gap: 8, alignItems: "center",
}}>
{l}
{cnt}
))}
{tabMov === "compras" && (
<>
Fardos comprados al socio para reventa. Los precios se cargarán cuando se defina el módulo de costos.
{comprasSocio.length === 0 ? (
Sin compras registradas al socio.
setShowCompra(true)}>
Registrar primera compra
) : (
Fecha Producto Cantidad Pago Estado
{comprasSocio.map((c, i) => (
{c.fecha}
{c.producto}
{c.cantidad}
{c.formaPago}
{c.estado}
))}
)}
>
)}
{tabMov === "ventas" && (
<>
Fardos de lotes en sociedad vendidos a terceros. Se agregarán los precios en el módulo de ventas.
{ventasSocio.length === 0 ? (
Sin ventas registradas de lotes asociados.
setShowVenta(true)}>
Registrar venta
) : (
Fecha Comprador Producto Cantidad Estado
{ventasSocio.map((v, i) => (
{v.fecha}
{v.comprador}
{v.producto}
{v.cantidad}
{v.estado}
))}
)}
>
)}
{showNuevoSocio &&
setShowNuevoSocio(false)} onSuccess={(nombre) => { recargar(); setSocioSel(nombre); }} />}
{/* Modal: compra a socio */}
{showCompra && (
setShowCompra(false)}>
e.stopPropagation()}>
Compra de fardos al socio
setShowCompra(false)}>
{
e.preventDefault();
const fd = new FormData(e.target);
setCompras(prev => [...prev, {
socio: fd.get("socio"),
fecha: fd.get("fecha"),
producto: fd.get("producto"),
cantidad: fd.get("cantidad"),
formaPago: fd.get("formaPago"),
estado: "pendiente",
}]);
setTabMov("compras");
setSocioSel(fd.get("socio"));
setShowCompra(false);
window.toast("Compra registrada correctamente");
}}>
Empresa socia
{todosNombres.map(s => {s} )}
{todosNombres.length === 0 && Sin socios — registrá uno con "Nuevo socio" }
Producto / tipo de fardo
Forma de pago
Transferencia Cheque diferido Efectivo Cuenta corriente
El precio se completará cuando se defina la contabilidad de costos del módulo de asociación.
setShowCompra(false)}>Cancelar
Guardar compra
)}
{/* Modal: venta de fardos de lotes asociados */}
{showVenta && (
setShowVenta(false)}>
e.stopPropagation()}>
Venta de fardos asociados
setShowVenta(false)}>
{
e.preventDefault();
const fd = new FormData(e.target);
setVentas(prev => [...prev, {
socio: socioActual,
fecha: fd.get("fecha"),
comprador: fd.get("comprador"),
producto: fd.get("producto"),
cantidad: fd.get("cantidad"),
estado: "pendiente",
}]);
setTabMov("ventas");
setShowVenta(false);
window.toast("Venta registrada correctamente");
}}>
Lote / origen de los fardos
{lotesSocio.map(l => {l.nombre} · {l.socio} )}
{lotesSocio.length === 0 && Sin lotes — seleccioná un socio }
Tipo de fardo
Comprador / empresa destino
El precio de venta se completará en el módulo de contabilidad de asociación.
setShowVenta(false)}>Cancelar
Guardar venta
)}
);
}
function ModalNuevoSocio({ onClose, onSuccess }) {
const [nombre, setNombre] = useState("");
const [ciudad, setCiudad] = useState("");
const [contacto, setContacto] = useState("");
const [telefono, setTelefono] = useState("");
const [email, setEmail] = useState("");
const [cuit, setCuit] = useState("");
const [desde, setDesde] = useState(new Date().getFullYear().toString());
const [producto, setProducto] = useState("");
const [condicion, setCondicion] = useState("");
const [obs, setObs] = useState("");
const [loading, setLoading] = useState(false);
const [done, setDone] = useState(false);
const [err, setErr] = useState("");
const submit = async (e) => {
e.preventDefault();
setLoading(true); setErr("");
const res = await window.API.post("/proveedores", {
nombre,
ciudad: ciudad || null,
contacto: contacto || null,
telefono: telefono || null,
email: email || null,
cuit: cuit || null,
desde: desde ? parseInt(desde) : null,
producto_tipico: producto || null,
condicion_pago: condicion || null,
estado: "activo",
observaciones: obs || null,
});
setLoading(false);
if (res.id) {
setDone(true);
onSuccess && onSuccess(nombre);
setTimeout(onClose, 1400);
} else {
const msgs = res.errors ? Object.values(res.errors).flat().join(" ") : res.message;
setErr(msgs || "Error al guardar.");
}
};
return (
e.stopPropagation()}>
Nueva empresa socia
Registrar socio
{done ? (
Empresa socia "{nombre}" registrada correctamente.
) : (
Nombre de la empresa
setNombre(e.target.value)} required placeholder="Ej: La Pampita SA" autoFocus />
Observaciones
setObs(e.target.value)} placeholder="Acuerdos, porcentajes, notas..." />
{err && {err}
}
Cancelar
{loading ? "Guardando..." : <> Registrar socio>}
)}
);
}
Object.assign(window, { Asociacion });
/* ============================================
LOGÍSTICA — viajes de entrega y retiro (sin precios)
============================================ */
function Logistica() {
const D = window.DATA;
const [filtroTipo, setFiltroTipo] = useState("todos");
const [filtroEstado, setFiltroEstado] = useState("todos");
const [showNew, setShowNew] = useState(false);
const [detail, setDetail] = useState(null);
const [VIAJES, setVIAJES] = useState([]);
const recargar = () => {
window.API.get("/viajes").then(r => {
if (r.data) setVIAJES(r.data.map(window.API_MAP.viaje));
});
};
useEffect(() => { recargar(); }, []);
const filtrados = VIAJES
.filter(v => filtroTipo === "todos" ? true : v.tipo === filtroTipo)
.filter(v => filtroEstado === "todos" ? true : v.estado === filtroEstado)
.sort((a, b) => {
const ad = a.fecha.split("/").reverse().join("");
const bd = b.fecha.split("/").reverse().join("");
return bd.localeCompare(ad);
});
// KPIs
const isMayo = (f) => f.endsWith("/05/2026");
const viajesMes = VIAJES.filter(v => isMayo(v.fecha));
const rollosEntregados = viajesMes.filter(v => v.tipo === "entrega" && v.estado === "completado").reduce((s, v) => s + v.rollos, 0);
const rollosRetirados = viajesMes.filter(v => v.tipo === "retiro" && v.estado === "completado").reduce((s, v) => s + v.rollos, 0);
const programados = VIAJES.filter(v => v.estado === "programado" || v.estado === "en curso").length;
return (
Logística
Viajes para entregar y retirar rollos · sin movimientos de dinero
window.toast("3 viajes planificados para los próximos 7 días", "info")}> Planificación
setShowNew(true)}>
Nuevo viaje
{filtrados.length} viajes
Tocá una fila para ver el detalle
Tipo
{[["todos","Todos"],["entrega","Entregas"],["retiro","Retiros"]].map(([k,l]) => (
setFiltroTipo(k)}>{l}
))}
Estado
{[["todos","Todos"],["programado","Programados"],["en curso","En curso"],["completado","Completados"]].map(([k,l]) => (
setFiltroEstado(k)}>{l}
))}
{filtrados.map(v => (
setDetail(v)} />
))}
{filtrados.length === 0 && (
No hay viajes con esos filtros.
)}
{detail &&
setDetail(null)} />}
{showNew && setShowNew(false)} onSuccess={recargar} />}
);
}
/* Una fila/tarjeta de viaje */
function ViajeRow({ viaje, onClick }) {
const D = window.DATA;
const isEntrega = viaje.tipo === "entrega";
const estadoCls = viaje.estado === "completado" ? "b-ok"
: viaje.estado === "en curso" ? "b-info"
: "b-warn";
return (
e.currentTarget.style.borderColor = "var(--green-400)"}
onMouseLeave={e => e.currentTarget.style.borderColor = "var(--admin-line)"}
>
{/* Fecha */}
{viaje.fecha}
#{viaje.id.toUpperCase()}
{/* Icono de tipo */}
{/* Origen → destino */}
{isEntrega ? "Entrega" : "Retiro"}
{viaje.estado}
{viaje.chofer}
{viaje.origen}
{viaje.destino}
{/* Rollos */}
{D.fmtNum(viaje.rollos)}
{viaje.tipoRollo}
);
}
/* Modal de detalle */
function ViajeDetail({ viaje, onClose }) {
const D = window.DATA;
const isEntrega = viaje.tipo === "entrega";
return (
e.stopPropagation()} style={{ maxWidth: 560 }}>
Viaje #{viaje.id.toUpperCase()}
{isEntrega ? "Entrega de rollos" : "Retiro de rollos"}
{/* Ruta */}
{/* Cantidad de rollos */}
Cantidad de rollos
{D.fmtNum(viaje.rollos)}
{viaje.tipoRollo}
{/* Datos extra */}
{viaje.estado}} />
{viaje.obs && viaje.obs !== "—" && }
Cerrar
{viaje.estado === "programado" && (
{ window.toast(`Viaje #${viaje.id.toUpperCase()} marcado en curso`); onClose(); }}> Marcar en curso
)}
{viaje.estado === "en curso" && (
{ window.toast(`Viaje #${viaje.id.toUpperCase()} completado`); onClose(); }}> Marcar completado
)}
);
}
/* Modal: nuevo viaje */
function ViajeForm({ onClose, onSuccess }) {
const [tipo, setTipo] = useState("entrega");
const [fecha, setFecha] = useState(new Date().toISOString().slice(0,10));
const [origen, setOrigen] = useState("");
const [destino, setDestino] = useState("");
const [rollos, setRollos] = useState("");
const [tipoRollo, setTipoRollo] = useState("");
const [camion, setCamion] = useState("");
const [estado, setEstado] = useState("programado");
const [obs, setObs] = useState("");
const [empleadoId,setEmpleadoId]= useState("");
const [empleados, setEmpleados] = useState([]);
const [fardos, setFardos] = useState([]);
const [err, setErr] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
window.API.get("/empleados").then(r => { if (r.data) setEmpleados(r.data); });
window.API.get("/fardos").then(r => { if (r.data) setFardos(r.data); });
}, []);
const submit = async (e) => {
e.preventDefault();
setErr(""); setLoading(true);
const res = await window.API.post("/viajes", {
empleado_id: empleadoId || null,
fecha,
tipo,
camion: camion || null,
origen,
destino,
cantidad_rollos:rollos ? parseInt(rollos) : null,
tipo_rollo: tipoRollo || null,
estado,
observaciones: obs || null,
});
setLoading(false);
if (res.data?.id || res.id) {
window.toast("Viaje guardado correctamente");
onSuccess && onSuccess();
onClose();
} else {
const msgs = res.errors ? Object.values(res.errors).flat().join(" · ") : (res.message || "Error al guardar.");
setErr(msgs);
}
};
return (
);
}
Object.assign(window, { Logistica });