🌱
ZLYX
Sistema de Gestión Agrícola
🔐
Nueva Contraseña
Crea una contraseña segura para tu cuenta
☰
📊 Panel Global - Todos los Cultivos
--
Salud Financiera Prom.
La IA analiza todos tus cultivos simultáneamente y prioriza dónde poner atención esta semana.
Cultivo Área Semana Inversión Ventas Utilidad ROI S. Financiera
No hay cultivos
🥧 Distribución de Inversión
💰 Resumen de Ventas
⚠️
$0
Descuentos
Sin descuentos
✅
$0
Venta Neta Confirmada
⏳
0
Pendientes de Liquidar
ℹ️ Sin datos Selecciona un cultivo para ver alertas.
✨
✨ Análisis IA
⏳ Analizando...
✨ Ver análisis completo
📊 Ventas por Presentación
📅 Cierre Semanal
💰 Ventas
📋 Historial
📅 Cierre Semanal Unificado
Categoría Monto Moneda
Sin gastos agregados
Siguiente: Producción →
📊
Merma Calculada
0.00 kg (0.00 %)
← Anterior
Siguiente: Consumo →
Registra el consumo de insumos del almacén esta semana:
No hay insumos en el almacén
💰
Valor Total Consumido
$0.00
← Anterior
✅ Guardar Cierre Completo
Fecha Cliente Presentación Cantidad Bruta Descuentos Neta Estado
Sin ventas registradas
Sem Fecha Gastos Extra Consumo Cosecha 1ra Merma
Sin cierres registrados
📦 Ver almacén de:
🌍 Todos los cultivos (Global)
💡
¿Cómo funciona el inventario?
Puedes agregar insumos de 2 formas :
① Desde aquí, registrando directamente (ideal para insumos que ya tenías en bodega antes de iniciar el ciclo).
② Desde Configuración → Presupuesto , al crear una categoría con la casilla "Agregar a almacén" activada.
Cuando hagas un cierre semanal, los consumos se descontarán automáticamente de aquí y se reflejarán en el presupuesto.
Insumo Categoría Cultivo Cantidad Unidad P. Unit. Valor Estado
Sin insumos registrados
Insumo Categoría Cantidad Unidad P. Unit. Valor Estado
Sin insumos registrados
Define los parámetros generales del ciclo para proyectar producción, gastos e ingresos.
💰 Distribución de Presupuesto por Fase
⚠️ Los porcentajes deben sumar 100%
Ajusta la producción esperada y el precio por semana usando los sliders. Los gastos se distribuyen automáticamente según las fases configuradas.
Semana
Fase
Producción (kg)
Precio $/kg
Ingreso Est.
Gasto Est.
Configura el ciclo primero
Compara el avance real del ciclo contra lo que proyectaste. Los datos reales se toman de los cierres semanales registrados.
📅
0/0
Semanas Transcurridas
🌾 Producción: Proyectada vs Real
💸 Gastos: Proyectado vs Real
💰 Ingresos: Proyectado vs Real
Sem.
Prod. Proy.
Prod. Real
Desv.
Gasto Proy.
Gasto Real
Desv.
Sin datos de comparación
🧮 Calcular
🔗 Vincular a Cultivo
📄 Descargar PDF
Mes Fecha Cuota Capital Interés Saldo
Ingresa datos para calcular
Fuente Tipo Saldo Pago Mensual Tasa Vence Cultivo
Sin deudas registradas
Análisis profundo de tu cultivo con inteligencia artificial. Identifica riesgos, oportunidades y acciones concretas basadas en tus datos reales.
✨ Generar Análisis IA
✨
Análisis IA disponible en Plan Empresarial
Obtén análisis profundos generados por IA con identificación de riesgos, oportunidades y acciones concretas para tu cultivo.
Solicitar upgrade →
📈 Planeación del Ciclo
Configura proyecciones y compara con datos reales semana a semana.
Ir a Planeación →
🌱 Cultivos
🏷️ Conceptos
💵 Presupuesto
📦 Presentaciones de ventas
📜 Histórico
💾 Respaldo
Nombre Tipo Área Inicio Moneda Estado
Sin cultivos registrados
Concepto Subcategoría Presupuesto G. Inicial Gastado Disponible
Sin categorías
Concepto Color
Sin conceptos
Nombre Peso Precio Moneda Descripción Acciones
Sin presentaciones registradas
Si inicias el sistema a mitad de un ciclo, ingresa los totales anteriores:
💾 Guardar Apertura
🗑️ Eliminar
📤
Exportar Respaldo Local
Descarga un archivo JSON con todos tus cultivos, cierres, ventas, almacén y deudas.
📤 Descargar JSON
📥
Importar Datos
¿Tienes datos de una versión anterior de ZLYX? Contáctanos para migrarlos.
📧 Solicitar migración
Elimina permanentemente todos los datos locales. Tus datos en la nube no se ven afectados.
🗑️ Limpiar datos locales
📦
Venta: -
Venta Bruta: $0.00
Descuentos aplicados
+ Agregar descuento
Sin descuentos. Haz clic en "Agregar descuento" si aplica alguno.
🚨
Riesgo de Pérdida Detectado
Los gastos de esta semana superan el ingreso proyectado.
Costo Marginal: $0.00
Ingreso Marginal Est.: $0.00
Diferencia: $0.00
Se recomienda evaluar el cierre del ciclo para evitar pérdidas adicionales.
// ========================================
// ESTADO GLOBAL
// ========================================
let cultivos = [];
let conceptos = [];
let almacen = [];
let presentaciones = [];
let proyecciones = [];
let cultivoActual = null;
let gastosSemanaTemp = [];
let charts = {};
// ========================================
// UTILIDADES DE FORMATO
// ========================================
function uid(prefijo) {
return prefijo + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
}
function esc(str) {
if (str === null || str === undefined) return '';
return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');
}
function toFixed2(valor) {
return parseFloat((parseFloat(valor) || 0).toFixed(2));
}
function formatMoney(valor) {
return '$' + toFixed2(valor).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatNumber(valor) {
return toFixed2(valor).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatPercent(valor) {
return toFixed2(valor).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + '%';
}
function convertirAMonedaBase(monto, monedaOrigen, tipoCambio, monedaBase) {
if (monedaOrigen === monedaBase) return toFixed2(monto);
if (monedaOrigen === 'USD' && monedaBase === 'MXN') return toFixed2(monto * tipoCambio);
if (monedaOrigen === 'MXN' && monedaBase === 'USD') return toFixed2(monto / tipoCambio);
return toFixed2(monto);
}
// ========================================
// NAVEGACIÓN
// ========================================
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.querySelector('.sidebar-overlay');
sidebar.classList.toggle('open');
overlay.classList.toggle('active');
}
function cambiarTab(tabId) {
// Cerrar sidebar en mobile
if (window.innerWidth <= 900) toggleSidebar();
document.querySelectorAll('.tabs-container').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const tab = document.getElementById('tab-' + tabId);
if (tab) tab.classList.add('active');
// Buscar el nav-item correspondiente (compatible con todos los browsers)
const navItem = document.querySelector(`.nav-item[onclick*="'${tabId}'"]`);
if (navItem) navItem.classList.add('active');
if (tabId === 'dashboard') actualizarDashboard();
if (tabId === 'inteligencia') {
actualizarTablaProyectado();
actualizarInteligencia();
if (cultivoActual) {
const t = calcularTotales();
actualizarAnalisisDetallado(t);
}
}
if (tabId === 'planeacion') {
cargarConfigPlaneacion();
actualizarSlidersSemanas();
setTimeout(() => {
actualizarGraficaPlaneacion();
actualizarComparativa();
}, 100);
}
if (tabId === 'config') actualizarTablaCultivos();
}
function cambiarSubTab(section, subTab) {
const container = document.getElementById('tab-' + section);
container.querySelectorAll('.sub-tab-content').forEach(s => s.classList.remove('active'));
container.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.getElementById(section + '-' + subTab).classList.add('active');
// Buscar el botón correspondiente (compatible con todos los browsers)
const btn = container.querySelector(`.tab-btn[onclick*="'${subTab}'"]`);
if (btn) btn.classList.add('active');
// Acciones específicas por sub-tab
if (section === 'planeacion') {
if (subTab === 'distribucion') {
setTimeout(() => {
actualizarSlidersSemanas();
actualizarGraficaPlaneacion();
}, 100);
}
if (subTab === 'comparativa') {
setTimeout(() => actualizarComparativa(), 100);
}
}
}
// ========================================
// MODALES
// ========================================
function mostrarModal(id) {
document.getElementById(id).classList.add('active');
cargarSelectsConceptos();
}
function cerrarModal(id) {
document.getElementById(id).classList.remove('active');
}
function cargarSelectsConceptos() {
const selects = ['categoriaConcepto', 'catRapidaConcepto'];
selects.forEach(selId => {
const sel = document.getElementById(selId);
if (sel) {
sel.innerHTML = 'Selecciona concepto... ' +
conceptos.map(c => `${esc(c.nombre)} `).join('');
}
});
}
// ========================================
// CULTIVOS
// ========================================
function actualizarSelectorCultivos() {
const sel = document.getElementById('selectorCultivo');
sel.innerHTML = 'Selecciona cultivo... ' +
'📊 Panel Global ' +
cultivos.map(c => `${esc(c.nombre)} `).join('');
}
function seleccionarCultivo(id) {
if (id === '__GLOBAL__' || id === '' || !id) {
cultivoActual = null;
document.getElementById('dashboardGlobal').style.display = 'block';
document.getElementById('dashboardCultivo').style.display = 'none';
actualizarBotonesGroq();
actualizarDashboardGlobal();
return;
}
cultivoActual = cultivos.find(c => c.id === id) || null;
// Si el cultivo no existe (fue eliminado), mostrar panel global
if (!cultivoActual) {
document.getElementById('selectorCultivo').value = '__GLOBAL__';
document.getElementById('dashboardGlobal').style.display = 'block';
document.getElementById('dashboardCultivo').style.display = 'none';
actualizarDashboardGlobal();
return;
}
document.getElementById('dashboardGlobal').style.display = 'none';
document.getElementById('dashboardCultivo').style.display = 'block';
// Mostrar/ocultar botón IA según plan
actualizarBotonesGroq();
// Asegurar que tenga almacén propio
if (!cultivoActual.almacen) cultivoActual.almacen = [];
// Limpiar respuesta IA anterior al cambiar cultivo
const respDiv = document.getElementById('groqRespuestaCultivo');
if (respDiv) respDiv.style.display = 'none';
const textoDiv = document.getElementById('groqCultivoTexto');
if (textoDiv) textoDiv.textContent = '';
actualizarSelectCategorias();
actualizarUIHistorico();
actualizarDashboard();
actualizarTablaCierres();
actualizarTablaVentas();
actualizarTablaPresupuesto();
generarListaConsumoInventario();
cargarSelectorPresentaciones();
cargarConfigPlaneacion();
document.getElementById('presupuestoTotal').value = cultivoActual.presupuestoTotal || '';
}
function guardarCultivo() {
const editId = document.getElementById('cultivoNombre').getAttribute('data-edit-id');
// Solo validar límite en creación nueva, no en edición
if (!editId && !validarLimiteCultivos()) return;
const nombre = document.getElementById('cultivoNombre').value.trim();
const tipo = document.getElementById('cultivoTipo').value.trim();
const area = parseFloat(document.getElementById('cultivoArea').value) || 0;
const fecha = document.getElementById('cultivoFecha').value;
const moneda = document.getElementById('cultivoMoneda').value;
const duracion = parseInt(document.getElementById('cultivoDuracion').value) || 30;
if (!nombre) { alert('Ingresa el nombre del cultivo'); return; }
if (editId) {
// Modo edición
const cultivo = cultivos.find(c => c.id === editId);
if (cultivo) {
cultivo.nombre = nombre;
cultivo.tipo = tipo;
cultivo.area = toFixed2(area);
cultivo.fechaInicio = fecha;
cultivo.moneda = moneda;
cultivo.duracionSemanas = duracion;
}
document.getElementById('cultivoNombre').removeAttribute('data-edit-id');
} else {
// Modo nuevo
const cultivo = {
id: uid('cult_'),
nombre,
tipo,
area: toFixed2(area),
fechaInicio: fecha,
moneda,
duracionSemanas: duracion,
estado: 'Activo',
presupuestoTotal: 0,
categorias: [],
cierres: [],
ventas: [],
gastosExtra: [],
apertura: null,
planificacion: [],
credito: null,
almacen: [],
planeacion: null
};
cultivos.push(cultivo);
}
guardarDatos();
// Supabase
const cultivoAGuardar = editId ? cultivos.find(c => c.id === editId) || cultivos[cultivos.length-1] : cultivos[cultivos.length-1];
guardarCultivoSupabase(cultivoAGuardar);
actualizarSelectorCultivos();
actualizarTablaCultivos();
// Limpiar form
document.getElementById('cultivoNombre').value = '';
document.getElementById('cultivoTipo').value = '';
document.getElementById('cultivoArea').value = '';
document.getElementById('cultivoFecha').value = '';
document.getElementById('cultivoDuracion').value = '';
alert(editId ? 'Cultivo actualizado' : 'Cultivo guardado correctamente');
}
function eliminarCultivo(id) {
if (!confirm('¿Eliminar este cultivo y todos sus datos (cierres, ventas, presupuesto, almacén)?')) return;
// Si es el cultivo actual, limpiar UI primero
const esCultivoActual = cultivoActual && cultivoActual.id === id;
// Eliminar del array
cultivos = cultivos.filter(c => c.id !== id);
// Limpiar deudas vinculadas a este cultivo (evitar huérfanas)
const deudasHuerfanas = deudas.filter(d => d.cultivoId === id);
if (deudasHuerfanas.length > 0) {
const accion = confirm(`Este cultivo tiene ${deudasHuerfanas.length} deuda(s) vinculada(s).\n\n¿Deseas eliminar también las deudas?\n(Aceptar = Eliminar deudas | Cancelar = Desvincular pero conservar)`);
if (accion) {
deudas = deudas.filter(d => d.cultivoId !== id);
} else {
deudas.forEach(d => { if (d.cultivoId === id) d.cultivoId = '__NINGUNO__'; });
}
}
// Guardar inmediatamente
guardarDatos();
eliminarCultivoSupabase(id);
// Si era el cultivo actual, resetear todo
if (esCultivoActual) {
cultivoActual = null;
// Limpiar selector
document.getElementById('selectorCultivo').value = '';
// Destruir todos los gráficos del cultivo
destruirGraficosCultivo();
// Mostrar panel global o limpiar dashboard
if (cultivos.length > 0) {
document.getElementById('dashboardGlobal').style.display = 'block';
document.getElementById('dashboardCultivo').style.display = 'none';
actualizarDashboardGlobal();
} else {
document.getElementById('dashboardGlobal').style.display = 'block';
document.getElementById('dashboardCultivo').style.display = 'none';
limpiarDashboardVacio();
}
// Limpiar todas las tablas y elementos relacionados
limpiarTablasDelCultivo();
}
// Actualizar selectores y tablas globales
actualizarSelectorCultivos();
actualizarSelectorAlmacen();
actualizarSelectorDeudaCultivo();
actualizarTablaCultivos();
if (cultivos.length > 0) {
actualizarDashboardGlobal();
}
alert('Cultivo eliminado correctamente');
}
function destruirGraficosCultivo() {
// Destruir todos los gráficos relacionados con el cultivo
const chartKeys = ['tendencia', 'conceptos', 'produccion', 'stockCritico',
'gastosProyReal', 'prodProyReal', 'planeacion',
'compProd', 'compGasto', 'compIngreso'];
chartKeys.forEach(key => {
if (charts[key]) {
charts[key].destroy();
charts[key] = null;
}
});
}
function limpiarDashboardVacio() {
// Limpiar KPIs
const kpis = {
'valROI': '0.00%',
'valUtilidad': '$0.00',
'valInversion': '$0.00',
'valVentas': '$0.00',
'valMerma': '0.00%',
'valDSCR': 'N/A'
};
Object.entries(kpis).forEach(([id, val]) => {
const el = document.getElementById(id);
if (el) el.textContent = val;
});
// Limpiar info del cultivo
const info = {
'dashNombreCultivo': 'Selecciona un cultivo',
'dashArea': '0',
'dashSemana': '0',
'dashMoneda': '-',
'dashEstado': '-'
};
Object.entries(info).forEach(([id, val]) => {
const el = document.getElementById(id);
if (el) el.textContent = val;
});
// Resetear clases de KPIs
['kpiROI', 'kpiUtilidad', 'kpiMerma'].forEach(id => {
const el = document.getElementById(id);
if (el) el.className = 'kpi-card';
});
// Limpiar alertas IA
const alertasIA = document.getElementById('alertasIA');
if (alertasIA) alertasIA.innerHTML = 'Selecciona un cultivo para ver alertas
';
}
function limpiarTablasDelCultivo() {
// Limpiar tabla de cierres
const tablaCierres = document.getElementById('tablaCierres');
if (tablaCierres) tablaCierres.innerHTML = 'Selecciona un cultivo ';
// Limpiar tabla de ventas
const tablaVentas = document.getElementById('tablaVentas');
if (tablaVentas) tablaVentas.innerHTML = 'Selecciona un cultivo ';
// Limpiar tabla de presupuesto
const tablaPresupuesto = document.getElementById('tablaPresupuesto');
if (tablaPresupuesto) tablaPresupuesto.innerHTML = 'Selecciona un cultivo ';
// Limpiar tabla de almacén
const tablaAlmacen = document.getElementById('tablaAlmacen');
if (tablaAlmacen) tablaAlmacen.innerHTML = 'Selecciona un cultivo ';
// Limpiar selector de categorías en cierre
const selectCategoria = document.getElementById('cierreCategoria');
if (selectCategoria) selectCategoria.innerHTML = 'Selecciona categoría... ';
// Limpiar input de presupuesto total
const presupuestoTotal = document.getElementById('presupuestoTotal');
if (presupuestoTotal) presupuestoTotal.value = '';
// Limpiar stats de ventas
const statsConfig = {
'statCantidadVendida': '0 kg',
'statIngresoBruto': '$0.00',
'statDescuentos': '$0.00',
'statIngresoNeto': '$0.00',
'statPrecioPromedio': '$0.00/kg',
'statVentasLiquidadas': '$0.00',
'statVentasPendientes': '$0.00'
};
Object.entries(statsConfig).forEach(([id, val]) => {
const el = document.getElementById(id);
if (el) el.textContent = val;
});
// Limpiar lista de gastos temporales en cierre semanal
gastosTemp = [];
const listaGastos = document.getElementById('listaGastosTemp');
if (listaGastos) listaGastos.innerHTML = 'Sin gastos agregados
';
// Limpiar totales del cierre
const totalGastosEl = document.getElementById('totalGastosTemp');
if (totalGastosEl) totalGastosEl.textContent = '$0.00';
// Limpiar campos del formulario de cierre
const camposCierre = ['cierreSemana', 'cierreCategoria', 'cierreMonto', 'cierreCosecha',
'cierrePrimera', 'cierreSegunda', 'cierreNotas', 'cierreGastoExtra'];
camposCierre.forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
resetCheckboxesCierre();
// Limpiar datos de planeación
distribucionSemanal = [];
const tablaDistribucion = document.getElementById('tablaDistribucionSemanal');
if (tablaDistribucion) tablaDistribucion.innerHTML = 'Selecciona un cultivo ';
// Limpiar tabla comparativa
const tablaComparativa = document.getElementById('tablaComparativa');
if (tablaComparativa) tablaComparativa.innerHTML = 'Sin datos ';
// Limpiar KPIs de planeación
const planKpis = {
'planInversionEst': '$0',
'planProduccionEst': '0 ton',
'planIngresoEst': '$0',
'planUtilidadEst': '$0',
'planROIEst': '0%',
'planTotalProduccion': '0 kg',
'planPrecioPromedio': '$0/kg',
'planTotalIngresos': '$0',
'planTotalGastos': '$0',
'compAvanceSemanas': '0/0',
'compAvanceProduccion': '0%',
'compAvanceGastos': '0%',
'compAvanceIngresos': '0%'
};
Object.entries(planKpis).forEach(([id, val]) => {
const el = document.getElementById(id);
if (el) el.textContent = val;
});
}
function editarCultivo(id) {
const cultivo = cultivos.find(c => c.id === id);
if (!cultivo) return;
document.getElementById('cultivoNombre').value = cultivo.nombre;
document.getElementById('cultivoTipo').value = cultivo.tipo;
document.getElementById('cultivoArea').value = cultivo.area;
document.getElementById('cultivoFecha').value = cultivo.fechaInicio || '';
document.getElementById('cultivoMoneda').value = cultivo.moneda;
document.getElementById('cultivoDuracion').value = cultivo.duracionSemanas || 30;
// Guardar ID para edición
document.getElementById('cultivoNombre').setAttribute('data-edit-id', id);
}
function actualizarTablaCultivos() {
const tbody = document.getElementById('tablaCultivos');
if (cultivos.length === 0) {
tbody.innerHTML = 'Sin cultivos registrados ';
return;
}
tbody.innerHTML = cultivos.map(c => `
${esc(c.nombre)}
${c.tipo}
${formatNumber(c.area)} ha
${c.fechaInicio || '-'}
${c.moneda}
${c.estado}
✏️
🗑️
`).join('');
}
// ========================================
// CONCEPTOS
// ========================================
function guardarConcepto() {
const nombre = document.getElementById('conceptoNombre').value.trim();
const color = document.getElementById('conceptoColor').value;
const btnGuardar = document.querySelector('#config-conceptos .btn[onclick*="guardarConcepto"]');
const editId = btnGuardar?.getAttribute('data-edit-id');
if (!nombre) { alert('Ingresa el nombre del concepto'); return; }
if (editId) {
// Modo edición
const concepto = conceptos.find(c => c.id === editId);
if (concepto) {
concepto.nombre = nombre;
concepto.color = color;
}
btnGuardar.removeAttribute('data-edit-id');
btnGuardar.innerHTML = '➕ Agregar';
} else {
// Modo nuevo
conceptos.push({
id: uid('conc_'),
nombre,
color
});
}
guardarDatos();
const concAGuardar = editId ? conceptos.find(c => c.id === editId) : conceptos[conceptos.length-1];
if (concAGuardar) guardarConceptoSupabase(concAGuardar);
actualizarTablaConceptos();
document.getElementById('conceptoNombre').value = '';
alert(editId ? 'Concepto actualizado' : 'Concepto guardado');
}
function eliminarConcepto(id) {
if (!confirm('¿Eliminar este concepto?')) return;
conceptos = conceptos.filter(c => c.id !== id);
guardarDatos();
eliminarConceptoSupabase(id);
actualizarTablaConceptos();
}
function actualizarTablaConceptos() {
const tbody = document.getElementById('tablaConceptos');
if (conceptos.length === 0) {
tbody.innerHTML = 'Sin conceptos. Agrega uno nuevo. ';
return;
}
tbody.innerHTML = conceptos.map(c => `
${c.nombre}
${c.color}
✏️
🗑️
`).join('');
cargarSelectsConceptos();
}
function editarConcepto(id) {
const concepto = conceptos.find(c => c.id === id);
if (!concepto) return;
document.getElementById('conceptoNombre').value = concepto.nombre;
document.getElementById('conceptoColor').value = concepto.color;
// Cambiar botón a modo editar
const btnGuardar = document.querySelector('#config-conceptos .btn[onclick*="guardarConcepto"]');
if (btnGuardar) {
btnGuardar.setAttribute('data-edit-id', id);
btnGuardar.innerHTML = '💾 Actualizar';
}
}
// ========================================
// CATEGORÍAS / PRESUPUESTO
// ========================================
function guardarPresupuestoTotal() {
if (!cultivoActual) { alert('Selecciona un cultivo'); return; }
cultivoActual.presupuestoTotal = toFixed2(parseFloat(document.getElementById('presupuestoTotal').value) || 0);
guardarDatos();
actualizarPresupuestoTotalSupabase(cultivoActual.id, cultivoActual.presupuestoTotal);
alert('Presupuesto total guardado');
}
function toggleCamposAlmacenCategoria() {
const checked = document.getElementById('categoriaAgregarAlmacen').checked;
document.getElementById('camposAlmacenCategoria').style.display = checked ? 'block' : 'none';
}
function guardarCategoria() {
if (!cultivoActual) { alert('Selecciona un cultivo'); return; }
const conceptoId = document.getElementById('categoriaConcepto').value;
const nombre = document.getElementById('categoriaNombre').value.trim();
const presupuesto = parseFloat(document.getElementById('categoriaPresupuesto').value) || 0;
const gastadoInicial = parseFloat(document.getElementById('categoriaGastadoInicial').value) || 0;
const editId = document.getElementById('categoriaNombre').getAttribute('data-edit-id');
if (!conceptoId || !nombre) { alert('Completa todos los campos'); return; }
if (editId) {
// Modo edición
const cat = cultivoActual.categorias.find(c => c.id === editId);
if (cat) {
cat.conceptoId = conceptoId;
cat.nombre = nombre;
cat.presupuesto = toFixed2(presupuesto);
cat.gastadoInicial = toFixed2(gastadoInicial);
}
document.getElementById('categoriaNombre').removeAttribute('data-edit-id');
} else {
// Modo nuevo
const catId = uid('cat_');
cultivoActual.categorias.push({
id: catId,
conceptoId,
nombre,
presupuesto: toFixed2(presupuesto),
gastadoInicial: toFixed2(gastadoInicial),
gastado: toFixed2(gastadoInicial)
});
// Si checkbox de almacén está activo, crear insumo en almacén del cultivo
const agregarAlm = document.getElementById('categoriaAgregarAlmacen').checked;
if (agregarAlm) {
const almNombre = document.getElementById('categoriaAlmNombre').value.trim() || nombre;
const almCantidad = parseFloat(document.getElementById('categoriaAlmCantidad').value) || 0;
const almUnidad = document.getElementById('categoriaAlmUnidad').value;
const almStockMin = parseFloat(document.getElementById('categoriaAlmStockMin').value) || 0;
const precioUnit = almCantidad > 0 ? toFixed2(presupuesto / almCantidad) : 0;
if (!cultivoActual.almacen) cultivoActual.almacen = [];
cultivoActual.almacen.push({
id: uid('ins_'),
nombre: almNombre,
categoria: nombre,
cantidad: toFixed2(almCantidad),
unidad: almUnidad,
precio: precioUnit,
stockMin: toFixed2(almStockMin),
origenPresupuesto: catId
});
}
}
guardarDatos();
// Supabase: guardar categoría
const catGuardada = editId
? cultivoActual.categorias.find(c => c.id === editId)
: cultivoActual.categorias[cultivoActual.categorias.length - 1];
if (catGuardada) guardarCategoriaSupabase(catGuardada, cultivoActual.id);
// Si se agregó insumo al almacén, guardarlo también
if (!editId && document.getElementById('categoriaAgregarAlmacen').checked) {
const insCat = cultivoActual.almacen[cultivoActual.almacen.length - 1];
if (insCat) guardarInsumoSupabase(insCat, cultivoActual.id);
}
actualizarTablaPresupuesto();
actualizarSelectCategorias();
actualizarTablaAlmacen();
// Limpiar formulario completo
document.getElementById('categoriaNombre').value = '';
document.getElementById('categoriaPresupuesto').value = '';
document.getElementById('categoriaGastadoInicial').value = '';
document.getElementById('categoriaAgregarAlmacen').checked = false;
document.getElementById('camposAlmacenCategoria').style.display = 'none';
document.getElementById('categoriaAlmNombre').value = '';
document.getElementById('categoriaAlmCantidad').value = '';
document.getElementById('categoriaAlmStockMin').value = '';
document.getElementById('categoriaAlmUnidad').value = 'kg';
alert(editId ? 'Categoría actualizada' : (document.getElementById('categoriaAgregarAlmacen')?.checked ? 'Categoría guardada y agregada al almacén' : 'Categoría guardada'));
}
function guardarCategoriaRapida() {
if (!cultivoActual) { alert('Selecciona un cultivo'); return; }
const conceptoId = document.getElementById('catRapidaConcepto').value;
const nombre = document.getElementById('catRapidaNombre').value.trim();
const presupuesto = parseFloat(document.getElementById('catRapidaPresupuesto').value) || 0;
if (!conceptoId || !nombre) { alert('Completa todos los campos'); return; }
cultivoActual.categorias.push({
id: uid('cat_'),
conceptoId,
nombre,
presupuesto: toFixed2(presupuesto),
gastado: 0
});
guardarDatos();
const catRapida = cultivoActual.categorias[cultivoActual.categorias.length - 1];
if (catRapida) guardarCategoriaSupabase(catRapida, cultivoActual.id);
actualizarSelectCategorias();
cerrarModal('modalCategoriaRapida');
document.getElementById('catRapidaNombre').value = '';
document.getElementById('catRapidaPresupuesto').value = '';
}
function eliminarCategoria(id) {
if (!cultivoActual) return;
if (!confirm('¿Eliminar esta categoría del presupuesto?')) return;
cultivoActual.categorias = cultivoActual.categorias.filter(c => c.id !== id);
guardarDatos();
eliminarCategoriaSupabase(id);
actualizarTablaPresupuesto();
actualizarSelectCategorias();
}
function editarCategoria(id) {
if (!cultivoActual) return;
const cat = cultivoActual.categorias.find(c => c.id === id);
if (!cat) return;
document.getElementById('categoriaConcepto').value = cat.conceptoId;
document.getElementById('categoriaNombre').value = cat.nombre;
document.getElementById('categoriaPresupuesto').value = cat.presupuesto;
document.getElementById('categoriaGastadoInicial').value = cat.gastadoInicial || 0;
// Guardar ID para edición
document.getElementById('categoriaNombre').setAttribute('data-edit-id', id);
}
function actualizarTablaPresupuesto() {
if (!cultivoActual) return;
const tbody = document.getElementById('tablaPresupuesto');
if (cultivoActual.categorias.length === 0) {
tbody.innerHTML = 'Sin categorías ';
return;
}
tbody.innerHTML = cultivoActual.categorias.map(cat => {
const concepto = conceptos.find(c => c.id === cat.conceptoId);
const gastadoInicial = cat.gastadoInicial || 0;
const disponible = toFixed2(cat.presupuesto - cat.gastado);
return `
${concepto?.nombre || 'Sin Concepto'}
${cat.nombre}
${formatMoney(cat.presupuesto)}
${formatMoney(gastadoInicial)}
${formatMoney(cat.gastado)}
${formatMoney(disponible)}
✏️
🗑️
`;
}).join('');
}
function actualizarSelectCategorias() {
if (!cultivoActual) return;
const sel = document.getElementById('cierreCategoria');
sel.innerHTML = 'Selecciona categoría... ' +
cultivoActual.categorias.map(c => `${esc(c.nombre)} `).join('');
}
// ========================================
// CIERRE SEMANAL
// ========================================
function irPasoCierre(paso) {
document.querySelectorAll('.cierre-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.cierre-step').forEach(s => s.classList.remove('active'));
document.getElementById('cierreSeccion' + paso).classList.add('active');
document.getElementById('stepCierre' + paso).classList.add('active');
if (paso === 3) generarListaConsumoInventario();
}
function agregarGastoSemana() {
const catId = document.getElementById('cierreCategoria').value;
const monto = parseFloat(document.getElementById('cierreGastoMonto').value) || 0;
const moneda = document.getElementById('cierreGastoMoneda').value;
// TC: del input inline si es USD, o del campo global
const tcInline = parseFloat(document.getElementById('tcGastoInput')?.value) || 0;
const tcGlobal = parseFloat(document.getElementById('cierreTipoCambio')?.value) || 0;
const tcFinal = moneda === 'USD' ? (tcInline || tcGlobal) : 0;
// Congelar TC para gastos siguientes en la misma semana
if (moneda === 'USD' && tcFinal > 0 && !_tcCongelado) {
_tcCongelado = tcFinal;
const tcInput = document.getElementById('tcGastoInput');
if (tcInput) { tcInput.value = tcFinal; tcInput.readOnly = true; tcInput.style.background = 'var(--arena-oscuro)'; }
}
if (!catId || monto <= 0) { alert('Selecciona categoría y monto'); return; }
const cat = cultivoActual?.categorias.find(c => c.id === catId);
gastosSemanaTemp.push({
categoriaId: catId,
categoriaNombre: cat?.nombre || 'Sin nombre',
monto: toFixed2(monto),
moneda
});
actualizarTablaGastosSemana();
document.getElementById('cierreGastoMonto').value = '';
}
function eliminarGastoTemp(idx) {
gastosSemanaTemp.splice(idx, 1);
actualizarTablaGastosSemana();
}
function editarGastoTemp(idx) {
const gasto = gastosSemanaTemp[idx];
if (!gasto) return;
document.getElementById('cierreCategoria').value = gasto.categoriaId;
document.getElementById('cierreGastoMonto').value = gasto.monto;
document.getElementById('cierreGastoMoneda').value = gasto.moneda;
// Eliminar el gasto para que al agregar se reemplace
gastosSemanaTemp.splice(idx, 1);
actualizarTablaGastosSemana();
}
function actualizarTablaGastosSemana() {
const tbody = document.getElementById('tablaGastosSemana');
if (gastosSemanaTemp.length === 0) {
tbody.innerHTML = 'Sin gastos agregados ';
return;
}
tbody.innerHTML = gastosSemanaTemp.map((g, i) => `
${g.categoriaNombre}
${formatMoney(g.monto)}
${g.moneda}
✏️
🗑️
`).join('');
}
function calcularMermaAuto() {
const cosecha = parseFloat(document.getElementById('cierreCosecha').value) || 0;
const primera = parseFloat(document.getElementById('cierrePrimera').value) || 0;
const segunda = parseFloat(document.getElementById('cierreSegunda').value) || 0;
const merma = Math.max(0, cosecha - primera - segunda);
const pct = cosecha > 0 ? (merma / cosecha) * 100 : 0;
document.getElementById('cierreMermaCalc').textContent = formatNumber(merma);
document.getElementById('cierreMermaPct').textContent = formatNumber(pct);
}
function generarListaConsumoInventario() {
const container = document.getElementById('listaConsumoInventario');
// Usar almacén del cultivo actual, NO el global
const almacenCultivo = cultivoActual?.almacen || [];
if (almacenCultivo.length === 0) {
container.innerHTML = 'No hay insumos en el almacén de este cultivo
Puedes agregar insumos desde la pestaña Almacén o al crear categorías en Presupuesto con la casilla "Agregar a almacén". ';
return;
}
container.innerHTML = almacenCultivo.map(ins => {
const esBajo = ins.cantidad <= ins.stockMin && ins.stockMin > 0;
const borderColor = esBajo ? 'var(--peligro)' : 'var(--arena-oscuro)';
return `
${ins.nombre}
${esBajo ? '
⚠️ Stock bajo ' : ''}
Disponible: ${formatNumber(ins.cantidad)} ${ins.unidad} · ${formatMoney(ins.precio)}/${ins.unidad}
${ins.unidad}
`;
}).join('');
}
function calcularTotalConsumo() {
const inputs = document.querySelectorAll('.consumo-input');
let total = 0;
inputs.forEach(inp => {
const cantidad = parseFloat(inp.value) || 0;
const precio = parseFloat(inp.dataset.precio) || 0;
const disponible = parseFloat(inp.dataset.disponible) || 0;
// Validar que no exceda el stock disponible
if (cantidad > disponible) {
inp.style.borderColor = 'var(--peligro)';
inp.title = `Máximo disponible: ${disponible}`;
} else {
inp.style.borderColor = '';
inp.title = '';
}
total += cantidad * precio;
});
document.getElementById('valorConsumoTotal').textContent = formatMoney(total);
}
function guardarCierreSemanal() {
if (!cultivoActual) { alert('Selecciona un cultivo'); return; }
const semana = parseInt(document.getElementById('cierreSemana').value);
const fecha = document.getElementById('cierreFecha').value;
const tipoCambio = parseFloat(document.getElementById('cierreTipoCambio').value) || 0;
const gastoExtra = parseFloat(document.getElementById('cierreGastoExtra').value) || 0;
const notaExtra = document.getElementById('cierreNotaExtra')?.value?.trim() || '';
const cosechaTotal = parseFloat(document.getElementById('cierreCosecha').value) || 0;
const primeraCalidad = parseFloat(document.getElementById('cierrePrimera').value) || 0;
const segundaCalidad = parseFloat(document.getElementById('cierreSegunda').value) || 0;
const merma = Math.max(0, cosechaTotal - primeraCalidad - segundaCalidad);
if (!semana || !fecha) { alert('Ingresa semana y fecha'); return; }
// Verificar si hay gastos en USD y no hay TC
const hayUSD = gastosSemanaTemp.some(g => g.moneda === 'USD');
if (hayUSD && tipoCambio <= 0) {
alert('Ingresa el Tipo de Cambio (hay gastos en USD)');
irPasoCierre(1);
return;
}
// Convertir gastos a moneda base y actualizar categorías
let totalGastos = 0;
gastosSemanaTemp.forEach(g => {
const montoBase = convertirAMonedaBase(g.monto, g.moneda, tipoCambio, cultivoActual.moneda);
totalGastos += montoBase;
const cat = cultivoActual.categorias.find(c => c.id === g.categoriaId);
if (cat) cat.gastado = toFixed2(cat.gastado + montoBase);
});
// Procesar consumo de inventario
const consumos = [];
let valorConsumo = 0;
const alertasReabastecimiento = [];
document.querySelectorAll('.consumo-input').forEach(inp => {
const cantidad = parseFloat(inp.value) || 0;
if (cantidad > 0) {
const insumoId = inp.dataset.insumo;
const precio = parseFloat(inp.dataset.precio) || 0;
const valor = toFixed2(cantidad * precio);
valorConsumo += valor;
consumos.push({ insumoId, cantidad: toFixed2(cantidad), valor });
// Restar del almacén del cultivo (NO del global)
const ins = cultivoActual.almacen?.find(i => i.id === insumoId);
if (ins) {
ins.cantidad = toFixed2(Math.max(0, ins.cantidad - cantidad));
// Verificar si necesita reabastecimiento
if (ins.stockMin > 0 && ins.cantidad <= ins.stockMin) {
alertasReabastecimiento.push(`⚠️ "${ins.nombre}" quedó en ${formatNumber(ins.cantidad)} ${ins.unidad} (mínimo: ${formatNumber(ins.stockMin)} ${ins.unidad}). ¡Necesita reabastecerse!`);
}
}
}
});
// Verificar alerta de margen negativo (Semana 32)
const costoMarginal = toFixed2(totalGastos + gastoExtra + valorConsumo);
const ingresoMarginal = calcularIngresoMarginalEstimado(semana);
if (costoMarginal > ingresoMarginal && ingresoMarginal > 0) {
mostrarAlertaMargenNegativo(costoMarginal, ingresoMarginal);
}
// Crear cierre
const cierre = {
id: uid('cierre_'),
semana,
fecha,
tipoCambio,
gastos: [...gastosSemanaTemp],
totalGastos: toFixed2(totalGastos),
gastoExtra: toFixed2(gastoExtra),
notaExtra,
cosechaTotal: toFixed2(cosechaTotal),
primeraCalidad: toFixed2(primeraCalidad),
segundaCalidad: toFixed2(segundaCalidad),
merma: toFixed2(merma),
consumos,
valorConsumo: toFixed2(valorConsumo)
};
cultivoActual.cierres.push(cierre);
guardarDatos();
// Supabase: guardar cierre con gastos y consumos
guardarCierreSupabase(cierre, cultivoActual.id);
// Supabase: actualizar cantidades en almacén y gastado en categorías
cierre.gastos.forEach(g => {
const cat = cultivoActual.categorias.find(c => c.id === g.categoriaId);
if (cat) actualizarGastadoCategoria(cat.id, cat.gastado);
});
cierre.consumos.forEach(co => {
const ins = cultivoActual.almacen?.find(i => i.id === co.insumoId);
if (ins) actualizarCantidadInsumo(ins.id, ins.cantidad);
});
// Limpiar
gastosSemanaTemp = [];
document.getElementById('cierreSemana').value = '';
document.getElementById('cierreFecha').value = '';
document.getElementById('cierreTipoCambio').value = '';
document.getElementById('cierreGastoExtra').value = '';
document.getElementById('cierreNotaExtra').value = '';
document.getElementById('cierreCosecha').value = '';
document.getElementById('cierrePrimera').value = '';
document.getElementById('cierreSegunda').value = '';
document.getElementById('cierreMermaCalc').textContent = '0.00';
document.getElementById('cierreMermaPct').textContent = '0.00';
actualizarTablaGastosSemana();
irPasoCierre(1);
actualizarTablaCierres();
actualizarTablaPresupuesto();
actualizarTablaAlmacen();
actualizarDashboard();
let mensajeCierre = 'Cierre semanal guardado correctamente';
if (alertasReabastecimiento.length > 0) {
mensajeCierre += '\n\n🔔 ALERTAS DE INVENTARIO:\n' + alertasReabastecimiento.join('\n');
}
alert(mensajeCierre);
}
function eliminarCierre(id) {
if (!cultivoActual) return;
if (!confirm('¿Eliminar este cierre?')) return;
cultivoActual.cierres = cultivoActual.cierres.filter(c => c.id !== id);
guardarDatos();
eliminarCierreSupabase(id);
actualizarTablaCierres();
actualizarDashboard();
}
function actualizarTablaCierres() {
if (!cultivoActual) return;
const tbody = document.getElementById('tablaCierres');
if (cultivoActual.cierres.length === 0) {
tbody.innerHTML = 'Sin cierres registrados ';
return;
}
tbody.innerHTML = cultivoActual.cierres.map(c => `
S${c.semana}
${c.fecha}
${formatMoney(c.totalGastos)}
${formatMoney(c.gastoExtra)}
${formatMoney(c.valorConsumo)}
${formatNumber(c.cosechaTotal)} kg
${formatNumber(c.primeraCalidad)} kg
${formatNumber(c.merma)} kg
👁️
🗑️
`).join('');
}
function verDetalleCierre(id) {
const cierre = cultivoActual?.cierres.find(c => c.id === id);
if (!cierre) return;
let detalle = `CIERRE SEMANA ${cierre.semana}\n`;
detalle += `Fecha: ${cierre.fecha}\n\n`;
detalle += `GASTOS:\n`;
detalle += `- Total Gastos: ${formatMoney(cierre.totalGastos)}\n`;
detalle += `- Gasto Extra: ${formatMoney(cierre.gastoExtra)}\n`;
detalle += `- Valor Consumo: ${formatMoney(cierre.valorConsumo)}\n\n`;
detalle += `PRODUCCIÓN:\n`;
detalle += `- Cosecha Total: ${formatNumber(cierre.cosechaTotal)} kg\n`;
detalle += `- 1ra Calidad: ${formatNumber(cierre.primeraCalidad)} kg\n`;
detalle += `- 2da Calidad: ${formatNumber(cierre.segundaCalidad)} kg\n`;
detalle += `- Merma: ${formatNumber(cierre.merma)} kg\n`;
if (cierre.notaExtra) {
detalle += `\nNOTA: ${cierre.notaExtra}`;
}
alert(detalle);
}
// ========================================
// VENTAS
// ========================================
function cargarSelectorPresentaciones() {
const sel = document.getElementById('ventaPresentacion');
if (!sel) return;
sel.innerHTML = '-- Venta sin presentación -- ' +
presentaciones.map(p => `${p.nombre} (${formatNumber(p.peso)} kg - ${formatMoney(p.precio)}) `).join('');
}
function aplicarPresentacionVenta() {
const presId = document.getElementById('ventaPresentacion').value;
const unidadesInput = document.getElementById('ventaUnidades');
if (!presId) {
// Sin presentación, limpiar y habilitar campos manuales
unidadesInput.value = '';
document.getElementById('ventaCantidad').value = '';
document.getElementById('ventaBruta').value = '';
document.getElementById('ventaCantidad').removeAttribute('readonly');
document.getElementById('ventaBruta').removeAttribute('readonly');
return;
}
// Con presentación, calcular automáticamente
unidadesInput.value = 1;
calcularVentaPorPresentacion();
}
function calcularVentaPorPresentacion() {
const presId = document.getElementById('ventaPresentacion').value;
if (!presId) return;
const pres = presentaciones.find(p => p.id === presId);
if (!pres) return;
const unidades = parseInt(document.getElementById('ventaUnidades').value) || 0;
const cantidadKg = toFixed2(unidades * pres.peso);
const ventaBruta = toFixed2(unidades * pres.precio);
document.getElementById('ventaCantidad').value = cantidadKg;
document.getElementById('ventaBruta').value = ventaBruta;
}
function guardarVenta() {
if (!cultivoActual) { alert('Selecciona un cultivo'); return; }
const fecha = document.getElementById('ventaFecha').value;
const cliente = document.getElementById('ventaCliente').value.trim();
const cantidad = parseFloat(document.getElementById('ventaCantidad').value) || 0;
const bruta = parseFloat(document.getElementById('ventaBruta').value) || 0;
const editId = document.getElementById('ventaFecha').getAttribute('data-edit-id');
// Obtener presentación si se seleccionó
const presId = document.getElementById('ventaPresentacion').value;
const unidades = parseInt(document.getElementById('ventaUnidades').value) || 0;
const pres = presId ? presentaciones.find(p => p.id === presId) : null;
if (!fecha || !cliente || cantidad <= 0 || bruta <= 0) {
alert('Completa todos los campos');
return;
}
if (editId) {
// Modo edición
const venta = cultivoActual.ventas.find(v => v.id === editId);
if (venta) {
venta.fecha = fecha;
venta.cliente = cliente;
venta.cantidad = toFixed2(cantidad);
venta.bruta = toFixed2(bruta);
venta.neta = toFixed2(bruta - obtenerTotalDescuentos(venta));
venta.presentacionId = presId || null;
venta.presentacionNombre = pres?.nombre || null;
venta.unidades = unidades || null;
}
document.getElementById('ventaFecha').removeAttribute('data-edit-id');
} else {
// Modo nuevo
cultivoActual.ventas.push({
id: uid('venta_'),
fecha,
cliente,
cantidad: toFixed2(cantidad),
bruta: toFixed2(bruta),
descCalidad: 0,
descRetraso: 0,
neta: toFixed2(bruta),
presentacionId: presId || null,
presentacionNombre: pres?.nombre || null,
unidades: unidades || null
});
}
guardarDatos();
const ventaGuardada = editId
? cultivoActual.ventas.find(v => v.id === editId)
: cultivoActual.ventas[cultivoActual.ventas.length - 1];
if (ventaGuardada) guardarVentaSupabase(ventaGuardada, cultivoActual.id);
actualizarTablaVentas();
actualizarDashboard();
// Limpiar formulario
document.getElementById('ventaFecha').value = '';
document.getElementById('ventaCliente').value = '';
document.getElementById('ventaCantidad').value = '';
document.getElementById('ventaBruta').value = '';
document.getElementById('ventaPresentacion').value = '';
document.getElementById('ventaUnidades').value = '';
alert(editId ? 'Venta actualizada' : 'Venta registrada');
}
function eliminarVenta(id) {
if (!cultivoActual) return;
if (!confirm('¿Eliminar esta venta?')) return;
cultivoActual.ventas = cultivoActual.ventas.filter(v => v.id !== id);
guardarDatos();
eliminarVentaSupabase(id);
actualizarTablaVentas();
actualizarDashboard();
}
function abrirLiquidacion(ventaId) {
const venta = cultivoActual?.ventas.find(v => v.id === ventaId);
if (!venta) return;
document.getElementById('liquidacionVentaId').value = ventaId;
document.getElementById('liquidacionInfo').textContent = `${venta.cliente} - ${venta.fecha}`;
document.getElementById('liquidacionBruta').textContent = formatMoney(venta.bruta);
// Limpiar lista de descuentos
const lista = document.getElementById('listaDescuentosLiquidacion');
lista.innerHTML = '';
// Cargar descuentos existentes (nuevo formato o migrar viejo)
const descuentos = obtenerDescuentosVenta(venta);
if (descuentos.length > 0) {
descuentos.forEach(d => agregarFilaDescuento(d.motivo, d.monto));
}
actualizarVisibilidadDescuentos();
calcularNetaLiquidacion();
mostrarModal('modalLiquidacion');
}
function obtenerDescuentosVenta(venta) {
// Compatibilidad: migrar formato viejo (descCalidad/descRetraso) al nuevo (descuentos[])
if (venta.descuentos && venta.descuentos.length > 0) {
return venta.descuentos;
}
const desc = [];
if (venta.descCalidad && venta.descCalidad > 0) desc.push({ motivo: 'Calidad', monto: venta.descCalidad });
if (venta.descRetraso && venta.descRetraso > 0) desc.push({ motivo: 'Retraso', monto: venta.descRetraso });
return desc;
}
function obtenerTotalDescuentos(venta) {
const descuentos = obtenerDescuentosVenta(venta);
return descuentos.reduce((sum, d) => sum + (d.monto || 0), 0);
}
function agregarFilaDescuento(motivo = '', monto = '') {
const lista = document.getElementById('listaDescuentosLiquidacion');
const fila = document.createElement('div');
fila.className = 'fila-descuento';
fila.style.cssText = 'display:flex; gap:10px; align-items:center; margin-bottom:8px; padding:10px 12px; background:var(--arena); border-radius:var(--radio-sm); border-left:3px solid var(--peligro);';
fila.innerHTML = `
✕
`;
lista.appendChild(fila);
actualizarVisibilidadDescuentos();
// Enfocar el campo de motivo si está vacío
if (!motivo) fila.querySelector('.desc-motivo').focus();
}
function actualizarVisibilidadDescuentos() {
const filas = document.querySelectorAll('.fila-descuento');
const msg = document.getElementById('sinDescuentosMsg');
if (msg) msg.style.display = filas.length === 0 ? 'block' : 'none';
}
function calcularNetaLiquidacion() {
const ventaId = document.getElementById('liquidacionVentaId').value;
const venta = cultivoActual?.ventas.find(v => v.id === ventaId);
if (!venta) return;
let totalDesc = 0;
document.querySelectorAll('.desc-monto').forEach(inp => {
totalDesc += parseFloat(inp.value) || 0;
});
const neta = venta.bruta - totalDesc;
document.getElementById('liquidacionTotalDesc').textContent = formatMoney(totalDesc);
document.getElementById('liquidacionNeta').textContent = formatMoney(neta);
}
function aplicarLiquidacion() {
const ventaId = document.getElementById('liquidacionVentaId').value;
const venta = cultivoActual?.ventas.find(v => v.id === ventaId);
if (!venta) return;
// Recopilar descuentos dinámicos
const descuentos = [];
let totalDesc = 0;
const filas = document.querySelectorAll('.fila-descuento');
filas.forEach(fila => {
const motivo = fila.querySelector('.desc-motivo').value.trim() || 'Sin especificar';
const monto = toFixed2(parseFloat(fila.querySelector('.desc-monto').value) || 0);
if (monto > 0) {
descuentos.push({ motivo, monto });
totalDesc += monto;
}
});
venta.descuentos = descuentos;
// Mantener compatibilidad con campos viejos (para fórmulas que aún los lean)
venta.descCalidad = toFixed2(totalDesc);
venta.descRetraso = 0;
venta.neta = toFixed2(venta.bruta - totalDesc);
venta.liquidada = true;
venta.fechaLiquidacion = new Date().toISOString().split('T')[0];
guardarDatos();
guardarVentaSupabase(venta, cultivoActual.id);
guardarDescuentosVenta(venta.id, venta.descuentos || []);
actualizarTablaVentas();
actualizarDashboard();
cerrarModal('modalLiquidacion');
alert('Liquidación aplicada correctamente');
}
function actualizarTablaVentas() {
if (!cultivoActual) return;
const tbody = document.getElementById('tablaVentas');
if (cultivoActual.ventas.length === 0) {
tbody.innerHTML = 'Sin ventas registradas ';
return;
}
tbody.innerHTML = cultivoActual.ventas.map(v => {
const descTotal = obtenerTotalDescuentos(v);
const descDetalle = obtenerDescuentosVenta(v).map(d => `${d.motivo}: ${formatMoney(d.monto)}`).join(', ') || 'Sin descuentos';
const presInfo = v.presentacionNombre ? `${v.presentacionNombre} (${v.unidades || 1})` : '-';
const estadoClass = v.liquidada ? 'badge-success' : 'badge-warning';
const estadoTexto = v.liquidada ? '✅ Liquidada' : '⏳ Pendiente';
return `
${v.fecha}
${esc(v.cliente)}
${presInfo}
${formatNumber(v.cantidad)} kg
${formatMoney(v.bruta)}
${formatMoney(descTotal)}
${formatMoney(v.neta)}
${estadoTexto}
✏️
${!v.liquidada ? `💰 ` : ''}
🗑️
`;
}).join('');
}
function editarVenta(id) {
const venta = cultivoActual?.ventas.find(v => v.id === id);
if (!venta) return;
document.getElementById('ventaFecha').value = venta.fecha;
document.getElementById('ventaCliente').value = venta.cliente;
document.getElementById('ventaCantidad').value = venta.cantidad;
document.getElementById('ventaBruta').value = venta.bruta;
// Cargar presentación si existe
document.getElementById('ventaPresentacion').value = venta.presentacionId || '';
document.getElementById('ventaUnidades').value = venta.unidades || '';
// Guardar ID para edición
document.getElementById('ventaFecha').setAttribute('data-edit-id', id);
}
// ========================================
// ALMACÉN (POR CULTIVO)
// ========================================
let vistaAlmacenGlobal = true;
function toggleCategoriaPersonalizada() {
const select = document.getElementById('insumoCategoria');
const input = document.getElementById('insumoCategoriaPersonalizada');
if (select.value === '__OTRA__') {
input.style.display = 'block';
input.focus();
} else {
input.style.display = 'none';
input.value = '';
}
}
function actualizarSelectorAlmacen() {
const sel = document.getElementById('selectorAlmacen');
sel.innerHTML = '🌍 Todos los cultivos (Global) ' +
cultivos.map(c => `${esc(c.nombre)} `).join('');
}
function cambiarVistaAlmacen() {
const sel = document.getElementById('selectorAlmacen');
const vistaGlobal = document.getElementById('almacenVistaGlobal');
const vistaCultivo = document.getElementById('almacenVistaCultivo');
const formulario = document.getElementById('almacenFormulario');
const info = document.getElementById('almacenInfo');
if (sel.value === '__GLOBAL__') {
vistaAlmacenGlobal = true;
vistaGlobal.style.display = 'block';
vistaCultivo.style.display = 'none';
formulario.style.display = 'none';
info.textContent = 'Vista consolidada de todos los almacenes';
actualizarTablaAlmacenGlobal();
} else {
vistaAlmacenGlobal = false;
const cultivo = cultivos.find(c => c.id === sel.value);
if (!cultivo) return;
// Asegurar que el cultivo tenga array de almacén
if (!cultivo.almacen) cultivo.almacen = [];
vistaGlobal.style.display = 'none';
vistaCultivo.style.display = 'block';
formulario.style.display = 'block';
info.textContent = `Almacén de: ${cultivo.nombre}`;
actualizarTablaAlmacenCultivo(cultivo);
}
}
function getAlmacenActual() {
const sel = document.getElementById('selectorAlmacen');
if (sel.value === '__GLOBAL__') return null;
const cultivo = cultivos.find(c => c.id === sel.value);
if (!cultivo) return null;
if (!cultivo.almacen) cultivo.almacen = [];
return cultivo.almacen;
}
function getCultivoAlmacenActual() {
const sel = document.getElementById('selectorAlmacen');
return cultivos.find(c => c.id === sel.value);
}
function guardarInsumo() {
const almacenActual = getAlmacenActual();
if (!almacenActual) {
alert('Selecciona un cultivo primero');
return;
}
const nombre = document.getElementById('insumoNombre').value.trim();
let categoria = document.getElementById('insumoCategoria').value;
const categoriaPersonalizada = document.getElementById('insumoCategoriaPersonalizada').value.trim();
const cantidad = parseFloat(document.getElementById('insumoCantidad').value) || 0;
const unidad = document.getElementById('insumoUnidad').value;
const precio = parseFloat(document.getElementById('insumoPrecio').value) || 0;
const stockMin = parseFloat(document.getElementById('insumoStockMin').value) || 0;
const editId = document.getElementById('insumoNombre').getAttribute('data-edit-id');
if (!nombre) { alert('Ingresa el nombre del insumo'); return; }
if (categoria === '__OTRA__') {
if (!categoriaPersonalizada) {
alert('Ingresa el nombre de la nueva categoría');
return;
}
categoria = categoriaPersonalizada;
agregarCategoriaAlSelect(categoria);
}
if (editId) {
const insumo = almacenActual.find(i => i.id === editId);
if (insumo) {
insumo.nombre = nombre;
insumo.categoria = categoria;
insumo.cantidad = toFixed2(cantidad);
insumo.unidad = unidad;
insumo.precio = toFixed2(precio);
insumo.stockMin = toFixed2(stockMin);
}
document.getElementById('insumoNombre').removeAttribute('data-edit-id');
} else {
almacenActual.push({
id: uid('ins_'),
nombre,
categoria,
cantidad: toFixed2(cantidad),
unidad,
precio: toFixed2(precio),
stockMin: toFixed2(stockMin)
});
}
guardarDatos();
const insCultivoActual = getCultivoAlmacenActual();
const insGuardado = editId
? insCultivoActual.almacen.find(i => i.id === editId)
: insCultivoActual.almacen[insCultivoActual.almacen.length - 1];
if (insGuardado) guardarInsumoSupabase(insGuardado, insCultivoActual.id);
actualizarTablaAlmacenCultivo(insCultivoActual);
document.getElementById('insumoNombre').value = '';
document.getElementById('insumoCantidad').value = '';
document.getElementById('insumoPrecio').value = '';
document.getElementById('insumoStockMin').value = '';
document.getElementById('insumoCategoria').value = 'Insumos';
document.getElementById('insumoCategoriaPersonalizada').value = '';
document.getElementById('insumoCategoriaPersonalizada').style.display = 'none';
alert(editId ? 'Insumo actualizado' : 'Insumo guardado');
}
function agregarCategoriaAlSelect(nuevaCategoria) {
const select = document.getElementById('insumoCategoria');
const existe = Array.from(select.options).some(opt => opt.value === nuevaCategoria);
if (!existe) {
const otraOption = select.querySelector('option[value="__OTRA__"]');
const newOption = document.createElement('option');
newOption.value = nuevaCategoria;
newOption.textContent = nuevaCategoria;
select.insertBefore(newOption, otraOption);
}
}
function eliminarInsumo(id) {
if (!confirm('¿Eliminar este insumo?')) return;
const almacenActual = getAlmacenActual();
if (!almacenActual) return;
const cultivo = getCultivoAlmacenActual();
cultivo.almacen = almacenActual.filter(i => i.id !== id);
guardarDatos();
eliminarInsumoSupabase(id);
actualizarTablaAlmacenCultivo(cultivo);
}
function editarInsumo(id) {
const almacenActual = getAlmacenActual();
if (!almacenActual) return;
const insumo = almacenActual.find(i => i.id === id);
if (!insumo) return;
document.getElementById('insumoNombre').value = insumo.nombre;
const select = document.getElementById('insumoCategoria');
const categoriaExiste = Array.from(select.options).some(opt => opt.value === insumo.categoria);
if (!categoriaExiste && insumo.categoria !== '__OTRA__') {
agregarCategoriaAlSelect(insumo.categoria);
}
select.value = insumo.categoria;
document.getElementById('insumoCantidad').value = insumo.cantidad;
document.getElementById('insumoUnidad').value = insumo.unidad;
document.getElementById('insumoPrecio').value = insumo.precio;
document.getElementById('insumoStockMin').value = insumo.stockMin;
document.getElementById('insumoCategoriaPersonalizada').style.display = 'none';
document.getElementById('insumoNombre').setAttribute('data-edit-id', id);
}
function actualizarTablaAlmacenCultivo(cultivo) {
const tbody = document.getElementById('tablaAlmacen');
if (!cultivo || !cultivo.almacen || cultivo.almacen.length === 0) {
tbody.innerHTML = 'Sin insumos registrados en este cultivo ';
return;
}
tbody.innerHTML = cultivo.almacen.map(ins => {
const valor = toFixed2(ins.cantidad * ins.precio);
const esBajo = ins.cantidad <= ins.stockMin;
return `
${ins.nombre}
${ins.categoria}
${formatNumber(ins.cantidad)}
${ins.unidad}
${formatMoney(ins.precio)}
${formatMoney(valor)}
${esBajo ? '⚠️ Bajo' : '✓ OK'}
✏️
🗑️
`;
}).join('');
}
function actualizarTablaAlmacenGlobal() {
const tbody = document.getElementById('tablaAlmacenGlobal');
const resumen = document.getElementById('resumenAlmacenGlobal');
// Recopilar todos los insumos de todos los cultivos
const todosInsumos = [];
const categorias = {};
cultivos.forEach(cultivo => {
if (cultivo.almacen && cultivo.almacen.length > 0) {
cultivo.almacen.forEach(ins => {
todosInsumos.push({ ...ins, cultivoNombre: cultivo.nombre, cultivoId: cultivo.id });
// Agrupar por categoría
if (!categorias[ins.categoria]) {
categorias[ins.categoria] = { cantidad: 0, valor: 0 };
}
categorias[ins.categoria].cantidad++;
categorias[ins.categoria].valor += ins.cantidad * ins.precio;
});
}
});
if (todosInsumos.length === 0) {
tbody.innerHTML = 'Sin insumos registrados en ningún cultivo ';
resumen.innerHTML = 'Sin datos
';
return;
}
tbody.innerHTML = todosInsumos.map(ins => {
const valor = toFixed2(ins.cantidad * ins.precio);
const esBajo = ins.cantidad <= ins.stockMin;
return `
${ins.nombre}
${ins.categoria}
${ins.cultivoNombre}
${formatNumber(ins.cantidad)}
${ins.unidad}
${formatMoney(ins.precio)}
${formatMoney(valor)}
${esBajo ? '⚠️ Bajo' : '✓ OK'}
`;
}).join('');
// Resumen por categoría
resumen.innerHTML = Object.entries(categorias).map(([cat, data]) => `
${cat}
${data.cantidad} items
${formatMoney(data.valor)}
`).join('');
}
// Migrar almacén global existente a cultivos (compatibilidad)
function migrarAlmacenACultivos() {
if (almacen.length > 0 && cultivos.length > 0) {
// Si hay almacén global y cultivos sin almacén propio, migrar al primer cultivo
const cultivoSinAlmacen = cultivos.find(c => !c.almacen || c.almacen.length === 0);
if (cultivoSinAlmacen) {
cultivoSinAlmacen.almacen = [...almacen];
almacen = []; // Limpiar almacén global
guardarDatos();
console.log('Almacén migrado a:', cultivoSinAlmacen.nombre);
}
}
}
function actualizarTablaAlmacen() {
// Función de compatibilidad - redirige según vista actual
if (vistaAlmacenGlobal) {
actualizarTablaAlmacenGlobal();
} else {
const cultivo = getCultivoAlmacenActual();
if (cultivo) actualizarTablaAlmacenCultivo(cultivo);
}
}
// ========================================
// APERTURA HISTÓRICA
// ========================================
function guardarApertura() {
if (!cultivoActual) { alert('Selecciona un cultivo'); return; }
cultivoActual.apertura = {
semanaInicio: parseInt(document.getElementById('aperturaSemana').value) || 1,
gastosAnteriores: toFixed2(parseFloat(document.getElementById('aperturaGastos').value) || 0),
ventasAnteriores: toFixed2(parseFloat(document.getElementById('aperturaVentas').value) || 0),
produccionAnterior: toFixed2(parseFloat(document.getElementById('aperturaProduccion').value) || 0)
};
guardarDatos();
guardarAperturaSupabase(cultivoActual.apertura, cultivoActual.id);
actualizarUIHistorico();
actualizarDashboard();
alert('Apertura guardada');
}
function eliminarApertura() {
if (!cultivoActual) return;
if (!confirm('¿Eliminar datos de apertura?')) return;
cultivoActual.apertura = null;
guardarDatos();
eliminarAperturaSupabase(cultivoActual.id);
actualizarUIHistorico();
actualizarDashboard();
}
function actualizarUIHistorico() {
if (!cultivoActual) return;
const ap = cultivoActual.apertura;
const resumen = document.getElementById('resumenApertura');
if (ap) {
document.getElementById('aperturaSemana').value = ap.semanaInicio || '';
document.getElementById('aperturaGastos').value = ap.gastosAnteriores || '';
document.getElementById('aperturaVentas').value = ap.ventasAnteriores || '';
document.getElementById('aperturaProduccion').value = ap.produccionAnterior || '';
document.getElementById('aperturaResumenTexto').textContent =
`Semana ${ap.semanaInicio} · Gastos: ${formatMoney(ap.gastosAnteriores)} · Ventas: ${formatMoney(ap.ventasAnteriores)} · Producción: ${formatNumber(ap.produccionAnterior)} kg`;
resumen.style.display = 'block';
} else {
document.getElementById('aperturaSemana').value = '';
document.getElementById('aperturaGastos').value = '';
document.getElementById('aperturaVentas').value = '';
document.getElementById('aperturaProduccion').value = '';
resumen.style.display = 'none';
}
}
// ========================================
// DASHBOARD
// ========================================
function calcularTotales() {
if (!cultivoActual) return { inversionTotal: 0, ventasNetas: 0, utilidadNeta: 0, roi: 0, porcMerma: 0, produccionTotal: 0, dscr: null };
const gastosRegistrados = cultivoActual.cierres.reduce((s, c) => s + c.totalGastos + c.gastoExtra + c.valorConsumo, 0);
const aperturaGastos = cultivoActual.apertura?.gastosAnteriores || 0;
const aperturaVentas = cultivoActual.apertura?.ventasAnteriores || 0;
const aperturaProd = cultivoActual.apertura?.produccionAnterior || 0;
const inversionTotal = toFixed2(gastosRegistrados + aperturaGastos);
const ventasNetas = toFixed2(cultivoActual.ventas.reduce((s, v) => s + v.neta, 0) + aperturaVentas);
const utilidadNeta = toFixed2(ventasNetas - inversionTotal);
const roi = inversionTotal > 0 ? toFixed2((utilidadNeta / inversionTotal) * 100) : 0;
const produccionTotal = cultivoActual.cierres.reduce((s, c) => s + c.cosechaTotal, 0) + aperturaProd;
const mermaTotal = cultivoActual.cierres.reduce((s, c) => s + c.merma, 0);
const porcMerma = produccionTotal > 0 ? toFixed2((mermaTotal / produccionTotal) * 100) : 0;
// DSCR unificado: crédito simulado + deudas registradas
let dscr = null;
let cuotaMensualTotal = 0;
// 1. Cuota del crédito simulado vinculado al cultivo
if (cultivoActual.credito) {
cuotaMensualTotal += cultivoActual.credito.cuota || 0;
}
// 2. Pagos mensuales de deudas asignadas a este cultivo (o prorrateadas)
if (typeof getPagoMensualDeudas === 'function') {
cuotaMensualTotal += getPagoMensualDeudas(cultivoActual.id);
}
if (cuotaMensualTotal > 0) {
const semanas = cultivoActual.cierres.length || 1;
const utilidadMensual = utilidadNeta / (semanas / 4);
dscr = toFixed2(utilidadMensual / cuotaMensualTotal);
}
return { inversionTotal, ventasNetas, utilidadNeta, roi, porcMerma, produccionTotal, dscr, cuotaMensualTotal };
}
function actualizarDashboard() {
if (!cultivoActual) return;
const t = calcularTotales();
document.getElementById('dashNombreCultivo').textContent = cultivoActual.nombre;
document.getElementById('dashArea').textContent = formatNumber(cultivoActual.area);
document.getElementById('dashSemana').textContent = cultivoActual.cierres.length || 0;
document.getElementById('dashMoneda').textContent = cultivoActual.moneda;
document.getElementById('dashEstado').textContent = cultivoActual.estado;
document.getElementById('valROI').textContent = formatPercent(t.roi);
document.getElementById('valUtilidad').textContent = formatMoney(t.utilidadNeta);
document.getElementById('valInversion').textContent = formatMoney(t.inversionTotal);
document.getElementById('valVentas').textContent = formatMoney(t.ventasNetas);
document.getElementById('valMerma').textContent = formatPercent(t.porcMerma);
document.getElementById('kpiROI').className = 'kpi-card ' + (t.roi >= 20 ? 'success' : t.roi >= 0 ? 'warning' : 'danger');
document.getElementById('kpiUtilidad').className = 'kpi-card ' + (t.utilidadNeta >= 0 ? 'success' : 'danger');
document.getElementById('kpiMerma').className = 'kpi-card ' + (t.porcMerma <= 8 ? 'success' : t.porcMerma <= 15 ? 'warning' : 'danger');
const dscrEl = document.getElementById('valDSCR');
if (t.dscr !== null) {
dscrEl.textContent = formatNumber(t.dscr);
dscrEl.className = 'dscr-badge ' + (t.dscr >= 1.2 ? 'dscr-verde' : t.dscr >= 1.0 ? 'dscr-amarillo' : 'dscr-rojo');
} else {
dscrEl.textContent = 'N/A';
dscrEl.className = 'dscr-badge';
}
generarAlertasIA(t);
actualizarGraficos();
actualizarStatsVentas();
}
function generarAlertasIA(t) {
const container = document.getElementById('alertasIA');
const alertas = [];
if (t.dscr !== null && t.dscr < 1.0) {
alertas.push({ tipo: 'danger', icon: '🚨', titulo: 'Riesgo de Insolvencia', msg: `El DSCR es ${formatNumber(t.dscr)}. La utilidad no cubre la cuota del crédito.` });
} else if (t.dscr !== null && t.dscr < 1.2) {
alertas.push({ tipo: 'warning', icon: '⚠️', titulo: 'DSCR en Zona de Cuidado', msg: `El DSCR es ${formatNumber(t.dscr)}. Está cerca del límite.` });
}
if (t.porcMerma > 15) {
alertas.push({ tipo: 'danger', icon: '📉', titulo: 'Merma Crítica', msg: `La merma es del ${formatPercent(t.porcMerma)}. Revise procesos.` });
}
if (t.roi < 0) {
alertas.push({ tipo: 'danger', icon: '💸', titulo: 'ROI Negativo', msg: `El retorno es del ${formatPercent(t.roi)}. Está generando pérdidas.` });
}
const alertaDesv = verificarDesviacionGastos();
if (alertaDesv) alertas.push(alertaDesv);
if (alertas.length === 0) {
alertas.push({ tipo: 'success', icon: '✅', titulo: '¡Todo en orden!', msg: 'No hay alertas críticas.' });
}
container.innerHTML = alertas.map(a => `
`).join('');
// Actualizar Score de Salud en Dashboard
actualizarScoreSalud(t);
}
function verificarDesviacionGastos() {
if (!cultivoActual || cultivoActual.cierres.length < 3) return null;
const gastos = cultivoActual.cierres.map(c => c.totalGastos + c.gastoExtra + c.valorConsumo);
const media = gastos.reduce((s, g) => s + g, 0) / gastos.length;
const varianza = gastos.reduce((s, g) => s + Math.pow(g - media, 2), 0) / gastos.length;
const desviacion = Math.sqrt(varianza);
const ultimo = gastos[gastos.length - 1];
if (Math.abs(ultimo - media) > 2 * desviacion) {
return {
tipo: 'warning',
icon: '📊',
titulo: 'Gasto Atípico',
msg: `El gasto de la última semana (${formatMoney(ultimo)}) se desvía más de 2σ del promedio (${formatMoney(media)}).`
};
}
return null;
}
// ========================================
// SCORE DE SALUD Y ANÁLISIS DETALLADO
// ========================================
function calcularScoreSalud(t) {
if (!cultivoActual || cultivoActual.cierres.length === 0) {
return { total: 0, desglose: {} };
}
// Componentes del score (cada uno de 0-100, con peso)
const componentes = {};
// 1. ROI (peso: 25%) - positivo es bueno
let scoreROI = 50; // neutral
if (t.roi > 20) scoreROI = 100;
else if (t.roi > 10) scoreROI = 80;
else if (t.roi > 0) scoreROI = 60;
else if (t.roi > -10) scoreROI = 40;
else scoreROI = 20;
componentes.roi = { valor: scoreROI, peso: 0.25, label: 'Rentabilidad (ROI)' };
// 2. DSCR (peso: 20%) - si hay crédito
if (t.dscr !== null) {
let scoreDSCR = 50;
if (t.dscr >= 1.5) scoreDSCR = 100;
else if (t.dscr >= 1.2) scoreDSCR = 80;
else if (t.dscr >= 1.0) scoreDSCR = 60;
else if (t.dscr >= 0.8) scoreDSCR = 40;
else scoreDSCR = 20;
componentes.dscr = { valor: scoreDSCR, peso: 0.20, label: 'Cobertura Deuda (DSCR)' };
}
// 3. Merma (peso: 20%) - menor es mejor
let scoreMerma = 100;
if (t.porcMerma > 20) scoreMerma = 20;
else if (t.porcMerma > 15) scoreMerma = 40;
else if (t.porcMerma > 10) scoreMerma = 60;
else if (t.porcMerma > 5) scoreMerma = 80;
componentes.merma = { valor: scoreMerma, peso: 0.20, label: 'Control de Merma' };
// 4. Cumplimiento de Presupuesto (peso: 20%)
let scorePres = 100;
const presTotal = cultivoActual.presupuestoTotal || 0;
if (presTotal > 0) {
const porcentajeUsado = (t.inversionTotal / presTotal) * 100;
if (porcentajeUsado > 120) scorePres = 20;
else if (porcentajeUsado > 100) scorePres = 50;
else if (porcentajeUsado > 80) scorePres = 80;
else scorePres = 100;
}
componentes.presupuesto = { valor: scorePres, peso: 0.20, label: 'Cumplimiento Presupuesto' };
// 5. Eficiencia Productiva (peso: 15%) - kg/$ invertido
let scoreEficiencia = 50;
if (t.inversionTotal > 0 && t.produccionTotal > 0) {
const eficiencia = t.produccionTotal / t.inversionTotal;
if (eficiencia > 0.1) scoreEficiencia = 100;
else if (eficiencia > 0.05) scoreEficiencia = 80;
else if (eficiencia > 0.02) scoreEficiencia = 60;
else scoreEficiencia = 40;
}
componentes.eficiencia = { valor: scoreEficiencia, peso: 0.15, label: 'Eficiencia Productiva' };
// Calcular score total ponderado
let totalPeso = 0;
let totalScore = 0;
Object.values(componentes).forEach(c => {
totalScore += c.valor * c.peso;
totalPeso += c.peso;
});
// Ajustar si no hay DSCR
const scoreFinal = totalPeso > 0 ? Math.round(totalScore / totalPeso) : 0;
return { total: scoreFinal, desglose: componentes };
}
function actualizarScoreSalud(t) {
const score = calcularScoreSalud(t);
// Actualizar barra y valor en Dashboard
const barra = document.getElementById('scoreSaludBarra');
const valor = document.getElementById('scoreSaludValor');
if (barra && valor) {
barra.style.width = score.total + '%';
barra.style.background = score.total >= 70 ? 'var(--exito)' : score.total >= 40 ? 'var(--advertencia)' : 'var(--peligro)';
valor.textContent = score.total + '/100';
valor.style.color = score.total >= 70 ? 'var(--exito)' : score.total >= 40 ? 'var(--advertencia)' : 'var(--peligro)';
}
}
function actualizarAnalisisDetallado(t) {
if (!cultivoActual) return;
const score = calcularScoreSalud(t);
// Score grande en Inteligencia IA
const scoreValorGrande = document.getElementById('scoreValorGrande');
const scoreBarraGrande = document.getElementById('scoreBarraGrande');
if (scoreValorGrande && scoreBarraGrande) {
scoreValorGrande.textContent = score.total + '/100';
scoreValorGrande.style.color = score.total >= 70 ? 'var(--exito)' : score.total >= 40 ? 'var(--advertencia)' : 'var(--peligro)';
scoreBarraGrande.style.width = score.total + '%';
scoreBarraGrande.style.background = score.total >= 70 ? 'var(--exito)' : score.total >= 40 ? 'var(--advertencia)' : 'var(--peligro)';
}
// Desglose del score
const desglose = document.getElementById('scoreDesglose');
if (desglose) {
desglose.innerHTML = Object.entries(score.desglose).map(([key, comp]) => {
const color = comp.valor >= 70 ? 'var(--exito)' : comp.valor >= 40 ? 'var(--advertencia)' : 'var(--peligro)';
return `
${comp.label}
${comp.valor}/100
`;
}).join('');
}
// Generar alertas detalladas categorizadas
generarAlertasDetalladas(t, score);
// Generar proyecciones
generarProyecciones(t);
}
function generarAlertasDetalladas(t, score) {
const criticas = [];
const advertencias = [];
const oportunidades = [];
// === ALERTAS CRÍTICAS ===
if (t.roi < -10) {
criticas.push({ titulo: 'Pérdidas Significativas', desc: `ROI de ${formatPercent(t.roi)}. Requiere acción inmediata.` });
} else if (t.roi < 0) {
criticas.push({ titulo: 'ROI Negativo', desc: `Retorno de ${formatPercent(t.roi)}. El cultivo no es rentable aún.` });
}
if (t.dscr !== null && t.dscr < 1.0) {
criticas.push({ titulo: 'Riesgo de Insolvencia', desc: `DSCR de ${formatNumber(t.dscr)}. No cubre el servicio de deuda.` });
}
if (t.porcMerma > 20) {
criticas.push({ titulo: 'Merma Excesiva', desc: `${formatPercent(t.porcMerma)} de pérdida. Revisar urgente.` });
}
// Stock crítico
const almacenCultivoIA = cultivoActual?.almacen || [];
const stockCritico = almacenCultivoIA.filter(i => i.stockMin > 0 && i.cantidad <= i.stockMin);
if (stockCritico.length > 0) {
criticas.push({ titulo: 'Stock Crítico', desc: `${stockCritico.length} insumo(s) bajo mínimo: ${stockCritico.map(i => i.nombre).join(', ')}` });
}
// === ADVERTENCIAS ===
if (t.dscr !== null && t.dscr >= 1.0 && t.dscr < 1.2) {
advertencias.push({ titulo: 'DSCR en Zona de Cuidado', desc: `DSCR de ${formatNumber(t.dscr)}. Monitorear de cerca.` });
}
if (t.porcMerma > 10 && t.porcMerma <= 20) {
advertencias.push({ titulo: 'Merma Elevada', desc: `${formatPercent(t.porcMerma)} de merma. Considerar mejoras.` });
}
// Presupuesto
const presTotal = cultivoActual.presupuestoTotal || 0;
if (presTotal > 0) {
const porcentajeUsado = (t.inversionTotal / presTotal) * 100;
if (porcentajeUsado > 100) {
advertencias.push({ titulo: 'Presupuesto Excedido', desc: `Has usado ${formatPercent(porcentajeUsado)} del presupuesto.` });
} else if (porcentajeUsado > 80) {
advertencias.push({ titulo: 'Presupuesto al Límite', desc: `${formatPercent(porcentajeUsado)} consumido. Quedan ${formatMoney(presTotal - t.inversionTotal)}.` });
}
}
// Tendencia de merma (3 semanas consecutivas)
if (cultivoActual.cierres.length >= 3) {
const ultimas3 = cultivoActual.cierres.slice(-3);
const mermasSubiendo = ultimas3[0].merma < ultimas3[1].merma && ultimas3[1].merma < ultimas3[2].merma;
if (mermasSubiendo) {
advertencias.push({ titulo: 'Merma en Aumento', desc: 'La merma ha subido por 3 semanas consecutivas.' });
}
}
// Gasto atípico
const alertaDesv = verificarDesviacionGastos();
if (alertaDesv) {
advertencias.push({ titulo: alertaDesv.titulo, desc: alertaDesv.msg });
}
// === OPORTUNIDADES ===
if (t.roi > 15) {
oportunidades.push({ titulo: 'Excelente Rentabilidad', desc: `ROI de ${formatPercent(t.roi)}. Considerar reinversión o expansión.` });
} else if (t.roi > 5) {
oportunidades.push({ titulo: 'Buena Rentabilidad', desc: `ROI de ${formatPercent(t.roi)}. El cultivo es rentable.` });
}
if (t.porcMerma < 5 && cultivoActual.cierres.length > 2) {
oportunidades.push({ titulo: 'Merma Controlada', desc: `Solo ${formatPercent(t.porcMerma)} de merma. Excelente control de calidad.` });
}
// Identificar categoría de mayor gasto para optimización
if (cultivoActual.categorias.length > 0) {
const catMayorGasto = cultivoActual.categorias.reduce((max, cat) => cat.gastado > max.gastado ? cat : max, cultivoActual.categorias[0]);
if (catMayorGasto.gastado > 0) {
const porcentaje = (catMayorGasto.gastado / t.inversionTotal) * 100;
if (porcentaje > 40) {
oportunidades.push({ titulo: 'Optimización de Costos', desc: `"${catMayorGasto.nombre}" representa el ${formatPercent(porcentaje)} de gastos. Buscar eficiencias.` });
}
}
}
if (t.dscr !== null && t.dscr >= 1.5) {
oportunidades.push({ titulo: 'Capacidad de Deuda', desc: `DSCR de ${formatNumber(t.dscr)}. Buena capacidad para financiamiento adicional.` });
}
// Renderizar
const renderAlertas = (lista, clase) => lista.length > 0
? lista.map(a => ``).join('')
: `Sin alertas
`;
document.querySelector('#alertasCriticas .alertas-lista').innerHTML = renderAlertas(criticas, 'critica');
document.querySelector('#alertasAdvertencias .alertas-lista').innerHTML = renderAlertas(advertencias, 'advertencia');
document.querySelector('#alertasOportunidades .alertas-lista').innerHTML = renderAlertas(oportunidades, 'oportunidad');
}
function generarProyecciones(t) {
const container = document.getElementById('proyeccionesInteligentes');
if (!container || !cultivoActual) return;
const cierres = cultivoActual.cierres;
const proyecciones_arr = [];
// 1. Proyección de quiebre de presupuesto
const presTotal = cultivoActual.presupuestoTotal || 0;
if (presTotal > 0 && cierres.length >= 2) {
const gastoPromedio = cierres.reduce((s, c) => s + c.totalGastos + c.gastoExtra + c.valorConsumo, 0) / cierres.length;
const disponible = presTotal - t.inversionTotal;
const semanasRestantes = disponible > 0 ? Math.floor(disponible / gastoPromedio) : 0;
proyecciones_arr.push({
icono: '💰',
valor: semanasRestantes > 0 ? `${semanasRestantes} sem` : 'Agotado',
label: 'Presupuesto para',
detalle: `Gasto promedio: ${formatMoney(gastoPromedio)}/sem`,
color: semanasRestantes > 4 ? 'var(--exito)' : semanasRestantes > 2 ? 'var(--advertencia)' : 'var(--peligro)'
});
}
// 2. Proyección de agotamiento de stock
const almacenProy = cultivoActual?.almacen || [];
if (almacenProy.length > 0 && cierres.length >= 2) {
// Calcular consumo promedio de insumos
let insumoMasCritico = null;
let semanasMinimas = Infinity;
almacenProy.forEach(ins => {
const consumos = cierres.filter(c => c.consumos).map(c => {
const consumo = c.consumos?.find(x => x.insumoId === ins.id);
return consumo ? consumo.cantidad : 0;
});
if (consumos.length > 0) {
const consumoPromedio = consumos.reduce((s, c) => s + c, 0) / consumos.length;
if (consumoPromedio > 0) {
const semanas = Math.floor(ins.cantidad / consumoPromedio);
if (semanas < semanasMinimas) {
semanasMinimas = semanas;
insumoMasCritico = ins.nombre;
}
}
}
});
if (insumoMasCritico && semanasMinimas < 10) {
proyecciones_arr.push({
icono: '📦',
valor: `${semanasMinimas} sem`,
label: 'Stock crítico próximo',
detalle: `"${insumoMasCritico}" se agotará primero`,
color: semanasMinimas > 4 ? 'var(--exito)' : semanasMinimas > 2 ? 'var(--advertencia)' : 'var(--peligro)'
});
}
}
// 3. Proyección de ROI al final del ciclo
if (cierres.length >= 3 && cultivoActual.duracionSemanas) {
const semanasRestantes = cultivoActual.duracionSemanas - cierres.length;
if (semanasRestantes > 0) {
const gastoPromedio = cierres.reduce((s, c) => s + c.totalGastos + c.gastoExtra + c.valorConsumo, 0) / cierres.length;
const ventaPromedio = cultivoActual.ventas.length > 0
? cultivoActual.ventas.reduce((s, v) => s + v.neta, 0) / cultivoActual.ventas.length
: 0;
const inversionProyectada = t.inversionTotal + (gastoPromedio * semanasRestantes);
const ventasProyectadas = t.ventasNetas + (ventaPromedio * semanasRestantes * 0.8); // Factor conservador
const roiProyectado = inversionProyectada > 0 ? ((ventasProyectadas - inversionProyectada) / inversionProyectada) * 100 : 0;
proyecciones_arr.push({
icono: '📈',
valor: formatPercent(roiProyectado),
label: 'ROI proyectado al cierre',
detalle: `Faltan ${semanasRestantes} semanas`,
color: roiProyectado > 10 ? 'var(--exito)' : roiProyectado > 0 ? 'var(--advertencia)' : 'var(--peligro)'
});
}
}
// 4. Tendencia de eficiencia
if (cierres.length >= 4) {
const mitad = Math.floor(cierres.length / 2);
const primera = cierres.slice(0, mitad);
const segunda = cierres.slice(mitad);
const eficiencia1 = primera.reduce((s, c) => s + c.cosechaTotal, 0) / primera.reduce((s, c) => s + c.totalGastos + c.gastoExtra + c.valorConsumo, 0);
const eficiencia2 = segunda.reduce((s, c) => s + c.cosechaTotal, 0) / segunda.reduce((s, c) => s + c.totalGastos + c.gastoExtra + c.valorConsumo, 0);
const cambio = eficiencia1 > 0 ? ((eficiencia2 - eficiencia1) / eficiencia1) * 100 : 0;
proyecciones_arr.push({
icono: cambio >= 0 ? '📊' : '📉',
valor: `${cambio >= 0 ? '+' : ''}${formatNumber(cambio)}%`,
label: 'Tendencia eficiencia',
detalle: cambio >= 0 ? 'Mejorando vs inicio' : 'Disminuyendo vs inicio',
color: cambio > 5 ? 'var(--exito)' : cambio > -5 ? 'var(--advertencia)' : 'var(--peligro)'
});
}
// Renderizar
if (proyecciones_arr.length === 0) {
container.innerHTML = `
Registra más datos (mínimo 3 cierres semanales) para ver proyecciones.
`;
} else {
container.innerHTML = proyecciones_arr.map(p => `
${p.icono}
${p.valor}
${p.label}
${p.detalle}
`).join('');
}
}
// ========================================
// DASHBOARD GLOBAL
// ========================================
function actualizarDashboardGlobal() {
// Si no hay cultivos, mostrar estado vacío
if (cultivos.length === 0) {
document.getElementById('globalHectareas').textContent = '0';
document.getElementById('globalInversion').textContent = '$0.00';
document.getElementById('globalVentas').textContent = '$0.00';
document.getElementById('globalUtilidad').textContent = '$0.00';
document.getElementById('globalROIPromedio').textContent = '0.00%';
document.getElementById('globalScoreSalud').textContent = '0/100';
document.getElementById('globalMejorCultivo').innerHTML = 'Sin cultivos
';
document.getElementById('globalPeorCultivo').innerHTML = 'Sin cultivos
';
document.getElementById('globalAlertasConsolidadas').innerHTML = 'Agrega un cultivo para ver alertas
';
document.getElementById('globalStockCritico').innerHTML = 'Sin inventario
';
const tablaGlobal = document.getElementById('tablaGlobal');
if (tablaGlobal) tablaGlobal.innerHTML = 'Sin cultivos registrados ';
return;
}
let totalHa = 0, totalInversion = 0, totalVentas = 0, totalUtilidad = 0;
let totalROI = 0, totalScore = 0;
const resumen = cultivos.map(c => {
const temp = cultivoActual;
cultivoActual = c;
const t = calcularTotales();
const score = calcularScoreSalud(t);
cultivoActual = temp;
totalHa += c.area;
totalInversion += t.inversionTotal;
totalVentas += t.ventasNetas;
totalUtilidad += t.utilidadNeta;
totalROI += t.roi;
totalScore += score.total;
return { cultivo: c, totales: t, score: score.total };
});
const roiPromedio = resumen.length > 0 ? totalROI / resumen.length : 0;
const scorePromedio = resumen.length > 0 ? Math.round(totalScore / resumen.length) : 0;
document.getElementById('globalHectareas').textContent = formatNumber(totalHa);
document.getElementById('globalInversion').textContent = formatMoney(totalInversion);
document.getElementById('globalVentas').textContent = formatMoney(totalVentas);
document.getElementById('globalUtilidad').textContent = formatMoney(totalUtilidad);
document.getElementById('globalROIPromedio').textContent = formatPercent(roiPromedio);
document.getElementById('globalScoreSalud').textContent = scorePromedio + '/100';
// Mejor y peor cultivo
if (resumen.length > 0) {
const ordenados = [...resumen].sort((a, b) => b.totales.roi - a.totales.roi);
const mejor = ordenados[0];
const peor = ordenados[ordenados.length - 1];
document.getElementById('globalMejorCultivo').innerHTML = `
${mejor.cultivo.nombre}
${formatPercent(mejor.totales.roi)}
ROI
`;
document.getElementById('globalPeorCultivo').innerHTML = `
${peor.cultivo.nombre}
${formatPercent(peor.totales.roi)}
ROI
`;
}
// Alertas consolidadas
const alertasConsolidadas = [];
resumen.forEach(r => {
if (r.totales.roi < 0) alertasConsolidadas.push({ tipo: 'critica', texto: `${r.cultivo.nombre}: ROI negativo (${formatPercent(r.totales.roi)})` });
if (r.totales.porcMerma > 15) alertasConsolidadas.push({ tipo: 'advertencia', texto: `${r.cultivo.nombre}: Merma alta (${formatPercent(r.totales.porcMerma)})` });
if (r.totales.dscr !== null && r.totales.dscr < 1) alertasConsolidadas.push({ tipo: 'critica', texto: `${r.cultivo.nombre}: DSCR crítico (${formatNumber(r.totales.dscr)})` });
});
const alertasDiv = document.getElementById('globalAlertasConsolidadas');
if (alertasConsolidadas.length > 0) {
alertasDiv.innerHTML = alertasConsolidadas.map(a => `
${a.texto}
`).join('');
} else {
alertasDiv.innerHTML = '✅ Sin alertas críticas
';
}
// Stock crítico global
const stockCritico = [];
cultivos.forEach(c => {
if (c.almacen) {
c.almacen.forEach(ins => {
if (ins.stockMin > 0 && ins.cantidad <= ins.stockMin) {
stockCritico.push({ insumo: ins.nombre, cultivo: c.nombre, cantidad: ins.cantidad, unidad: ins.unidad });
}
});
}
});
const stockDiv = document.getElementById('globalStockCritico');
if (stockCritico.length > 0) {
stockDiv.innerHTML = stockCritico.slice(0, 5).map(s => `
${s.insumo} (${s.cultivo})
${formatNumber(s.cantidad)} ${s.unidad} restantes
`).join('') + (stockCritico.length > 5 ? `+${stockCritico.length - 5} más...
` : '');
} else {
stockDiv.innerHTML = '✅ Stock OK en todos los cultivos
';
}
// Resumen de deudas
const deudasDiv = document.getElementById('globalDeudasResumen');
if (deudas.length > 0) {
deudasDiv.style.display = 'block';
const totalDeudas = deudas.reduce((sum, d) => sum + d.saldoActual, 0);
const pagoMensual = deudas.reduce((sum, d) => sum + d.pagoMensual, 0);
document.getElementById('globalTotalDeudas').textContent = formatMoney(totalDeudas);
document.getElementById('globalPagoMensualDeudas').textContent = formatMoney(pagoMensual);
const deudasConVenc = deudas.filter(d => d.vencimiento).sort((a, b) => new Date(a.vencimiento) - new Date(b.vencimiento));
if (deudasConVenc.length > 0) {
const dias = Math.ceil((new Date(deudasConVenc[0].vencimiento) - new Date()) / (1000 * 60 * 60 * 24));
document.getElementById('globalProximaDeuda').textContent = dias > 0 ? `${dias} días` : 'Vencida';
}
} else {
deudasDiv.style.display = 'none';
}
// Tabla resumen
const tbody = document.getElementById('tablaResumenGlobal');
if (cultivos.length === 0) {
tbody.innerHTML = 'No hay cultivos ';
} else {
tbody.innerHTML = resumen.map(r => `
${r.cultivo.nombre}
${formatNumber(r.cultivo.area)} ha
S${r.cultivo.cierres.length || 0}
${formatMoney(r.totales.inversionTotal)}
${formatMoney(r.totales.ventasNetas)}
${formatMoney(r.totales.utilidadNeta)}
${formatPercent(r.totales.roi)}
${r.score}/100
`).join('');
}
actualizarGraficosGlobales(resumen);
}
function actualizarGraficosGlobales(datos) {
if (charts.globalRent) charts.globalRent.destroy();
const ctx1 = document.getElementById('chartGlobalRentabilidad');
if (ctx1 && datos.length > 0) {
charts.globalRent = new Chart(ctx1, {
type: 'bar',
data: {
labels: datos.map(d => d.cultivo.nombre),
datasets: [
{ label: 'Utilidad', data: datos.map(d => d.totales.utilidadNeta), backgroundColor: '#2F5D3A' },
{ label: 'Inversión', data: datos.map(d => d.totales.inversionTotal), backgroundColor: '#7A5C3E' }
]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
if (charts.globalInv) charts.globalInv.destroy();
const ctx2 = document.getElementById('chartGlobalInversion');
if (ctx2 && datos.length > 0) {
charts.globalInv = new Chart(ctx2, {
type: 'pie',
data: {
labels: datos.map(d => d.cultivo.nombre),
datasets: [{
data: datos.map(d => d.totales.inversionTotal),
backgroundColor: ['#2F5D3A', '#7A5C3E', '#4A7C59', '#9B7B5B', '#3A6B7C']
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
}
// ========================================
// GRÁFICOS CULTIVO
// ========================================
function actualizarGraficos() {
if (!cultivoActual) return;
// Tendencia de gastos
if (charts.tendencia) charts.tendencia.destroy();
const ctxTend = document.getElementById('chartTendencia');
if (ctxTend && cultivoActual.cierres.length > 0) {
let acum = 0;
const datosAcum = cultivoActual.cierres.map(c => {
acum += c.totalGastos + c.gastoExtra + c.valorConsumo;
return toFixed2(acum);
});
charts.tendencia = new Chart(ctxTend, {
type: 'line',
data: {
labels: cultivoActual.cierres.map(c => `S${c.semana}`),
datasets: [{
label: 'Gasto Acumulado',
data: datosAcum,
borderColor: '#2F5D3A',
backgroundColor: 'rgba(47, 93, 58, 0.1)',
fill: true,
tension: 0.4
}]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
// Por concepto
if (charts.conceptos) charts.conceptos.destroy();
const ctxConc = document.getElementById('chartConceptos');
if (ctxConc && cultivoActual.categorias.length > 0) {
const gastoPorConcepto = {};
const coloresPorConcepto = {};
cultivoActual.categorias.forEach(cat => {
const concepto = conceptos.find(c => c.id === cat.conceptoId);
const nombre = concepto ? concepto.nombre : 'Sin Concepto';
const color = concepto ? concepto.color : '#455A64';
gastoPorConcepto[nombre] = (gastoPorConcepto[nombre] || 0) + cat.gastado;
coloresPorConcepto[nombre] = color;
});
const labels = Object.keys(gastoPorConcepto);
charts.conceptos = new Chart(ctxConc, {
type: 'pie',
data: {
labels: labels,
datasets: [{ data: Object.values(gastoPorConcepto), backgroundColor: labels.map(l => coloresPorConcepto[l]) }]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
// Producción
if (charts.produccion) charts.produccion.destroy();
const ctxProd = document.getElementById('chartProduccion');
if (ctxProd && cultivoActual.cierres.length > 0) {
charts.produccion = new Chart(ctxProd, {
type: 'bar',
data: {
labels: cultivoActual.cierres.map(c => `S${c.semana}`),
datasets: [
{ label: 'Cosecha Total', data: cultivoActual.cierres.map(c => c.cosechaTotal), backgroundColor: '#2F5D3A' },
{ label: '1ra Calidad', data: cultivoActual.cierres.map(c => c.primeraCalidad), backgroundColor: '#4A7C59' }
]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
// Stock crítico
if (charts.stockCritico) charts.stockCritico.destroy();
const ctxStock = document.getElementById('chartStockCritico');
const almacenChart = cultivoActual?.almacen || [];
if (ctxStock && almacenChart.length > 0) {
const sorted = [...almacenChart].sort((a, b) => a.cantidad - b.cantidad).slice(0, 5);
charts.stockCritico = new Chart(ctxStock, {
type: 'bar',
data: {
labels: sorted.map(i => i.nombre),
datasets: [{
label: 'Cantidad',
data: sorted.map(i => i.cantidad),
backgroundColor: sorted.map(i => i.stockMin > 0 && i.cantidad <= i.stockMin ? '#C44536' : '#2F5D3A')
}]
},
options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false }
});
}
}
// ========================================
// INTELIGENCIA PREDICTIVA
// ========================================
function actualizarInteligencia() {
if (!cultivoActual) return;
// Encontrar el máximo de semanas entre proyecciones y cierres reales
const maxSemanaProyectada = proyecciones.length > 0 ? Math.max(...proyecciones.map(p => p.semana)) : 0;
const maxSemanaCierre = cultivoActual.cierres.length > 0 ? Math.max(...cultivoActual.cierres.map(c => c.semana)) : 0;
const semanas = Math.max(maxSemanaProyectada, maxSemanaCierre, cultivoActual.duracionSemanas || 30);
const labels = Array.from({ length: semanas }, (_, i) => `S${i + 1}`);
// Gastos proyectado vs real
if (charts.gastosProyReal) charts.gastosProyReal.destroy();
const ctx1 = document.getElementById('chartGastosProyReal');
if (ctx1) {
const proyectado = labels.map((_, i) => {
const p = proyecciones.find(x => x.semana === i + 1);
return p ? p.gastoEstimado : null;
});
const real = labels.map((_, i) => {
const c = cultivoActual.cierres?.find(x => x.semana === i + 1);
return c ? c.totalGastos + c.gastoExtra + c.valorConsumo : null;
});
charts.gastosProyReal = new Chart(ctx1, {
type: 'line',
data: {
labels: labels,
datasets: [
{ label: 'Proyectado', data: proyectado, borderColor: '#7A5C3E', borderDash: [5, 5], fill: false },
{ label: 'Real', data: real, borderColor: '#2F5D3A', fill: false }
]
},
options: { responsive: true, maintainAspectRatio: false, spanGaps: true }
});
}
// Producción proyectada vs real
if (charts.prodProyReal) charts.prodProyReal.destroy();
const ctx2 = document.getElementById('chartProdProyReal');
if (ctx2) {
const proyectado = labels.map((_, i) => {
const p = proyecciones.find(x => x.semana === i + 1);
return p ? p.produccionEstimada : null;
});
const real = labels.map((_, i) => {
const c = cultivoActual.cierres?.find(x => x.semana === i + 1);
return c ? c.cosechaTotal : null;
});
charts.prodProyReal = new Chart(ctx2, {
type: 'line',
data: {
labels: labels,
datasets: [
{ label: 'Proyectado', data: proyectado, borderColor: '#7A5C3E', borderDash: [5, 5], fill: false },
{ label: 'Real', data: real, borderColor: '#2F5D3A', fill: false }
]
},
options: { responsive: true, maintainAspectRatio: false, spanGaps: true }
});
}
}
// ========================================
// GESTIÓN DE DEUDAS
// ========================================
let deudas = [];
function actualizarSelectorDeudaCultivo() {
const sel = document.getElementById('deudaCultivo');
sel.innerHTML = 'Sin asignar (deuda general) ' +
'Todos los cultivos (prorratear) ' +
cultivos.map(c => `${esc(c.nombre)} `).join('');
}
function guardarDeuda() {
const nombre = document.getElementById('deudaNombre').value.trim();
const tipo = document.getElementById('deudaTipo').value;
const montoOriginal = parseFloat(document.getElementById('deudaMontoOriginal').value) || 0;
const saldoActual = parseFloat(document.getElementById('deudaSaldoActual').value) || 0;
const tasa = parseFloat(document.getElementById('deudaTasa').value) || 0;
const pagoMensual = parseFloat(document.getElementById('deudaPagoMensual').value) || 0;
const vencimiento = document.getElementById('deudaVencimiento').value;
const cultivoId = document.getElementById('deudaCultivo').value;
const notas = document.getElementById('deudaNotas').value.trim();
const editId = document.getElementById('deudaEditId').value;
if (!nombre || montoOriginal <= 0) {
alert('Ingresa al menos el nombre y monto original');
return;
}
const deudaData = {
id: editId || uid('deuda_'),
nombre,
tipo,
montoOriginal: toFixed2(montoOriginal),
saldoActual: toFixed2(saldoActual || montoOriginal),
tasa: toFixed2(tasa),
pagoMensual: toFixed2(pagoMensual),
vencimiento,
cultivoId,
notas,
fechaRegistro: new Date().toISOString().split('T')[0]
};
if (editId) {
const idx = deudas.findIndex(d => d.id === editId);
if (idx >= 0) deudas[idx] = deudaData;
document.getElementById('deudaEditId').value = '';
} else {
deudas.push(deudaData);
}
guardarDatos();
guardarDeudaSupabase(deudaData);
actualizarTablaDeudas();
actualizarResumenDeudas();
limpiarFormularioDeuda();
alert(editId ? 'Deuda actualizada' : 'Deuda registrada');
}
function limpiarFormularioDeuda() {
document.getElementById('deudaNombre').value = '';
document.getElementById('deudaTipo').value = 'banco';
document.getElementById('deudaMontoOriginal').value = '';
document.getElementById('deudaSaldoActual').value = '';
document.getElementById('deudaTasa').value = '';
document.getElementById('deudaPagoMensual').value = '';
document.getElementById('deudaVencimiento').value = '';
document.getElementById('deudaCultivo').value = '__NINGUNO__';
document.getElementById('deudaNotas').value = '';
document.getElementById('deudaEditId').value = '';
}
function editarDeuda(id) {
const deuda = deudas.find(d => d.id === id);
if (!deuda) return;
document.getElementById('deudaNombre').value = deuda.nombre;
document.getElementById('deudaTipo').value = deuda.tipo;
document.getElementById('deudaMontoOriginal').value = deuda.montoOriginal;
document.getElementById('deudaSaldoActual').value = deuda.saldoActual;
document.getElementById('deudaTasa').value = deuda.tasa;
document.getElementById('deudaPagoMensual').value = deuda.pagoMensual;
document.getElementById('deudaVencimiento').value = deuda.vencimiento;
document.getElementById('deudaCultivo').value = deuda.cultivoId;
document.getElementById('deudaNotas').value = deuda.notas || '';
document.getElementById('deudaEditId').value = id;
}
function eliminarDeuda(id) {
if (!confirm('¿Eliminar esta deuda?')) return;
deudas = deudas.filter(d => d.id !== id);
guardarDatos();
eliminarDeudaSupabase(id);
actualizarTablaDeudas();
actualizarResumenDeudas();
}
function actualizarTablaDeudas() {
const tbody = document.getElementById('tablaDeudas');
if (deudas.length === 0) {
tbody.innerHTML = 'Sin deudas registradas ';
return;
}
const tipoLabels = {
banco: '🏦 Banco',
caja: '🏪 Caja',
proveedor: '📦 Proveedor',
particular: '👤 Particular',
gobierno: '🏛️ Gobierno',
otro: '📋 Otro'
};
tbody.innerHTML = deudas.map(d => {
let cultivoNombre = 'Sin asignar';
if (d.cultivoId === '__TODOS__') cultivoNombre = 'Todos';
else if (d.cultivoId !== '__NINGUNO__') {
const cultivo = cultivos.find(c => c.id === d.cultivoId);
cultivoNombre = cultivo?.nombre || 'Sin asignar';
}
const diasVenc = d.vencimiento ? Math.ceil((new Date(d.vencimiento) - new Date()) / (1000 * 60 * 60 * 24)) : null;
const vencColor = diasVenc !== null && diasVenc < 30 ? 'var(--peligro)' : diasVenc !== null && diasVenc < 90 ? 'var(--advertencia)' : 'inherit';
return `
${d.nombre}
${tipoLabels[d.tipo] || d.tipo}
${formatMoney(d.saldoActual)}
${formatMoney(d.pagoMensual)}
${d.tasa}%
${d.vencimiento || '-'}
${cultivoNombre}
✏️
🗑️
`;
}).join('');
}
function actualizarResumenDeudas() {
const totalPasivos = deudas.reduce((sum, d) => sum + d.saldoActual, 0);
const pagoMensualTotal = deudas.reduce((sum, d) => sum + d.pagoMensual, 0);
document.getElementById('deudaTotalPasivos').textContent = formatMoney(totalPasivos);
document.getElementById('deudaPagoMensualTotal').textContent = formatMoney(pagoMensualTotal);
// Próximo vencimiento
const deudasConVenc = deudas.filter(d => d.vencimiento).sort((a, b) => new Date(a.vencimiento) - new Date(b.vencimiento));
if (deudasConVenc.length > 0) {
const proxima = deudasConVenc[0];
const dias = Math.ceil((new Date(proxima.vencimiento) - new Date()) / (1000 * 60 * 60 * 24));
document.getElementById('deudaProximoVenc').textContent = dias > 0 ? `${dias} días` : 'Vencida';
document.getElementById('deudaProximoVenc').style.color = dias < 30 ? 'var(--peligro)' : 'inherit';
} else {
document.getElementById('deudaProximoVenc').textContent = '-';
}
// DSCR Global (si hay utilidad global)
let utilidadTotal = 0;
cultivos.forEach(c => {
const temp = cultivoActual;
cultivoActual = c;
const t = calcularTotales();
cultivoActual = temp;
utilidadTotal += t.utilidadNeta;
});
const utilidadMensual = utilidadTotal / Math.max(1, cultivos.reduce((sum, c) => sum + c.cierres.length, 0) / 4);
const dscrGlobal = pagoMensualTotal > 0 ? utilidadMensual / pagoMensualTotal : null;
document.getElementById('deudaDSCRGlobal').textContent = dscrGlobal !== null ? formatNumber(dscrGlobal) : 'N/A';
if (dscrGlobal !== null) {
document.getElementById('deudaDSCRGlobal').style.color = dscrGlobal < 1 ? 'var(--peligro)' : dscrGlobal < 1.2 ? 'var(--advertencia)' : 'var(--exito)';
}
}
function getDeudasPorCultivo(cultivoId) {
return deudas.filter(d => d.cultivoId === cultivoId || d.cultivoId === '__TODOS__');
}
function getPagoMensualDeudas(cultivoId) {
const deudasCultivo = getDeudasPorCultivo(cultivoId);
let total = 0;
deudasCultivo.forEach(d => {
if (d.cultivoId === '__TODOS__' && cultivos.length > 0) {
// Prorratear entre todos los cultivos
total += d.pagoMensual / cultivos.length;
} else {
total += d.pagoMensual;
}
});
return total;
}
// Funciones de prorrateo de crédito
function toggleProrrateoCultivos() {
const aplicarA = document.getElementById('creditoAplicarA').value;
const seleccionDiv = document.getElementById('creditoCultivosSeleccion');
if (aplicarA === 'seleccion') {
seleccionDiv.style.display = 'block';
const lista = document.getElementById('creditoCultivosLista');
lista.innerHTML = cultivos.map(c => `
${c.nombre} (${c.area} ha)
`).join('');
} else {
seleccionDiv.style.display = 'none';
}
}
// ========================================
// SIMULADOR DE CRÉDITO
// ========================================
function calcularCredito() {
const monto = parseFloat(document.getElementById('creditoMonto').value) || 0;
const tasaAnual = parseFloat(document.getElementById('creditoTasa').value) || 0;
const plazo = parseInt(document.getElementById('creditoPlazo').value) || 0;
const fechaInicio = document.getElementById('creditoFecha').value;
if (monto <= 0 || tasaAnual <= 0 || plazo <= 0) {
alert('Ingresa todos los datos del crédito');
return;
}
const tasaMensual = (tasaAnual / 100) / 12;
const cuota = monto * (tasaMensual * Math.pow(1 + tasaMensual, plazo)) / (Math.pow(1 + tasaMensual, plazo) - 1);
const amortizacion = [];
let saldo = monto;
let fechaActual = fechaInicio ? new Date(fechaInicio) : new Date();
for (let i = 1; i <= plazo; i++) {
const interes = toFixed2(saldo * tasaMensual);
const capital = toFixed2(cuota - interes);
saldo = toFixed2(Math.max(0, saldo - capital));
amortizacion.push({
mes: i,
fecha: fechaActual.toISOString().split('T')[0],
cuota: toFixed2(cuota),
capital,
interes,
saldo
});
fechaActual.setMonth(fechaActual.getMonth() + 1);
}
document.getElementById('tablaAmortizacion').innerHTML = amortizacion.map(a => `
${a.mes}
${a.fecha}
${formatMoney(a.cuota)}
${formatMoney(a.capital)}
${formatMoney(a.interes)}
${formatMoney(a.saldo)}
`).join('');
document.getElementById('creditoCuotaCalc').textContent = formatMoney(cuota);
document.getElementById('creditoTotalPagar').textContent = formatMoney(cuota * plazo);
document.getElementById('creditoTotalInteres').textContent = formatMoney((cuota * plazo) - monto);
window.creditoTemp = { monto, tasaAnual, plazo, cuota: toFixed2(cuota), amortizacion };
}
function borrarDatosCredito() {
document.getElementById('creditoMonto').value = '';
document.getElementById('creditoTasa').value = '';
document.getElementById('creditoPlazo').value = '';
document.getElementById('creditoFecha').value = '';
document.getElementById('tablaAmortizacion').innerHTML = 'Ingresa datos ';
document.getElementById('creditoCuotaCalc').textContent = '$0.00';
document.getElementById('creditoTotalPagar').textContent = '$0.00';
document.getElementById('creditoTotalInteres').textContent = '$0.00';
window.creditoTemp = null;
}
function vincularCreditoCultivo() {
if (!cultivoActual) { alert('Selecciona un cultivo'); return; }
if (!window.creditoTemp) { alert('Calcula el crédito primero'); return; }
cultivoActual.credito = window.creditoTemp;
guardarDatos();
guardarCreditoSupabase(window.creditoTemp, cultivoActual.id);
actualizarDashboard();
alert(`Crédito vinculado a ${cultivoActual.nombre}`);
}
function generarPDFAmortizacion() {
if (!window.creditoTemp) { alert('Calcula el crédito primero'); return; }
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
doc.setFontSize(18);
doc.text('Tabla de Amortización - ZLYX', 14, 22);
doc.setFontSize(11);
doc.text(`Monto: ${formatMoney(window.creditoTemp.monto)}`, 14, 35);
doc.text(`Tasa Anual: ${window.creditoTemp.tasaAnual}%`, 14, 42);
doc.text(`Plazo: ${window.creditoTemp.plazo} meses`, 14, 49);
doc.text(`Cuota Mensual: ${formatMoney(window.creditoTemp.cuota)}`, 14, 56);
doc.autoTable({
startY: 65,
head: [['Mes', 'Fecha', 'Cuota', 'Capital', 'Interés', 'Saldo']],
body: window.creditoTemp.amortizacion.map(a => [a.mes, a.fecha, formatMoney(a.cuota), formatMoney(a.capital), formatMoney(a.interes), formatMoney(a.saldo)]),
theme: 'grid',
headStyles: { fillColor: [47, 93, 58] }
});
doc.save('Amortizacion_ZLYX.pdf');
}
// ========================================
// PLANEACIÓN DEL CICLO
// ========================================
let distribucionSemanal = [];
function guardarConfigPlaneacion() {
if (!cultivoActual) { alert('Selecciona un cultivo'); return; }
cultivoActual.planeacion = {
semanasTotales: parseInt(document.getElementById('planSemanasTotales').value) || 0,
semanasPreCosecha: parseInt(document.getElementById('planSemanasPreCosecha').value) || 0,
produccionMeta: parseFloat(document.getElementById('planProduccionMeta').value) || 0,
presupuestoTotal: parseFloat(document.getElementById('planPresupuestoTotal').value) || 0,
porcPreparacion: parseInt(document.getElementById('planPorcPreparacion').value) || 40,
porcCrecimiento: parseInt(document.getElementById('planPorcCrecimiento').value) || 30,
porcCosecha: parseInt(document.getElementById('planPorcCosecha').value) || 30,
distribucion: distribucionSemanal
};
guardarDatos();
guardarPlaneacionSupabase(cultivoActual.planeacion, cultivoActual.id, distribucionSemanal);
actualizarResumenPlaneacion();
alert('Configuración de planeación guardada');
}
function cargarConfigPlaneacion() {
if (!cultivoActual || !cultivoActual.planeacion) {
// Sin datos guardados — dejar en blanco para que el usuario ingrese los suyos
document.getElementById('planSemanasTotales').value = '';
document.getElementById('planSemanasPreCosecha').value = '';
document.getElementById('planProduccionMeta').value = '';
document.getElementById('planPresupuestoTotal').value = '';
document.getElementById('planPorcPreparacion').value = 40;
document.getElementById('planPorcCrecimiento').value = 30;
document.getElementById('planPorcCosecha').value = 30;
distribucionSemanal = [];
actualizarPorcFases('preparacion');
actualizarPorcFases('crecimiento');
actualizarPorcFases('cosecha');
return;
}
const p = cultivoActual.planeacion;
document.getElementById('planSemanasTotales').value = p.semanasTotales || 45;
document.getElementById('planSemanasPreCosecha').value = p.semanasPreCosecha || 10;
document.getElementById('planProduccionMeta').value = p.produccionMeta || '';
document.getElementById('planPresupuestoTotal').value = p.presupuestoTotal || '';
document.getElementById('planPorcPreparacion').value = p.porcPreparacion || 40;
document.getElementById('planPorcCrecimiento').value = p.porcCrecimiento || 30;
document.getElementById('planPorcCosecha').value = p.porcCosecha || 30;
distribucionSemanal = p.distribucion || [];
actualizarPorcFases('preparacion');
actualizarPorcFases('crecimiento');
actualizarPorcFases('cosecha');
actualizarResumenPlaneacion();
actualizarSlidersSemanas();
}
function actualizarPorcFases(fase) {
const val = document.getElementById(`planPorc${fase.charAt(0).toUpperCase() + fase.slice(1)}`).value;
document.getElementById(`planPorc${fase.charAt(0).toUpperCase() + fase.slice(1)}Val`).textContent = val + '%';
// Verificar si suman 100
const prep = parseInt(document.getElementById('planPorcPreparacion').value) || 0;
const crec = parseInt(document.getElementById('planPorcCrecimiento').value) || 0;
const cos = parseInt(document.getElementById('planPorcCosecha').value) || 0;
const total = prep + crec + cos;
const alerta = document.getElementById('planFasesAlerta');
if (total !== 100) {
alerta.style.display = 'block';
alerta.textContent = `⚠️ Los porcentajes suman ${total}%. Deben sumar 100%.`;
} else {
alerta.style.display = 'none';
}
calcularDistribucionGastos();
}
function actualizarResumenPlaneacion() {
const produccionMeta = parseFloat(document.getElementById('planProduccionMeta').value) || 0;
const presupuesto = parseFloat(document.getElementById('planPresupuestoTotal').value) || 0;
// Calcular ingreso estimado basado en distribución
let ingresoTotal = 0;
distribucionSemanal.forEach(s => {
ingresoTotal += (s.produccion || 0) * (s.precio || 0);
});
// Si no hay distribución, estimar con precio promedio de presentaciones
if (ingresoTotal === 0 && produccionMeta > 0) {
const precioPromedio = presentaciones.length > 0
? presentaciones.reduce((sum, p) => sum + (p.precio / p.peso), 0) / presentaciones.length
: 18; // Default $18/kg
ingresoTotal = produccionMeta * 1000 * precioPromedio;
}
const utilidad = ingresoTotal - presupuesto;
const roi = presupuesto > 0 ? (utilidad / presupuesto) * 100 : 0;
document.getElementById('planInversionEst').textContent = formatMoney(presupuesto);
document.getElementById('planProduccionEst').textContent = formatNumber(produccionMeta) + ' ton';
document.getElementById('planIngresoEst').textContent = formatMoney(ingresoTotal);
document.getElementById('planUtilidadEst').textContent = formatMoney(utilidad);
document.getElementById('planUtilidadEst').style.color = utilidad >= 0 ? 'var(--exito)' : 'var(--peligro)';
document.getElementById('planROIEst').textContent = formatPercent(roi);
document.getElementById('planROIEst').style.color = roi >= 0 ? 'var(--exito)' : 'var(--peligro)';
}
function actualizarSlidersSemanas() {
const semanasTotales = parseInt(document.getElementById('planSemanasTotales').value) || 45;
const semanasPreCosecha = parseInt(document.getElementById('planSemanasPreCosecha').value) || 10;
const produccionMeta = parseFloat(document.getElementById('planProduccionMeta').value) || 0;
const presupuesto = parseFloat(document.getElementById('planPresupuestoTotal').value) || 0;
// Inicializar distribución si no existe
if (distribucionSemanal.length !== semanasTotales) {
distribucionSemanal = [];
for (let i = 1; i <= semanasTotales; i++) {
distribucionSemanal.push({
semana: i,
produccion: 0,
precio: 18, // Default
gasto: 0
});
}
}
// Calcular gastos por fase
calcularDistribucionGastos();
// Renderizar tabla de sliders
renderizarTablaDistribucion();
}
function calcularDistribucionGastos() {
const semanasTotales = parseInt(document.getElementById('planSemanasTotales').value) || 45;
const semanasPreCosecha = parseInt(document.getElementById('planSemanasPreCosecha').value) || 10;
const presupuesto = parseFloat(document.getElementById('planPresupuestoTotal').value) || 0;
const porcPrep = parseInt(document.getElementById('planPorcPreparacion').value) || 40;
const porcCrec = parseInt(document.getElementById('planPorcCrecimiento').value) || 30;
const porcCos = parseInt(document.getElementById('planPorcCosecha').value) || 30;
// Definir fases
const semanasCosecha = semanasTotales - semanasPreCosecha;
const semanasPrep = Math.ceil(semanasPreCosecha * 0.4); // 40% de pre-cosecha es preparación
const semanasCrec = semanasPreCosecha - semanasPrep;
// Calcular gasto por semana en cada fase
const gastoPrep = presupuesto * (porcPrep / 100);
const gastoCrec = presupuesto * (porcCrec / 100);
const gastoCos = presupuesto * (porcCos / 100);
const gastoPorSemPrep = semanasPrep > 0 ? gastoPrep / semanasPrep : 0;
const gastoPorSemCrec = semanasCrec > 0 ? gastoCrec / semanasCrec : 0;
const gastoPorSemCos = semanasCosecha > 0 ? gastoCos / semanasCosecha : 0;
// Actualizar distribución
distribucionSemanal.forEach((s, idx) => {
const semana = idx + 1;
if (semana <= semanasPrep) {
s.fase = 'preparacion';
s.gasto = toFixed2(gastoPorSemPrep);
} else if (semana <= semanasPreCosecha) {
s.fase = 'crecimiento';
s.gasto = toFixed2(gastoPorSemCrec);
} else {
s.fase = 'cosecha';
s.gasto = toFixed2(gastoPorSemCos);
}
});
renderizarTablaDistribucion();
actualizarGraficaPlaneacion();
}
function calcularDistribucionAuto() {
const produccionMeta = parseFloat(document.getElementById('planProduccionMeta').value) || 0;
if (produccionMeta > 0) {
generarDistribucionCampana();
}
actualizarResumenPlaneacion();
}
function generarDistribucionCampana() {
const semanasTotales = parseInt(document.getElementById('planSemanasTotales').value) || 45;
const semanasPreCosecha = parseInt(document.getElementById('planSemanasPreCosecha').value) || 10;
const produccionMeta = (parseFloat(document.getElementById('planProduccionMeta').value) || 0) * 1000; // A kg
const semanasCosecha = semanasTotales - semanasPreCosecha;
if (semanasCosecha <= 0 || produccionMeta <= 0) return;
// Generar curva de campana (distribución normal)
const centro = semanasCosecha / 2;
const sigma = semanasCosecha / 4;
let pesos = [];
let sumaPesos = 0;
for (let i = 0; i < semanasCosecha; i++) {
const x = i - centro;
const peso = Math.exp(-(x * x) / (2 * sigma * sigma));
pesos.push(peso);
sumaPesos += peso;
}
// Normalizar y asignar producción
let semCosechaIdx = 0;
distribucionSemanal.forEach((s, idx) => {
if (s.fase === 'cosecha') {
s.produccion = toFixed2((pesos[semCosechaIdx] / sumaPesos) * produccionMeta);
semCosechaIdx++;
} else {
s.produccion = 0;
}
});
if (cultivoActual) {
cultivoActual.planeacion = cultivoActual.planeacion || {};
cultivoActual.planeacion.distribucion = distribucionSemanal;
guardarDatos();
}
renderizarTablaDistribucion();
actualizarGraficaPlaneacion();
actualizarResumenPlaneacion();
}
function generarDistribucionUniforme() {
const semanasTotales = parseInt(document.getElementById('planSemanasTotales').value) || 45;
const semanasPreCosecha = parseInt(document.getElementById('planSemanasPreCosecha').value) || 10;
const produccionMeta = (parseFloat(document.getElementById('planProduccionMeta').value) || 0) * 1000;
const semanasCosecha = semanasTotales - semanasPreCosecha;
if (semanasCosecha <= 0) return;
const produccionPorSemana = produccionMeta / semanasCosecha;
distribucionSemanal.forEach(s => {
if (s.fase === 'cosecha') {
s.produccion = toFixed2(produccionPorSemana);
} else {
s.produccion = 0;
}
});
if (cultivoActual) {
cultivoActual.planeacion = cultivoActual.planeacion || {};
cultivoActual.planeacion.distribucion = distribucionSemanal;
guardarDatos();
}
renderizarTablaDistribucion();
actualizarGraficaPlaneacion();
actualizarResumenPlaneacion();
}
function limpiarDistribucion() {
distribucionSemanal.forEach(s => {
s.produccion = 0;
s.precio = 18;
});
renderizarTablaDistribucion();
actualizarGraficaPlaneacion();
actualizarResumenPlaneacion();
}
function renderizarTablaDistribucion() {
const tbody = document.getElementById('tablaDistribucionSemanal');
const semanasPreCosecha = parseInt(document.getElementById('planSemanasPreCosecha').value) || 10;
if (distribucionSemanal.length === 0) {
tbody.innerHTML = 'Configura el ciclo primero ';
return;
}
const faseLabels = {
preparacion: { nombre: '🔧 Prep.', color: '#F57C00' },
crecimiento: { nombre: '🌱 Crec.', color: '#2E7D32' },
cosecha: { nombre: '🌾 Cosecha', color: '#1565C0' }
};
tbody.innerHTML = distribucionSemanal.map((s, idx) => {
const faseInfo = faseLabels[s.fase] || { nombre: '-', color: '#999' };
const ingreso = s.produccion * s.precio;
const esCosecha = s.fase === 'cosecha';
return `
S${s.semana}
${faseInfo.nombre}
${esCosecha ? `
${formatNumber(s.produccion)} kg
` : '- '}
${esCosecha ? `
` : '- '}
${esCosecha ? formatMoney(ingreso) : '-'}
${formatMoney(s.gasto)}
`;
}).join('');
actualizarTotalesDistribucion();
}
function actualizarProduccionSemana(idx, valor) {
distribucionSemanal[idx].produccion = parseFloat(valor) || 0;
// Actualizar display del slider
renderizarTablaDistribucion();
actualizarGraficaPlaneacion();
actualizarResumenPlaneacion();
// Auto-guardar
if (cultivoActual) {
cultivoActual.planeacion = cultivoActual.planeacion || {};
cultivoActual.planeacion.distribucion = distribucionSemanal;
guardarDatos();
// Supabase: upsert solo esta semana
const semData = distribucionSemanal[idx];
if (semData) {
sb.from('cultivo_planeacion_semanas').upsert({
cultivo_id: cultivoActual.id,
semana: semData.semana, fase: semData.fase || null,
produccion: semData.produccion, precio: semData.precio, gasto: semData.gasto
}, { onConflict: 'cultivo_id,semana' });
}
}
}
function actualizarPrecioSemana(idx, valor) {
distribucionSemanal[idx].precio = parseFloat(valor) || 0;
renderizarTablaDistribucion();
actualizarResumenPlaneacion();
if (cultivoActual) {
cultivoActual.planeacion = cultivoActual.planeacion || {};
cultivoActual.planeacion.distribucion = distribucionSemanal;
guardarDatos();
// Supabase: upsert solo esta semana
const semData = distribucionSemanal[idx];
if (semData) {
sb.from('cultivo_planeacion_semanas').upsert({
cultivo_id: cultivoActual.id,
semana: semData.semana, fase: semData.fase || null,
produccion: semData.produccion, precio: semData.precio, gasto: semData.gasto
}, { onConflict: 'cultivo_id,semana' });
}
}
}
function actualizarTotalesDistribucion() {
const totalProd = distribucionSemanal.reduce((sum, s) => sum + (s.produccion || 0), 0);
const totalIngreso = distribucionSemanal.reduce((sum, s) => sum + (s.produccion || 0) * (s.precio || 0), 0);
const totalGasto = distribucionSemanal.reduce((sum, s) => sum + (s.gasto || 0), 0);
const semanasConPrecio = distribucionSemanal.filter(s => s.precio > 0 && s.produccion > 0);
const precioPromedio = semanasConPrecio.length > 0
? semanasConPrecio.reduce((sum, s) => sum + s.precio, 0) / semanasConPrecio.length
: 0;
document.getElementById('planTotalProduccion').textContent = formatNumber(totalProd) + ' kg';
document.getElementById('planPrecioPromedio').textContent = formatMoney(precioPromedio) + '/kg';
document.getElementById('planTotalIngresos').textContent = formatMoney(totalIngreso);
document.getElementById('planTotalGastos').textContent = formatMoney(totalGasto);
}
function actualizarGraficaPlaneacion() {
if (charts.planeacion) charts.planeacion.destroy();
const ctx = document.getElementById('chartPlaneacion');
if (!ctx || distribucionSemanal.length === 0) return;
const labels = distribucionSemanal.map(s => `S${s.semana}`);
const dataProd = distribucionSemanal.map(s => s.produccion || 0);
const dataGasto = distribucionSemanal.map(s => s.gasto || 0);
const dataIngreso = distribucionSemanal.map(s => (s.produccion || 0) * (s.precio || 0));
charts.planeacion = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'Producción (kg)',
data: dataProd,
borderColor: '#2E7D32',
backgroundColor: 'rgba(46, 125, 50, 0.1)',
fill: true,
yAxisID: 'y',
tension: 0.3
},
{
label: 'Gastos ($)',
data: dataGasto,
borderColor: '#F57C00',
backgroundColor: 'rgba(245, 124, 0, 0.1)',
fill: true,
yAxisID: 'y1',
tension: 0.3
},
{
label: 'Ingresos ($)',
data: dataIngreso,
borderColor: '#1565C0',
backgroundColor: 'rgba(21, 101, 192, 0.1)',
fill: true,
yAxisID: 'y1',
tension: 0.3
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
scales: {
y: {
type: 'linear',
position: 'left',
title: { display: true, text: 'Producción (kg)' }
},
y1: {
type: 'linear',
position: 'right',
title: { display: true, text: 'Dinero ($)' },
grid: { drawOnChartArea: false }
}
}
}
});
}
// Comparativa Real vs Proyectado
function actualizarComparativa() {
if (!cultivoActual) return;
const semanasTotales = cultivoActual.planeacion?.semanasTotales || 45;
const semanasReales = cultivoActual.cierres.length;
// KPIs de avance
document.getElementById('compAvanceSemanas').textContent = `${semanasReales}/${semanasTotales}`;
// Calcular totales proyectados y reales
const prodProy = distribucionSemanal.reduce((sum, s) => sum + (s.produccion || 0), 0);
const gastoProy = distribucionSemanal.reduce((sum, s) => sum + (s.gasto || 0), 0);
const ingresoProy = distribucionSemanal.reduce((sum, s) => sum + (s.produccion || 0) * (s.precio || 0), 0);
let prodReal = 0, gastoReal = 0, ingresoReal = 0;
cultivoActual.cierres.forEach(c => {
prodReal += c.cosechado || 0;
gastoReal += (c.totalGastos || 0) + (c.gastoExtra || 0) + (c.valorConsumo || 0);
});
cultivoActual.ventas?.forEach(v => {
ingresoReal += v.neta || 0;
});
const avanceProd = prodProy > 0 ? (prodReal / prodProy) * 100 : 0;
const avanceGasto = gastoProy > 0 ? (gastoReal / gastoProy) * 100 : 0;
const avanceIngreso = ingresoProy > 0 ? (ingresoReal / ingresoProy) * 100 : 0;
document.getElementById('compAvanceProduccion').textContent = formatPercent(avanceProd);
document.getElementById('compAvanceGastos').textContent = formatPercent(avanceGasto);
document.getElementById('compAvanceIngresos').textContent = formatPercent(avanceIngreso);
// Gráficas de comparativa
actualizarGraficasComparativa();
actualizarTablaComparativa();
actualizarAlertasDesviacion();
}
function actualizarGraficasComparativa() {
if (!cultivoActual) return;
const labels = distribucionSemanal.map(s => `S${s.semana}`);
// Datos proyectados
const prodProy = distribucionSemanal.map(s => s.produccion || 0);
const gastoProy = distribucionSemanal.map(s => s.gasto || 0);
const ingresoProy = distribucionSemanal.map(s => (s.produccion || 0) * (s.precio || 0));
// Datos reales (acumulados hasta las semanas registradas)
const prodReal = new Array(distribucionSemanal.length).fill(null);
const gastoReal = new Array(distribucionSemanal.length).fill(null);
const ingresoReal = new Array(distribucionSemanal.length).fill(null);
cultivoActual.cierres.forEach((c, idx) => {
if (idx < distribucionSemanal.length) {
prodReal[idx] = c.cosechado || 0;
gastoReal[idx] = (c.totalGastos || 0) + (c.gastoExtra || 0) + (c.valorConsumo || 0);
}
});
// Distribuir ingresos por semana (simplificado)
const ingresoTotalReal = cultivoActual.ventas?.reduce((sum, v) => sum + (v.neta || 0), 0) || 0;
const semanasConIngreso = cultivoActual.cierres.filter(c => (c.cosechado || 0) > 0).length;
if (semanasConIngreso > 0) {
const ingresoPorSem = ingresoTotalReal / semanasConIngreso;
cultivoActual.cierres.forEach((c, idx) => {
if (idx < distribucionSemanal.length && (c.cosechado || 0) > 0) {
ingresoReal[idx] = ingresoPorSem;
}
});
}
// Gráfica Producción
if (charts.compProd) charts.compProd.destroy();
const ctxProd = document.getElementById('chartCompProduccion');
if (ctxProd) {
charts.compProd = new Chart(ctxProd, {
type: 'line',
data: {
labels: labels,
datasets: [
{ label: 'Proyectado', data: prodProy, borderColor: '#2E7D32', borderDash: [5, 5], fill: false },
{ label: 'Real', data: prodReal, borderColor: '#1565C0', fill: false, spanGaps: false }
]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
// Gráfica Gastos
if (charts.compGasto) charts.compGasto.destroy();
const ctxGasto = document.getElementById('chartCompGastos');
if (ctxGasto) {
charts.compGasto = new Chart(ctxGasto, {
type: 'line',
data: {
labels: labels,
datasets: [
{ label: 'Proyectado', data: gastoProy, borderColor: '#F57C00', borderDash: [5, 5], fill: false },
{ label: 'Real', data: gastoReal, borderColor: '#E53935', fill: false, spanGaps: false }
]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
// Gráfica Ingresos
if (charts.compIngreso) charts.compIngreso.destroy();
const ctxIngreso = document.getElementById('chartCompIngresos');
if (ctxIngreso) {
charts.compIngreso = new Chart(ctxIngreso, {
type: 'line',
data: {
labels: labels,
datasets: [
{ label: 'Proyectado', data: ingresoProy, borderColor: '#7B1FA2', borderDash: [5, 5], fill: false },
{ label: 'Real', data: ingresoReal, borderColor: '#00897B', fill: false, spanGaps: false }
]
},
options: { responsive: true, maintainAspectRatio: false }
});
}
}
function actualizarTablaComparativa() {
if (!cultivoActual) return;
const tbody = document.getElementById('tablaComparativa');
const filas = distribucionSemanal.map((s, idx) => {
const cierre = cultivoActual.cierres[idx];
const prodProy = s.produccion || 0;
const prodReal = cierre ? (cierre.cosechaTotal || 0) : null;
const gastoProy = s.gasto || 0;
const gastoReal = cierre ? ((cierre.totalGastos || 0) + (cierre.gastoExtra || 0) + (cierre.valorConsumo || 0)) : null;
const desvProd = prodReal !== null && prodProy > 0 ? ((prodReal - prodProy) / prodProy) * 100 : null;
const desvGasto = gastoReal !== null && gastoProy > 0 ? ((gastoReal - gastoProy) / gastoProy) * 100 : null;
return `
S${s.semana}
${formatNumber(prodProy)} kg
${prodReal !== null ? formatNumber(prodReal) + ' kg' : '-'}
${desvProd !== null ? (desvProd >= 0 ? '+' : '') + formatNumber(desvProd) + '%' : '-'}
${formatMoney(gastoProy)}
${gastoReal !== null ? formatMoney(gastoReal) : '-'}
${desvGasto !== null ? (desvGasto >= 0 ? '+' : '') + formatNumber(desvGasto) + '%' : '-'}
`;
}).join('');
tbody.innerHTML = filas || 'Sin datos ';
}
function actualizarAlertasDesviacion() {
if (!cultivoActual) return;
const alertas = [];
const umbral = 20; // 20% de desviación
cultivoActual.cierres.forEach((cierre, idx) => {
if (idx >= distribucionSemanal.length) return;
const s = distribucionSemanal[idx];
const prodProy = s.produccion || 0;
const prodReal = cierre.cosechaTotal || 0;
const gastoProy = s.gasto || 0;
const gastoReal = (cierre.totalGastos || 0) + (cierre.gastoExtra || 0) + (cierre.valorConsumo || 0);
if (prodProy > 0) {
const desvProd = ((prodReal - prodProy) / prodProy) * 100;
if (Math.abs(desvProd) > umbral) {
alertas.push({
tipo: desvProd < 0 ? 'peligro' : 'exito',
semana: s.semana,
mensaje: `Producción S${s.semana}: ${desvProd > 0 ? '+' : ''}${formatNumber(desvProd)}% vs proyectado`
});
}
}
if (gastoProy > 0) {
const desvGasto = ((gastoReal - gastoProy) / gastoProy) * 100;
if (desvGasto > umbral) {
alertas.push({
tipo: 'advertencia',
semana: s.semana,
mensaje: `Gasto S${s.semana}: +${formatNumber(desvGasto)}% sobre lo proyectado`
});
}
}
});
const cardAlertas = document.getElementById('cardAlertasDesviacion');
const divAlertas = document.getElementById('alertasDesviacion');
if (alertas.length > 0) {
cardAlertas.style.display = 'block';
divAlertas.innerHTML = alertas.map(a => `
${a.mensaje}
`).join('');
} else {
cardAlertas.style.display = 'none';
}
}
// ========================================
// REPORTES PDF
// ========================================
function generarReportePDF(tipo) {
if (!cultivoActual && tipo !== 'global') { alert('Selecciona un cultivo'); return; }
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const t = tipo === 'global' ? null : calcularTotales();
doc.setFontSize(18);
doc.setTextColor(47, 93, 58);
doc.text(`ZLYX - Reporte ${tipo.charAt(0).toUpperCase() + tipo.slice(1)}`, 14, 22);
doc.setFontSize(10);
doc.setTextColor(0, 0, 0);
doc.text(`Generado: ${new Date().toLocaleDateString('es-MX')}`, 14, 30);
if (tipo === 'ejecutivo' && cultivoActual) {
doc.setFontSize(14);
doc.text(`Cultivo: ${cultivoActual.nombre}`, 14, 42);
doc.text(`Área: ${formatNumber(cultivoActual.area)} ha`, 14, 50);
const resumen = [
['ROI', formatPercent(t.roi)],
['Inversión Total', formatMoney(t.inversionTotal)],
['Ventas Netas', formatMoney(t.ventasNetas)],
['Utilidad Neta', formatMoney(t.utilidadNeta)],
['Merma', formatPercent(t.porcMerma)]
];
if (t.dscr !== null) resumen.push(['DSCR', formatNumber(t.dscr)]);
doc.autoTable({
startY: 58,
head: [['Indicador', 'Valor']],
body: resumen,
theme: 'striped',
headStyles: { fillColor: [47, 93, 58] }
});
}
if (tipo === 'ventas' && cultivoActual && cultivoActual.ventas.length > 0) {
doc.text(`Cultivo: ${cultivoActual.nombre}`, 14, 42);
doc.autoTable({
startY: 50,
head: [['Fecha', 'Cliente', 'Cantidad', 'Bruta', 'Desc.', 'Neta']],
body: cultivoActual.ventas.map(v => [v.fecha, v.cliente, `${formatNumber(v.cantidad)} kg`, formatMoney(v.bruta), formatMoney(obtenerTotalDescuentos(v)), formatMoney(v.neta)]),
theme: 'grid',
headStyles: { fillColor: [47, 93, 58] }
});
}
if (tipo === 'inventario' && cultivoActual && cultivoActual.almacen && cultivoActual.almacen.length > 0) {
doc.text(`Inventario del Almacén - ${cultivoActual.nombre}`, 14, 42);
doc.autoTable({
startY: 50,
head: [['Insumo', 'Cat.', 'Cant.', 'P. Unit.', 'Valor', 'Estado']],
body: cultivoActual.almacen.map(i => [i.nombre, i.categoria, `${formatNumber(i.cantidad)} ${i.unidad}`, formatMoney(i.precio), formatMoney(i.cantidad * i.precio), i.cantidad <= i.stockMin ? '⚠️ Bajo' : '✓ OK']),
theme: 'grid',
headStyles: { fillColor: [47, 93, 58] }
});
}
doc.save(`ZLYX_${tipo}_${new Date().toISOString().split('T')[0]}.pdf`);
}
// ========================================
// CONTEXTO IA
// ========================================
function generarContextoIA(conPregunta) {
if (!cultivoActual) { alert('Selecciona un cultivo'); return; }
const t = calcularTotales();
let texto = `# CONTEXTO ZLYX - ${cultivoActual.nombre.toUpperCase()}
Fecha: ${new Date().toLocaleDateString('es-MX')}
## DATOS DEL CULTIVO
- Tipo: ${cultivoActual.tipo}
- Área: ${formatNumber(cultivoActual.area)} hectáreas
- Moneda: ${cultivoActual.moneda}
- Semanas registradas: ${cultivoActual.cierres.length}
## INDICADORES FINANCIEROS
- ROI: ${formatPercent(t.roi)}
- Inversión Total: ${formatMoney(t.inversionTotal)}
- Ventas Netas: ${formatMoney(t.ventasNetas)}
- Utilidad Neta: ${formatMoney(t.utilidadNeta)}
- Merma: ${formatPercent(t.porcMerma)}`;
if (t.dscr !== null) {
texto += `\n- DSCR: ${formatNumber(t.dscr)} (${t.dscr >= 1.2 ? 'Saludable' : t.dscr >= 1.0 ? 'Cuidado' : 'Riesgo'})`;
}
if (conPregunta) {
const pregunta = prompt('¿Qué pregunta quieres hacerle a la IA?');
if (pregunta) texto += `\n\n## PREGUNTA\n${pregunta}`;
} else {
texto += '\n\n## SOLICITUD\nAnaliza los datos y proporciona: (1) Evaluación general, (2) Riesgos, (3) 3 acciones concretas, (4) Recomendación sobre el ciclo.';
}
document.getElementById('contextoIATexto').value = texto;
mostrarModal('modalContextoIA');
}
function copiarContextoIA() {
const textarea = document.getElementById('contextoIATexto');
const texto = textarea.value;
const btn = document.querySelector('#modalContextoIA .btn:last-child');
if (navigator.clipboard) {
navigator.clipboard.writeText(texto).then(() => {
if (btn) { btn.textContent = '✅ Copiado!'; setTimeout(() => btn.textContent = '📋 Copiar al Portapapeles', 2000); }
});
} else {
textarea.select();
document.execCommand('copy');
if (btn) { btn.textContent = '✅ Copiado!'; setTimeout(() => btn.textContent = '📋 Copiar al Portapapeles', 2000); }
}
}
// ========================================
// ALERTA MARGEN NEGATIVO
// ========================================
function calcularIngresoMarginalEstimado(semana) {
if (!cultivoActual || cultivoActual.ventas.length === 0) return 0;
const totalKg = cultivoActual.ventas.reduce((s, v) => s + v.cantidad, 0);
const totalVenta = cultivoActual.ventas.reduce((s, v) => s + v.neta, 0);
const precioPorKg = totalKg > 0 ? totalVenta / totalKg : 0;
const prodPromedio = cultivoActual.cierres.length > 0
? cultivoActual.cierres.reduce((s, c) => s + c.cosechaTotal, 0) / cultivoActual.cierres.length
: 0;
return toFixed2(precioPorKg * prodPromedio);
}
function mostrarAlertaMargenNegativo(costo, ingreso) {
document.getElementById('alertaCostoMarginal').textContent = formatMoney(costo);
document.getElementById('alertaIngresoMarginal').textContent = formatMoney(ingreso);
document.getElementById('alertaDiferencia').textContent = formatMoney(ingreso - costo);
mostrarModal('modalAlertaMargen');
}
// ========================================
// PRESENTACIONES
// ========================================
function guardarPresentacion() {
const editId = document.getElementById('presentacionEditId').value;
const nombre = document.getElementById('presentacionNombre').value.trim();
const peso = parseFloat(document.getElementById('presentacionPeso').value) || 0;
const precio = parseFloat(document.getElementById('presentacionPrecio').value) || 0;
const moneda = document.getElementById('presentacionMoneda').value;
const descripcion = document.getElementById('presentacionDescripcion').value.trim();
if (!nombre || peso <= 0 || precio <= 0) {
alert('Completa nombre, peso y precio');
return;
}
if (editId) {
// Editar existente
const pres = presentaciones.find(p => p.id === editId);
if (pres) {
pres.nombre = nombre;
pres.peso = toFixed2(peso);
pres.precio = toFixed2(precio);
pres.moneda = moneda;
pres.descripcion = descripcion;
}
document.getElementById('presentacionEditId').value = '';
} else {
// Nueva presentación
presentaciones.push({
id: uid('pres_'),
nombre,
peso: toFixed2(peso),
precio: toFixed2(precio),
moneda,
descripcion
});
}
guardarDatos();
const presAGuardar = editId ? presentaciones.find(p => p.id === editId) : presentaciones[presentaciones.length-1];
if (presAGuardar) guardarPresentacionSupabase(presAGuardar);
actualizarTablaPresentaciones();
cargarSelectorPresentaciones();
limpiarFormPresentacion();
alert(editId ? 'Presentación actualizada' : 'Presentación guardada');
}
function editarPresentacion(id) {
const pres = presentaciones.find(p => p.id === id);
if (!pres) return;
document.getElementById('presentacionEditId').value = pres.id;
document.getElementById('presentacionNombre').value = pres.nombre;
document.getElementById('presentacionPeso').value = pres.peso;
document.getElementById('presentacionPrecio').value = pres.precio;
document.getElementById('presentacionMoneda').value = pres.moneda;
document.getElementById('presentacionDescripcion').value = pres.descripcion || '';
}
function eliminarPresentacion(id) {
if (!confirm('¿Eliminar esta presentación?')) return;
presentaciones = presentaciones.filter(p => p.id !== id);
guardarDatos();
eliminarPresentacionSupabase(id);
actualizarTablaPresentaciones();
cargarSelectorPresentaciones();
}
function limpiarFormPresentacion() {
document.getElementById('presentacionEditId').value = '';
document.getElementById('presentacionNombre').value = '';
document.getElementById('presentacionPeso').value = '';
document.getElementById('presentacionPrecio').value = '';
document.getElementById('presentacionMoneda').value = 'MXN';
document.getElementById('presentacionDescripcion').value = '';
}
function actualizarTablaPresentaciones() {
const tbody = document.getElementById('tablaPresentaciones');
if (!tbody) return;
if (presentaciones.length === 0) {
tbody.innerHTML = 'Sin presentaciones registradas ';
return;
}
tbody.innerHTML = presentaciones.map(p => `
${p.nombre}
${formatNumber(p.peso)} kg
${formatMoney(p.precio)}
${p.moneda}
${p.descripcion || '-'}
✏️
🗑️
`).join('');
}
// ========================================
// PROYECCIONES
// ========================================
function agregarProyectado() {
const editId = document.getElementById('proyectadoEditId').value;
const semana = parseInt(document.getElementById('proyectadoSemana').value);
const gasto = parseFloat(document.getElementById('proyectadoGasto').value) || 0;
const produccion = parseFloat(document.getElementById('proyectadoProduccion').value) || 0;
if (!semana || semana < 1) {
alert('Ingresa un número de semana válido');
return;
}
if (editId) {
// Editar existente
const proy = proyecciones.find(p => p.id === editId);
if (proy) {
proy.semana = semana;
proy.gastoEstimado = toFixed2(gasto);
proy.produccionEstimada = toFixed2(produccion);
}
document.getElementById('proyectadoEditId').value = '';
} else {
// Nueva proyección
// Verificar si ya existe la semana
if (proyecciones.find(p => p.semana === semana)) {
alert('Ya existe una proyección para la semana ' + semana);
return;
}
proyecciones.push({
id: uid('proy_'),
semana,
gastoEstimado: toFixed2(gasto),
produccionEstimada: toFixed2(produccion)
});
}
guardarDatos();
actualizarTablaProyectado();
actualizarInteligencia();
limpiarFormProyectado();
}
function editarProyectado(id) {
const proy = proyecciones.find(p => p.id === id);
if (!proy) return;
document.getElementById('proyectadoEditId').value = proy.id;
document.getElementById('proyectadoSemana').value = proy.semana;
document.getElementById('proyectadoGasto').value = proy.gastoEstimado;
document.getElementById('proyectadoProduccion').value = proy.produccionEstimada;
}
function eliminarProyectado(id) {
if (!confirm('¿Eliminar esta proyección?')) return;
proyecciones = proyecciones.filter(p => p.id !== id);
guardarDatos();
actualizarTablaProyectado();
actualizarInteligencia();
}
function limpiarFormProyectado() {
document.getElementById('proyectadoEditId').value = '';
document.getElementById('proyectadoSemana').value = '';
document.getElementById('proyectadoGasto').value = '';
document.getElementById('proyectadoProduccion').value = '';
}
function actualizarTablaProyectado() {
const tbody = document.getElementById('tablaProyectado');
if (!tbody) return;
if (proyecciones.length === 0) {
tbody.innerHTML = 'Sin proyecciones ';
return;
}
// Ordenar por semana
const sorted = [...proyecciones].sort((a, b) => a.semana - b.semana);
tbody.innerHTML = sorted.map(p => `
S${p.semana}
${formatMoney(p.gastoEstimado)}
${formatNumber(p.produccionEstimada)} kg
✏️
🗑️
`).join('');
}
// ========================================
// STATS DE VENTAS
// ========================================
function actualizarStatsVentas() {
if (!cultivoActual) return;
const ventas = cultivoActual.ventas || [];
let ventaBruta = 0;
let descuentosTotales = 0;
let ventaNeta = 0;
let pendientes = 0;
// Agrupar descuentos por motivo para desglose
const descPorMotivo = {};
ventas.forEach(v => {
ventaBruta += v.bruta || 0;
const descVenta = obtenerDescuentosVenta(v);
descVenta.forEach(d => {
descuentosTotales += d.monto || 0;
const motivo = d.motivo || 'Otro';
descPorMotivo[motivo] = (descPorMotivo[motivo] || 0) + (d.monto || 0);
});
ventaNeta += v.neta || 0;
if (!v.liquidada) {
pendientes++;
}
});
const penalizaciones = descuentosTotales;
// Crear desglose dinámico
const desgloseTexto = Object.keys(descPorMotivo).length > 0
? Object.entries(descPorMotivo).map(([m, v]) => `${m}: ${formatMoney(v)}`).join(' | ')
: 'Sin descuentos';
document.getElementById('ventasBruta').textContent = formatMoney(ventaBruta);
document.getElementById('ventasPenalizaciones').textContent = formatMoney(penalizaciones);
document.getElementById('ventasDesglosePenalizaciones').textContent = desgloseTexto;
document.getElementById('ventasNetaConfirmada').textContent = formatMoney(ventaNeta);
document.getElementById('ventasPendientes').textContent = pendientes;
// Actualizar gráfico de Ventas por Presentación
actualizarChartVentasPresentacion();
}
function actualizarChartVentasPresentacion() {
const ctx = document.getElementById('chartVentasPres');
if (!ctx || !cultivoActual) return;
const ventas = cultivoActual.ventas || [];
// Agrupar ventas por presentación
const ventasPorPres = {};
let ventasSinPres = 0;
ventas.forEach(v => {
if (v.presentacionNombre) {
if (!ventasPorPres[v.presentacionNombre]) {
ventasPorPres[v.presentacionNombre] = { cantidad: 0, monto: 0, unidades: 0 };
}
ventasPorPres[v.presentacionNombre].cantidad += v.cantidad || 0;
ventasPorPres[v.presentacionNombre].monto += v.neta || 0;
ventasPorPres[v.presentacionNombre].unidades += v.unidades || 1;
} else {
ventasSinPres += v.neta || 0;
}
});
const labels = Object.keys(ventasPorPres);
const montos = labels.map(l => ventasPorPres[l].monto);
if (ventasSinPres > 0) {
labels.push('Sin Presentación');
montos.push(ventasSinPres);
}
// Destruir gráfico anterior si existe
if (window.chartVentasPresInstance) {
window.chartVentasPresInstance.destroy();
}
if (labels.length === 0) {
ctx.parentElement.innerHTML = 'Sin datos de ventas
';
return;
}
const colores = ['#2E7D32', '#388E3C', '#43A047', '#4CAF50', '#66BB6A', '#81C784', '#A5D6A7'];
window.chartVentasPresInstance = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: montos,
backgroundColor: colores.slice(0, labels.length),
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { font: { size: 11 } } },
tooltip: {
callbacks: {
label: function(context) {
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const porcentaje = ((context.raw / total) * 100).toFixed(1);
return `${context.label}: ${formatMoney(context.raw)} (${porcentaje}%)`;
}
}
}
}
}
});
}
// ========================================
// PERSISTENCIA
// ========================================
// ========================================
// SUPABASE CONFIG
// ========================================
const SUPABASE_URL = 'https://lvpnuivrgfumlomxgpby.supabase.co';
const SUPABASE_KEY = 'sb_publishable_AduAiCftwEbRWFeiw-ZymA_WrutW2P2';
const sb = supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
let usuarioActual = null;
let authTab = 'login';
let planActual = null; // 'basico', 'profesional', 'empresarial'
// ============================================================
// SISTEMA DE MEMBRESÍAS
// ============================================================
const PLANES = {
basico: {
nombre: 'Plan Básico',
max_cultivos: 1,
modulos: ['dashboard','monitoreo','almacen','reportes','config']
},
profesional: {
nombre: 'Plan Profesional',
max_cultivos: -1, // ilimitados
modulos: ['dashboard','monitoreo','almacen','planeacion','credito','inteligencia','reportes','config']
},
empresarial: {
nombre: 'Plan Empresarial',
max_cultivos: -1,
modulos: ['dashboard','monitoreo','almacen','planeacion','credito','inteligencia','reportes','reportes_avanzados','config','multiusuario']
}
};
async function cargarMembresia(userId) {
let data = null;
let error = null;
try {
const result = await sb.from('membresias')
.select('plan_id, estado, fecha_vencimiento')
.eq('user_id', userId)
.maybeSingle();
data = result.data;
error = result.error;
} catch(e) {
console.warn('ZLYX: error en membresía:', e.message);
error = e;
}
if (error || !data) {
planActual = 'basico';
} else if (data.estado !== 'activa') {
// Membresía suspendida o cancelada
mostrarPantallaAccesoDenegado(data.estado);
return false;
} else if (data.fecha_vencimiento && new Date(data.fecha_vencimiento) < new Date()) {
mostrarPantallaAccesoDenegado('vencida');
return false;
} else {
planActual = data.plan_id || 'basico';
}
aplicarRestriccionesPlan();
mostrarBadgePlan();
actualizarBotonesGroq();
return true;
}
function aplicarRestriccionesPlan() {
const plan = PLANES[planActual] || PLANES.basico;
// Módulos habilitados por plan
const modulosHabilitados = plan.modulos;
// Ocultar tabs de navegación que no corresponden al plan
const mapNavModulo = {
'planeacion': "cambiarTab('planeacion')",
'credito': "cambiarTab('credito')",
'inteligencia': "cambiarTab('inteligencia')",
};
Object.entries(mapNavModulo).forEach(([modulo, selector]) => {
const navItem = document.querySelector(`.nav-item[onclick*="${selector}"]`);
if (!navItem) return;
if (!modulosHabilitados.includes(modulo)) {
navItem.style.opacity = '0.35';
navItem.style.cursor = 'not-allowed';
navItem.onclick = (e) => {
e.preventDefault();
mostrarAlertaUpgrade(modulo);
};
// Agregar ícono de candado
if (!navItem.querySelector('.lock-icon')) {
const lock = document.createElement('span');
lock.className = 'lock-icon';
lock.textContent = '🔒';
lock.style.cssText = 'margin-left:auto;font-size:11px;opacity:0.6';
navItem.appendChild(lock);
}
}
});
// Guardar límite de cultivos en variable global para validación
window.MAX_CULTIVOS = plan.max_cultivos;
}
function mostrarBadgePlan() {
const plan = PLANES[planActual] || PLANES.basico;
// Agregar badge del plan en el sidebar debajo del email
const emailEl = document.getElementById('usuarioEmail');
if (emailEl && emailEl.parentNode) {
let badge = document.getElementById('planBadge');
if (!badge) {
badge = document.createElement('div');
badge.id = 'planBadge';
badge.style.cssText = 'font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;margin-top:2px;';
emailEl.parentNode.appendChild(badge);
}
const colores = {
basico: '#8b949e',
profesional: '#00c875',
empresarial: '#58a6ff'
};
badge.style.color = colores[planActual] || '#8b949e';
badge.textContent = plan.nombre;
}
}
function mostrarAlertaUpgrade(modulo) {
const plan = PLANES[planActual];
const nombres = {
planeacion: 'Planeación del Ciclo',
credito: 'Simulador de Crédito',
inteligencia: 'Análisis Inteligente'
};
alert(`🔒 ${nombres[modulo] || modulo} no está disponible en el ${plan.nombre}.
Contacta a soporte en admin.zlyx@gmail.com para actualizar tu plan.`);
}
function mostrarPantallaAccesoDenegado(motivo) {
const mensajes = {
suspendida: 'Tu cuenta ha sido suspendida. Contacta a soporte.',
cancelada: 'Tu membresía fue cancelada. Contacta a soporte para reactivarla.',
vencida: 'Tu membresía ha vencido. Contacta a soporte para renovarla.'
};
document.getElementById('modalAuth').style.display = 'none';
document.body.innerHTML = `
`;
}
// Validar límite de cultivos antes de guardar uno nuevo
function validarLimiteCultivos() {
const max = window.MAX_CULTIVOS || 1;
if (max === -1) return true; // ilimitados
if (cultivos.length >= max) {
alert(`🔒 Tu plan permite máximo ${max} cultivo(s) activo(s).
Contacta a soporte en admin.zlyx@gmail.com para actualizar al Plan Profesional (cultivos ilimitados).`);
return false;
}
return true;
}
// ========================================
// AUTH UI
// ========================================
function switchAuthTab(tab) {
// Simplificado: solo login, sin tabs ni recovery
authTab = 'login';
limpiarAuthMsg();
}
function mostrarAuthError(msg) {
const el = document.getElementById('authError');
el.textContent = msg;
el.style.display = 'block';
document.getElementById('authSuccess').style.display = 'none';
}
function mostrarAuthSuccess(msg) {
const el = document.getElementById('authSuccess');
el.textContent = msg;
el.style.display = 'block';
document.getElementById('authError').style.display = 'none';
}
function limpiarAuthMsg() {
document.getElementById('authError').style.display = 'none';
document.getElementById('authSuccess').style.display = 'none';
}
async function authSubmit() {
const email = document.getElementById('authEmail').value.trim();
const password = document.getElementById('authPassword').value;
const btn = document.getElementById('authSubmitBtn');
const loading = document.getElementById('authLoadingMsg');
if (!email || !password) { mostrarAuthError('Ingresa email y contraseña'); return; }
if (password.length < 6) { mostrarAuthError('La contraseña debe tener al menos 6 caracteres'); return; }
btn.disabled = true;
btn.style.opacity = '0.6';
loading.style.display = 'block';
limpiarAuthMsg();
if (authTab === 'login') {
const { data, error } = await sb.auth.signInWithPassword({ email, password });
if (error) {
mostrarAuthError(error.message === 'Invalid login credentials'
? 'Email o contraseña incorrectos'
: error.message);
} else {
// onAuthStateChange se encargará del resto
}
}
// Nota: registro deshabilitado — solo por invitación desde Supabase
btn.disabled = false;
btn.style.opacity = '1';
loading.style.display = 'none';
}
// ══════════════════════════════════════════════
// CHECKBOXES UX — Presupuesto, Gasto Extra, Sin Producción
// ══════════════════════════════════════════════
function toggleSinPresupuesto() {
const cb = document.getElementById('sinPresupuesto');
const wrapper = document.getElementById('presupuestoTotalWrapper');
const input = document.getElementById('presupuestoTotal');
if (cb.checked) {
wrapper.style.opacity = '0.4';
wrapper.style.pointerEvents = 'none';
input.value = '';
input.placeholder = 'Se calculará como suma de categorías';
// Calcular suma de categorías actual
if (cultivoActual) {
const suma = cultivoActual.categorias.reduce((s, c) => s + (c.presupuesto || 0), 0);
if (suma > 0) input.placeholder = 'Suma actual: $' + suma.toLocaleString('es-MX', {minimumFractionDigits:2});
}
} else {
wrapper.style.opacity = '1';
wrapper.style.pointerEvents = 'auto';
input.placeholder = '500,000';
}
}
function toggleGastoExtra() {
const cb = document.getElementById('checkGastoExtra');
const wrapper = document.getElementById('gastoExtraWrapper');
if (wrapper) {
wrapper.style.display = cb.checked ? 'block' : 'none';
if (!cb.checked) {
const input = document.getElementById('cierreGastoExtra');
const nota = document.getElementById('cierreNotaExtra');
if (input) input.value = '';
if (nota) nota.value = '';
}
}
}
function toggleSinProduccion() {
const cb = document.getElementById('checkSinProduccion');
const wrapper = document.getElementById('produccionWrapper');
if (wrapper) {
wrapper.style.display = cb.checked ? 'none' : 'block';
if (cb.checked) {
['cierreCosecha','cierrePrimera','cierreSegunda'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '0';
});
}
}
}
// Tipo de cambio — mostrar/ocultar por moneda y congelar si ya existe en semana
let _tcCongelado = null;
function toggleTCGasto(select) {
const wrapper = document.getElementById('tcGastoWrapper');
const input = document.getElementById('tcGastoInput');
if (!wrapper) return;
if (select.value === 'USD') {
wrapper.style.display = 'block';
// Si hay TC congelado para esta semana, pre-llenar
if (_tcCongelado) {
input.value = _tcCongelado;
input.readOnly = true;
input.style.background = 'var(--arena-oscuro)';
input.title = 'Tipo de cambio fijo para esta semana';
}
} else {
wrapper.style.display = 'none';
input.value = '';
}
}
function congelarTCDeSemana(semana) {
// Si la semana ya tiene gastos USD, usar ese TC
if (!cultivoActual) { _tcCongelado = null; return; }
const cierre = cultivoActual.cierres.find(c => c.semana === semana);
if (cierre && cierre.tipoCambio > 0) {
_tcCongelado = cierre.tipoCambio;
} else {
// Revisar gastosSemanaTemp
const gastoUSD = (window.gastosSemanaTemp || []).find(g => g.moneda === 'USD');
_tcCongelado = gastoUSD ? gastoUSD.tipoCambio : null;
}
}
// Limpiar checkboxes al abrir nuevo cierre
function resetCheckboxesCierre() {
const cbGasto = document.getElementById('checkGastoExtra');
const cbProd = document.getElementById('checkSinProduccion');
const tcInput = document.getElementById('tcGastoInput');
if (cbGasto) { cbGasto.checked = false; toggleGastoExtra(); }
if (cbProd) { cbProd.checked = false; toggleSinProduccion(); }
if (tcInput) { tcInput.readOnly = false; tcInput.style.background = ''; tcInput.value = ''; }
_tcCongelado = null;
// Ocultar TC del gasto
const tcW = document.getElementById('tcGastoWrapper');
if (tcW) tcW.style.display = 'none';
}
// ══════════════════════════════════════════════
// MODAL GENÉRICO DE EDICIÓN
// ══════════════════════════════════════════════
function abrirModalEdicion(titulo, htmlCuerpo, fnGuardar) {
document.getElementById('modalEdicionTitulo').textContent = '✏️ ' + titulo;
document.getElementById('modalEdicionCuerpo').innerHTML = htmlCuerpo;
document.getElementById('modalEdicionGuardar').onclick = fnGuardar;
document.getElementById('modalEdicion').classList.add('active');
}
function cerrarModalEdicion() {
document.getElementById('modalEdicion').classList.remove('active');
document.getElementById('modalEdicionCuerpo').innerHTML = '';
}
// ── Editar Cultivo ──
function editarCultivoModal(id) {
const c = cultivos.find(x => x.id === id);
if (!c) return;
const html = `
`;
abrirModalEdicion('Editar Cultivo', html, () => guardarEdicionCultivo(id));
}
async function guardarEdicionCultivo(id) {
const c = cultivos.find(x => x.id === id);
if (!c) return;
c.nombre = document.getElementById('ed_nombre').value.trim();
c.tipo = document.getElementById('ed_tipo').value.trim();
c.area = parseFloat(document.getElementById('ed_area').value) || c.area;
c.fechaInicio = document.getElementById('ed_fecha').value;
c.moneda = document.getElementById('ed_moneda').value;
c.duracionSemanas = parseInt(document.getElementById('ed_duracion').value) || c.duracionSemanas;
await sb.from('cultivos').update({
nombre: c.nombre, tipo: c.tipo, area: c.area,
fecha_inicio: c.fechaInicio, moneda: c.moneda,
duracion_semanas: c.duracionSemanas
}).eq('id', id);
cerrarModalEdicion();
actualizarSelectorCultivos();
actualizarTablaCultivos();
if (cultivoActual && cultivoActual.id === id) actualizarDashboard();
}
// ── Editar Categoría ──
function editarCategoriaModal(id) {
if (!cultivoActual) return;
const cat = cultivoActual.categorias.find(c => c.id === id);
if (!cat) return;
const conceptoOpts = conceptos.map(c =>
`${c.nombre} `
).join('');
const html = `
Concepto General
${conceptoOpts}
Nombre de Subcategoría
Presupuesto ($)
`;
abrirModalEdicion('Editar Categoría', html, () => guardarEdicionCategoria(id));
}
async function guardarEdicionCategoria(id) {
const cat = cultivoActual.categorias.find(c => c.id === id);
if (!cat) return;
cat.conceptoId = document.getElementById('ed_concepto').value;
cat.nombre = document.getElementById('ed_catNombre').value.trim();
cat.presupuesto = parseFloat(document.getElementById('ed_catPres').value) || 0;
await sb.from('cultivo_categorias').update({
concepto_id: cat.conceptoId, nombre: cat.nombre, presupuesto: cat.presupuesto
}).eq('id', id);
cerrarModalEdicion();
actualizarSelectCategorias();
actualizarTablaPresupuesto();
actualizarDashboard();
}
// ── Editar Venta ──
function editarVentaModal(id) {
const venta = cultivoActual?.ventas.find(v => v.id === id);
if (!venta) return;
const html = `
`;
abrirModalEdicion('Editar Venta', html, () => guardarEdicionVenta(id));
}
async function guardarEdicionVenta(id) {
const venta = cultivoActual?.ventas.find(v => v.id === id);
if (!venta) return;
venta.fecha = document.getElementById('ed_ventaFecha').value;
venta.cliente = document.getElementById('ed_ventaCliente').value.trim();
venta.cantidad = parseFloat(document.getElementById('ed_ventaKg').value) || venta.cantidad;
venta.bruta = parseFloat(document.getElementById('ed_ventaBruta').value) || venta.bruta;
await sb.from('cultivo_ventas').update({
fecha: venta.fecha, cliente: venta.cliente,
cantidad_kg: venta.cantidad, venta_bruta: venta.bruta
}).eq('id', id);
cerrarModalEdicion();
actualizarTablaVentas?.();
actualizarDashboard();
}
// ── Editar Insumo ──
function editarInsumoModal(id) {
const alm = getAlmacenActual();
const ins = alm?.find(i => i.id === id);
if (!ins) return;
const html = `
`;
abrirModalEdicion('Editar Insumo', html, () => guardarEdicionInsumo(id));
}
async function guardarEdicionInsumo(id) {
const alm = getAlmacenActual();
const ins = alm?.find(i => i.id === id);
if (!ins) return;
ins.nombre = document.getElementById('ed_insNombre').value.trim();
ins.categoria = document.getElementById('ed_insCat').value.trim();
ins.cantidad = parseFloat(document.getElementById('ed_insCant').value) || ins.cantidad;
ins.unidad = document.getElementById('ed_insUnidad').value.trim();
ins.precio = parseFloat(document.getElementById('ed_insPrecio').value) || 0;
ins.stockMin = parseFloat(document.getElementById('ed_insStockMin').value) || 0;
await sb.from('cultivo_almacen').update({
nombre: ins.nombre, categoria: ins.categoria,
cantidad: ins.cantidad, unidad: ins.unidad,
precio: ins.precio, stock_min: ins.stockMin
}).eq('id', id);
cerrarModalEdicion();
actualizarTablaAlmacen();
}
// ── Editar Deuda ──
function editarDeudaModal(id) {
const d = deudas.find(x => x.id === id);
if (!d) return;
const html = `
`;
abrirModalEdicion('Editar Deuda', html, () => guardarEdicionDeuda(id));
}
async function guardarEdicionDeuda(id) {
const d = deudas.find(x => x.id === id);
if (!d) return;
d.nombre = document.getElementById('ed_deudaNombre').value.trim();
d.tipo = document.getElementById('ed_deudaTipo').value;
d.saldoActual = parseFloat(document.getElementById('ed_deudaSaldo').value) || d.saldoActual;
d.pagoMensual = parseFloat(document.getElementById('ed_deudaPago').value) || d.pagoMensual;
d.tasa = parseFloat(document.getElementById('ed_deudaTasa').value) || 0;
d.notas = document.getElementById('ed_deudaNotas').value.trim();
await sb.from('deudas').update({
nombre: d.nombre, tipo: d.tipo, saldo_actual: d.saldoActual,
pago_mensual: d.pagoMensual, tasa: d.tasa, notas: d.notas
}).eq('id', id);
cerrarModalEdicion();
actualizarTablaDeudas();
actualizarResumenDeudas();
}
// ── Editar Presentación ──
function editarPresentacionModal(id) {
const p = presentaciones.find(x => x.id === id);
if (!p) return;
const html = `
`;
abrirModalEdicion('Editar Presentación', html, () => guardarEdicionPresentacion(id));
}
async function guardarEdicionPresentacion(id) {
const p = presentaciones.find(x => x.id === id);
if (!p) return;
p.nombre = document.getElementById('ed_presNombre').value.trim();
p.peso = parseFloat(document.getElementById('ed_presPeso').value) || p.peso;
p.precio = parseFloat(document.getElementById('ed_presPrecio').value) || p.precio;
p.moneda = document.getElementById('ed_presMoneda').value;
await sb.from('presentaciones').update({
nombre: p.nombre, peso: p.peso, precio: p.precio, moneda: p.moneda
}).eq('id', id);
cerrarModalEdicion();
actualizarTablaPresentaciones();
}
// ── Editar Proyectado ──
function editarProyectadoModal(id) {
const p = proyecciones.find(x => x.id === id);
if (!p) return;
const html = `
`;
abrirModalEdicion('Editar Proyección Semanal', html, () => guardarEdicionProyectado(id));
}
async function guardarEdicionProyectado(id) {
const p = proyecciones.find(x => x.id === id);
if (!p) return;
p.semana = parseInt(document.getElementById('ed_proySemana').value) || p.semana;
p.gastoEstimado = parseFloat(document.getElementById('ed_proyGasto').value) || 0;
p.produccionEstimada = parseFloat(document.getElementById('ed_proyProd').value) || 0;
p.precioEstimado = parseFloat(document.getElementById('ed_proyPrecio').value) || 0;
await sb.from('cultivo_proyectado_semanal').update({
semana: p.semana, gasto_estimado: p.gastoEstimado,
produccion_estimada: p.produccionEstimada, precio_estimado: p.precioEstimado
}).eq('id', id);
cerrarModalEdicion();
actualizarTablaProyectado();
}
// Agregar CSS para inputs del modal
const edStyle = document.createElement('style');
edStyle.textContent = `.form-input-edit { width:100%; padding:10px 12px; border:1px solid var(--arena-oscuro); border-radius:var(--radio-sm); font-family:inherit; font-size:14px; background:var(--blanco); transition:var(--transicion); }
.form-input-edit:focus { outline:none; border-color:var(--verde-claro); box-shadow:0 0 0 3px rgba(47,93,58,0.1); }`;
document.head.appendChild(edStyle);
function irPanelGlobal() {
// Activar tab Panel
cambiarTab('panel');
// Seleccionar Panel Global en el selector
const sel = document.getElementById('selectorCultivo');
if (sel) {
sel.value = '__GLOBAL__';
sel.dispatchEvent(new Event('change'));
}
// Actualizar estado visual del nav
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('navPanelGlobal')?.classList.add('active');
}
async function cerrarSesion() {
if (!confirm('¿Cerrar sesión?')) return;
await registrarLog('logout', 'Cierre de sesión');
await sb.auth.signOut();
usuarioActual = null; planActual = null;
cultivos = []; conceptos = []; almacen = [];
presentaciones = []; proyecciones = []; deudas = [];
cultivoActual = null;
document.getElementById('modalAuth').style.display = 'flex';
}
// ============================================================
// LOGS DE ACCESO
// ============================================================
async function registrarLog(evento, detalle = '') {
if (!usuarioActual) return;
try {
await sb.from('logs_acceso').insert({
user_id: usuarioActual.id,
evento,
detalle,
user_agent: navigator.userAgent.substring(0, 200)
});
} catch(e) { /* silencioso */ }
}
// ============================================================
// GROQ IA — Análisis real del cultivo
// ============================================================
const GROQ_API_KEY = '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';
const GROQ_MODEL = 'llama-3.3-70b-versatile'; // Mejor modelo gratuito
// ============================================================
// GROQ — Funciones por sección
// ============================================================
// Mostrar/ocultar botón IA en dashboard según plan
function actualizarBotonesGroq() {
const esEmpresarial = planActual === 'empresarial';
const esProOEmp = planActual === 'profesional' || esEmpresarial;
// Dashboard cultivo — botón IA profundo
const btnDash = document.getElementById('btnGroqDashboard');
if (btnDash) btnDash.style.display = esEmpresarial ? 'inline-flex' : 'none';
// Panel global — card IA global
const cardGlobal = document.getElementById('groqGlobalCard');
if (cardGlobal) cardGlobal.style.display = esEmpresarial ? 'block' : 'none';
// Tab Análisis Inteligente — secciones por plan
const cardGroq = document.getElementById('cardGroqAnalisis');
const cardUpgrade = document.getElementById('cardUpgradeIA');
const cardProyec = document.getElementById('cardProyecciones');
if (cardGroq) cardGroq.style.display = esEmpresarial ? 'block' : 'none';
if (cardUpgrade) cardUpgrade.style.display = esEmpresarial ? 'none' : 'block';
if (cardProyec) {
// Proyecciones disponibles para Pro y Empresarial
const badge = document.getElementById('proyeccionesPlanBadge');
if (badge && !esProOEmp) {
badge.innerHTML = 'Plan Básico — vista limitada ';
}
}
}
// ── ANÁLISIS CULTIVO INDIVIDUAL (desde dashboard) ──
async function groqAnalisisCultivo() {
if (!cultivoActual) return;
if (planActual !== 'empresarial') { mostrarAlertaUpgrade('inteligencia'); return; }
const t = calcularTotales();
const btn = document.getElementById('btnGroqDashboard');
const respDiv = document.getElementById('groqRespuestaCultivo');
const textoDiv = document.getElementById('groqCultivoTexto');
const loadingEl = document.getElementById('groqCultivoLoading');
btn.disabled = true;
btn.textContent = '⏳ Analizando...';
respDiv.style.display = 'block';
textoDiv.textContent = '';
loadingEl.textContent = '⏳ Procesando con Groq...';
const prompt = `Analiza estos datos financieros de un cultivo agrícola y dame un análisis profundo en español.
CULTIVO: ${cultivoActual.nombre} (${cultivoActual.tipo || 'N/D'})
ÁREA: ${cultivoActual.area} ha | SEMANAS: ${cultivoActual.cierres.length} | MONEDA: ${cultivoActual.moneda}
FINANCIEROS:
• ROI: ${t.roi.toFixed(2)}%
• Inversión total: $${t.inversionTotal.toLocaleString('es-MX', {minimumFractionDigits:2})}
• Ventas netas: $${t.ventasNetas.toLocaleString('es-MX', {minimumFractionDigits:2})}
• Utilidad neta: $${t.utilidadNeta.toLocaleString('es-MX', {minimumFractionDigits:2})}
• Merma: ${t.porcMerma.toFixed(2)}%
• Producción total: ${t.produccionTotal.toFixed(0)} kg
${t.dscr !== null ? `• DSCR: ${t.dscr.toFixed(2)} (${t.dscr >= 1.2 ? 'saludable' : t.dscr >= 1.0 ? 'zona de cuidado' : 'riesgo'})` : ''}
${cultivoActual.presupuestoTotal > 0 ? `• Presupuesto: $${cultivoActual.presupuestoTotal.toLocaleString('es-MX')} | Usado: ${((t.inversionTotal/cultivoActual.presupuestoTotal)*100).toFixed(1)}%` : ''}
CIERRES RECIENTES (últimas 3 semanas):
${cultivoActual.cierres.slice(-3).map(c => `• S${c.semana}: gastos $${c.totalGastos.toFixed(0)}, cosecha ${c.cosechaTotal.toFixed(0)}kg, merma ${c.merma.toFixed(0)}kg`).join('\n') || '• Sin cierres registrados'}
VENTAS: ${cultivoActual.ventas.length} registradas, ${cultivoActual.ventas.filter(v=>!v.liquidada).length} pendientes de liquidar
Dame exactamente esto (usa estos títulos):
1. ESTADO ACTUAL: evaluación concisa del ciclo (2-3 oraciones)
2. RIESGO PRINCIPAL: el problema más urgente con cifras específicas
3. OPORTUNIDAD: algo positivo o mejorable con potencial real
4. ACCIONES ESTA SEMANA: 3 acciones concretas numeradas
5. PROYECCIÓN: si continúa así, ¿cómo termina el ciclo?
Sé directo, usa números reales del análisis, habla como asesor financiero agrícola experto.`;
try {
const resp = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: { 'Authorization': `Bearer ${GROQ_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: GROQ_MODEL,
messages: [
{ role: 'system', content: 'Eres un asesor financiero agrícola experto en México y LATAM. Análisis precisos, directos y accionables. Siempre en español.' },
{ role: 'user', content: prompt }
],
max_tokens: 700,
temperature: 0.3
})
});
const data = await resp.json();
const respuesta = data.choices?.[0]?.message?.content || 'Sin respuesta.';
textoDiv.textContent = respuesta;
loadingEl.textContent = '';
await registrarLog('accion', `Análisis IA Groq — Cultivo: ${cultivoActual.nombre}`);
} catch(e) {
textoDiv.textContent = '❌ Error al conectar con Groq: ' + e.message;
loadingEl.textContent = '';
} finally {
btn.disabled = false;
btn.textContent = '✨ Análisis IA Profundo';
}
}
// ── ANÁLISIS GLOBAL (todos los cultivos) ──
async function groqAnalisisGlobal() {
if (planActual !== 'empresarial') { mostrarAlertaUpgrade('inteligencia'); return; }
if (cultivos.length === 0) { alert('No hay cultivos registrados.'); return; }
const btn = document.getElementById('btnGroqGlobal');
const respDiv = document.getElementById('groqGlobalRespuesta');
const textoDiv = document.getElementById('groqGlobalTexto');
const loadingEl = document.getElementById('groqGlobalLoading');
btn.disabled = true;
btn.textContent = '⏳ Analizando...';
respDiv.style.display = 'block';
textoDiv.textContent = '';
loadingEl.textContent = '⏳ Procesando con Groq...';
// Construir resumen de todos los cultivos
const resumenCultivos = cultivos.map(c => {
const temp = cultivoActual;
cultivoActual = c;
const t = calcularTotales();
cultivoActual = temp;
return {
nombre: c.nombre,
tipo: c.tipo || 'N/D',
area: c.area,
semanas: c.cierres.length,
roi: t.roi,
inversion: t.inversionTotal,
ventas: t.ventasNetas,
utilidad: t.utilidadNeta,
merma: t.porcMerma,
dscr: t.dscr,
produccion: t.produccionTotal,
ventasPendientes: c.ventas.filter(v => !v.liquidada).length
};
});
const totalInversion = resumenCultivos.reduce((s,c) => s + c.inversion, 0);
const totalVentas = resumenCultivos.reduce((s,c) => s + c.ventas, 0);
const totalUtilidad = resumenCultivos.reduce((s,c) => s + c.utilidad, 0);
const roiGlobal = totalInversion > 0 ? (totalUtilidad/totalInversion)*100 : 0;
const prompt = `Analiza la operación agrícola completa de este productor. Tiene ${cultivos.length} cultivo(s) activo(s).
RESUMEN GLOBAL:
• Inversión total portafolio: $${totalInversion.toLocaleString('es-MX', {minimumFractionDigits:2})}
• Ventas netas totales: $${totalVentas.toLocaleString('es-MX', {minimumFractionDigits:2})}
• Utilidad neta total: $${totalUtilidad.toLocaleString('es-MX', {minimumFractionDigits:2})}
• ROI global: ${roiGlobal.toFixed(2)}%
DETALLE POR CULTIVO:
${resumenCultivos.map((c,i) => `
Cultivo ${i+1}: ${c.nombre} (${c.tipo}) — ${c.area} ha — S${c.semanas}
• ROI: ${c.roi.toFixed(1)}% | Utilidad: $${c.utilidad.toLocaleString('es-MX', {minimumFractionDigits:0})}
• Merma: ${c.merma.toFixed(1)}% | Producción: ${c.produccion.toFixed(0)} kg
${c.dscr !== null ? `• DSCR: ${c.dscr.toFixed(2)}` : ''}
• Ventas pendientes: ${c.ventasPendientes}`).join('\n')}
Dame exactamente esto (usa estos títulos):
1. DIAGNÓSTICO GLOBAL: estado general del portafolio (2 oraciones con números)
2. CULTIVO PRIORITARIO: cuál necesita atención urgente y por qué
3. CULTIVO ESTRELLA: cuál tiene mejor desempeño y qué replicar
4. RIESGO SISTÉMICO: algo que afecte a toda la operación
5. ACCIONES ESTA SEMANA: 3 acciones concretas priorizadas por impacto
6. ESTRATEGIA: recomendación de mediano plazo para el portafolio
Compara los cultivos entre sí, usa cifras reales, sé directo como asesor financiero agrícola.`;
try {
const resp = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: { 'Authorization': `Bearer ${GROQ_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: GROQ_MODEL,
messages: [
{ role: 'system', content: 'Eres un asesor financiero agrícola experto en México y LATAM. Analizas portafolios de cultivos con precisión financiera. Siempre en español.' },
{ role: 'user', content: prompt }
],
max_tokens: 900,
temperature: 0.3
})
});
const data = await resp.json();
const respuesta = data.choices?.[0]?.message?.content || 'Sin respuesta.';
textoDiv.textContent = respuesta;
loadingEl.textContent = '✅ Análisis completado';
setTimeout(() => { loadingEl.textContent = ''; }, 3000);
await registrarLog('accion', `Análisis IA Global Groq — ${cultivos.length} cultivos`);
} catch(e) {
textoDiv.textContent = '❌ Error: ' + e.message;
loadingEl.textContent = '';
} finally {
btn.disabled = false;
btn.textContent = '✨ Analizar Operación Global';
}
}
async function consultarGroqIA() {
if (!cultivoActual) { alert('Selecciona un cultivo primero.'); return; }
if (!planActual || planActual === 'basico') {
mostrarAlertaUpgrade('inteligencia'); return;
}
if (!GROQ_API_KEY) {
alert('Groq no está configurado. Contacta al administrador.');
return;
}
const t = calcularTotales();
const contexto = `
Soy un productor agrícola usando ZLYX. Analiza mis datos financieros y dame recomendaciones específicas.
CULTIVO: ${cultivoActual.nombre} (${cultivoActual.tipo})
ÁREA: ${cultivoActual.area} hectáreas
SEMANAS REGISTRADAS: ${cultivoActual.cierres.length}
MONEDA: ${cultivoActual.moneda}
INDICADORES:
- ROI: ${t.roi.toFixed(2)}%
- Inversión total: $${t.inversionTotal.toFixed(2)}
- Ventas netas: $${t.ventasNetas.toFixed(2)}
- Utilidad neta: $${t.utilidadNeta.toFixed(2)}
- Merma: ${t.porcMerma.toFixed(2)}%
${t.dscr !== null ? `- DSCR: ${t.dscr.toFixed(2)}` : ''}
Dame: 1) Evaluación del estado financiero (2 oraciones), 2) Principal riesgo detectado, 3) Tres acciones concretas para mejorar rentabilidad. Responde en español, de forma directa y práctica para un productor agrícola.`;
const btn = document.getElementById('btnGroqIA');
if (btn) { btn.disabled = true; btn.textContent = '⏳ Analizando...'; }
try {
const resp = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${GROQ_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: GROQ_MODEL,
messages: [
{ role: 'system', content: 'Eres un asesor financiero agrícola experto. Respuestas concisas y accionables en español.' },
{ role: 'user', content: contexto }
],
max_tokens: 600,
temperature: 0.4
})
});
const data = await resp.json();
const respuesta = data.choices?.[0]?.message?.content || 'Sin respuesta de la IA.';
// Guardar en Supabase para histórico
await sb.from('logs_acceso').insert({
user_id: usuarioActual.id,
evento: 'accion',
detalle: `Consulta IA Groq - Cultivo: ${cultivoActual.nombre}`
});
// Mostrar respuesta en el modal de contexto IA
document.getElementById('contextoIATexto').value = `🤖 ANÁLISIS IA GROQ — ${cultivoActual.nombre}
${new Date().toLocaleString('es-MX')}
${respuesta}`;
mostrarModal('modalContextoIA');
} catch(e) {
alert('Error al consultar Groq: ' + e.message);
} finally {
if (btn) { btn.disabled = false; btn.textContent = '🤖 Analizar con IA'; }
}
}
// Escuchar cambios de sesión
let _cargandoDatos = false; // Flag para evitar cargas múltiples
sb.auth.onAuthStateChange(async (event, session) => {
if (event === 'PASSWORD_RECOVERY') {
document.getElementById('modalAuth').style.display = 'none';
document.getElementById('modalNuevaPassword').style.display = 'flex';
return;
}
if (event === 'USER_UPDATED') {
document.getElementById('modalNuevaPassword').style.display = 'none';
if (session?.user) {
usuarioActual = session.user;
document.getElementById('usuarioEmail').textContent = session.user.email;
if (!_cargandoDatos) await cargarDatosSupabase();
}
return;
}
if (event === 'SIGNED_IN' && session?.user) {
if (_cargandoDatos) return; // Evitar doble carga
_cargandoDatos = true;
const recoveryAbierto = document.getElementById('modalNuevaPassword').style.display === 'flex';
if (recoveryAbierto) { _cargandoDatos = false; return; }
usuarioActual = session.user;
document.getElementById('modalAuth').style.display = 'none';
document.getElementById('usuarioEmail').textContent = session.user.email;
setTimeout(() => registrarLog('login', 'Inicio de sesión'), 3000);
await cargarDatosSupabase();
_cargandoDatos = false;
return;
}
if (!session) {
_cargandoDatos = false;
if (document.getElementById('modalNuevaPassword').style.display !== 'flex') {
document.getElementById('modalAuth').style.display = 'flex';
}
}
});
async function guardarNuevaPassword() {
const nueva = document.getElementById('nuevaPassword').value;
const confirmar = document.getElementById('confirmarPassword').value;
const errEl = document.getElementById('recoveryError');
const successEl = document.getElementById('recoverySuccessMsg');
const loadingEl = document.getElementById('recoveryLoadingMsg');
const btn = document.querySelector('#modalNuevaPassword .btn');
errEl.style.display = 'none';
successEl.style.display = 'none';
if (!nueva || nueva.length < 6) {
errEl.textContent = 'La contraseña debe tener al menos 6 caracteres';
errEl.style.display = 'block';
return;
}
if (nueva !== confirmar) {
errEl.textContent = 'Las contraseñas no coinciden';
errEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.style.opacity = '0.6';
loadingEl.style.display = 'block';
const { error } = await sb.auth.updateUser({ password: nueva });
btn.disabled = false;
btn.style.opacity = '1';
loadingEl.style.display = 'none';
if (error) {
errEl.textContent = error.message;
errEl.style.display = 'block';
} else {
successEl.textContent = '✅ Contraseña actualizada. Iniciando sesión...';
successEl.style.display = 'block';
// Esperar 1.5s y luego cargar la app
setTimeout(async () => {
const { data } = await sb.auth.getSession();
if (data.session) {
usuarioActual = data.session.user;
document.getElementById('modalNuevaPassword').style.display = 'none';
document.getElementById('usuarioEmail').textContent = usuarioActual.email;
await cargarDatosSupabase();
}
}, 1500);
}
}
// ========================================
// CARGA DE DATOS DESDE SUPABASE
// ========================================
async function cargarDatosSupabase() {
if (!usuarioActual) return;
const uid = usuarioActual.id;
// Timeout de seguridad: si tarda más de 30s, ocultar loading silenciosamente
const timeoutId = setTimeout(() => {
console.error('ZLYX: timeout al cargar datos');
mostrarLoadingGlobal(false);
_cargandoDatos = false;
}, 30000);
try {
mostrarLoadingGlobal(true);
console.log('ZLYX: iniciando carga de datos para', uid.substring(0,8));
// Verificar membresía antes de cargar datos
const accesoOk = await cargarMembresia(uid);
if (!accesoOk) { clearTimeout(timeoutId); mostrarLoadingGlobal(false); return; }
console.log('ZLYX: membresía OK —', planActual);
// Cargar en paralelo para máxima velocidad
const [
{ data: concData },
{ data: presData },
{ data: cultData },
{ data: deudaData }
] = await Promise.all([
sb.from('conceptos').select('*').eq('user_id', uid).order('created_at'),
sb.from('presentaciones').select('*').eq('user_id', uid).order('created_at'),
sb.from('cultivos').select('*').eq('user_id', uid).order('created_at'),
sb.from('deudas').select('*').eq('user_id', uid).order('created_at')
]);
// Conceptos
conceptos = (concData || []).map(c => ({
id: c.id, nombre: c.nombre, color: c.color
}));
// Si no hay conceptos, crear defaults
if (conceptos.length === 0) {
await crearConceptosDefault();
}
// Presentaciones
presentaciones = (presData || []).map(p => ({
id: p.id, nombre: p.nombre, peso: p.peso,
precio: p.precio, moneda: p.moneda, descripcion: p.descripcion
}));
// Deudas
deudas = (deudaData || []).map(d => ({
id: d.id, nombre: d.nombre, tipo: d.tipo,
montoOriginal: d.monto_original, saldoActual: d.saldo_actual,
tasa: d.tasa, pagoMensual: d.pago_mensual,
periodicidad: d.periodicidad, tipoInteres: d.tipo_interes,
vencimiento: d.vencimiento, notas: d.notas,
cultivoId: d.cultivo_id, fechaRegistro: d.fecha_registro
}));
// Cultivos con todos sus datos anidados — carga en paralelo
cultivos = await Promise.all(
(cultData || []).map(c => cargarCultivoCompleto(c))
);
// Inicializar UI
clearTimeout(timeoutId);
console.log('ZLYX: datos cargados —', cultivos.length, 'cultivos');
inicializarUI();
mostrarLoadingGlobal(false);
} catch (err) {
clearTimeout(timeoutId);
console.error('ZLYX: error cargando datos:', err);
mostrarLoadingGlobal(false);
_cargandoDatos = false;
alert('Error al cargar datos: ' + err.message);
}
}
async function cargarCultivoCompleto(c) {
const cid = c.id;
const [
{ data: cats },
{ data: almData },
{ data: cierresData },
{ data: ventasData },
{ data: apertura },
{ data: planeacion },
{ data: planSemanas },
{ data: credito }
] = await Promise.all([
sb.from('cultivo_categorias').select('*').eq('cultivo_id', cid).order('created_at'),
sb.from('cultivo_almacen').select('*').eq('cultivo_id', cid),
sb.from('cultivo_cierres').select('*, cultivo_cierre_gastos(*), cultivo_cierre_consumos(*)').eq('cultivo_id', cid).order('semana'),
sb.from('cultivo_ventas').select('*, cultivo_venta_descuentos(*)').eq('cultivo_id', cid).order('fecha'),
sb.from('cultivo_apertura').select('*').eq('cultivo_id', cid).maybeSingle(),
sb.from('cultivo_planeacion').select('*').eq('cultivo_id', cid).maybeSingle(),
sb.from('cultivo_planeacion_semanas').select('*').eq('cultivo_id', cid).order('semana'),
sb.from('cultivo_credito').select('*').eq('cultivo_id', cid).maybeSingle()
]);
return {
id: cid,
nombre: c.nombre,
tipo: c.tipo,
area: c.area,
fechaInicio: c.fecha_inicio,
moneda: c.moneda,
duracionSemanas: c.duracion_semanas,
estado: c.estado,
presupuestoTotal: c.presupuesto_total,
categorias: (cats || []).map(cat => ({
id: cat.id, conceptoId: cat.concepto_id, nombre: cat.nombre,
presupuesto: cat.presupuesto, gastadoInicial: cat.gastado_inicial,
gastado: cat.gastado
})),
almacen: (almData || []).map(ins => ({
id: ins.id, nombre: ins.nombre, categoria: ins.categoria,
cantidad: ins.cantidad, unidad: ins.unidad,
precio: ins.precio, stockMin: ins.stock_min,
origenPresupuesto: ins.origen_presupuesto
})),
cierres: (cierresData || []).map(ci => ({
id: ci.id, semana: ci.semana, fecha: ci.fecha,
tipoCambio: ci.tipo_cambio,
gastos: (ci.cultivo_cierre_gastos || []).map(g => ({
categoriaId: g.categoria_id, categoriaNombre: g.categoria_nombre,
monto: g.monto, moneda: g.moneda
})),
totalGastos: ci.total_gastos, gastoExtra: ci.gasto_extra,
notaExtra: ci.nota_extra, cosechaTotal: ci.cosecha_total,
primeraCalidad: ci.primera_calidad, segundaCalidad: ci.segunda_calidad,
merma: ci.merma,
consumos: (ci.cultivo_cierre_consumos || []).map(co => ({
insumoId: co.insumo_id, cantidad: co.cantidad, valor: co.valor
})),
valorConsumo: ci.valor_consumo
})),
ventas: (ventasData || []).map(v => ({
id: v.id, fecha: v.fecha, cliente: v.cliente,
cantidad: v.cantidad_kg, bruta: v.venta_bruta, neta: v.venta_neta,
liquidada: v.liquidada, fechaLiquidacion: v.fecha_liquidacion,
presentacionId: v.presentacion_id,
presentacionNombre: v.presentacion_nombre,
unidades: v.unidades,
descuentos: (v.cultivo_venta_descuentos || []).map(d => ({
motivo: d.motivo, monto: d.monto
}))
})),
apertura: apertura ? {
semanaInicio: apertura.semana_inicio,
gastosAnteriores: apertura.gastos_anteriores,
ventasAnteriores: apertura.ventas_anteriores,
produccionAnterior: apertura.produccion_anterior
} : null,
planeacion: planeacion ? {
semanasTotales: planeacion.semanas_totales,
semanasPreCosecha: planeacion.semanas_pre_cosecha,
produccionMeta: planeacion.produccion_meta,
presupuestoTotal: planeacion.presupuesto_total,
porcPreparacion: planeacion.porc_preparacion,
porcCrecimiento: planeacion.porc_crecimiento,
porcCosecha: planeacion.porc_cosecha,
distribucion: (planSemanas || []).map(s => ({
semana: s.semana, fase: s.fase,
produccion: s.produccion, precio: s.precio, gasto: s.gasto
}))
} : null,
credito: credito ? {
monto: credito.monto, tasaAnual: credito.tasa_anual,
plazo: credito.plazo, cuota: credito.cuota,
amortizacion: credito.amortizacion
} : null,
gastosExtra: [],
planificacion: []
};
}
async function crearConceptosDefault() {
if (!usuarioActual) return;
const defaults = [
{ id: 'conc_1', nombre: 'Insumos', color: '#2E7D32' },
{ id: 'conc_2', nombre: 'Labor', color: '#1565C0' },
{ id: 'conc_3', nombre: 'Operativos', color: '#F57C00' },
{ id: 'conc_4', nombre: 'Ventas', color: '#7B1FA2' },
{ id: 'conc_5', nombre: 'Otros', color: '#455A64' }
];
const rows = defaults.map(c => ({ ...c, user_id: usuarioActual.id }));
const { error } = await sb.from('conceptos').insert(rows);
if (!error) conceptos = defaults;
}
// ========================================
// GUARDAR DATOS EN SUPABASE
// (reemplaza localStorage completamente)
// ========================================
// Guardar cultivo completo (upsert)
async function guardarCultivoSupabase(cultivo) {
if (!usuarioActual) return;
const { error } = await sb.from('cultivos').upsert({
id: cultivo.id,
user_id: usuarioActual.id,
nombre: cultivo.nombre,
tipo: cultivo.tipo,
area: cultivo.area,
fecha_inicio: cultivo.fechaInicio || null,
moneda: cultivo.moneda,
duracion_semanas: cultivo.duracionSemanas,
estado: cultivo.estado,
presupuesto_total: cultivo.presupuestoTotal || 0
});
if (error) console.error('Error guardando cultivo:', error);
}
async function eliminarCultivoSupabase(cultivoId) {
if (!usuarioActual) return;
// ON DELETE CASCADE se encarga de las tablas hijas
const { error } = await sb.from('cultivos').delete().eq('id', cultivoId);
if (error) console.error('Error eliminando cultivo:', error);
}
// Guardar concepto
async function guardarConceptoSupabase(concepto) {
if (!usuarioActual) return;
const { error } = await sb.from('conceptos').upsert({
id: concepto.id, user_id: usuarioActual.id,
nombre: concepto.nombre, color: concepto.color
});
if (error) console.error('Error guardando concepto:', error);
}
async function eliminarConceptoSupabase(id) {
const { error } = await sb.from('conceptos').delete().eq('id', id);
if (error) console.error('Error eliminando concepto:', error);
}
// Guardar presentación
async function guardarPresentacionSupabase(p) {
if (!usuarioActual) return;
const { error } = await sb.from('presentaciones').upsert({
id: p.id, user_id: usuarioActual.id,
nombre: p.nombre, peso: p.peso,
precio: p.precio, moneda: p.moneda, descripcion: p.descripcion || null
});
if (error) console.error('Error guardando presentación:', error);
}
async function eliminarPresentacionSupabase(id) {
const { error } = await sb.from('presentaciones').delete().eq('id', id);
if (error) console.error('Error eliminando presentación:', error);
}
// Guardar categoría de presupuesto
async function guardarCategoriaSupabase(cat, cultivoId) {
const { error } = await sb.from('cultivo_categorias').upsert({
id: cat.id, cultivo_id: cultivoId,
concepto_id: cat.conceptoId || null,
nombre: cat.nombre, presupuesto: cat.presupuesto,
gastado_inicial: cat.gastadoInicial || 0, gastado: cat.gastado || 0
});
if (error) console.error('Error guardando categoría:', error);
}
async function eliminarCategoriaSupabase(id) {
const { error } = await sb.from('cultivo_categorias').delete().eq('id', id);
if (error) console.error('Error eliminando categoría:', error);
}
// Actualizar gastado de una categoría (cuando se hace cierre)
async function actualizarGastadoCategoria(catId, nuevoGastado) {
const { error } = await sb.from('cultivo_categorias')
.update({ gastado: nuevoGastado })
.eq('id', catId);
if (error) console.error('Error actualizando gastado:', error);
}
// Guardar insumo de almacén
async function guardarInsumoSupabase(ins, cultivoId) {
const { error } = await sb.from('cultivo_almacen').upsert({
id: ins.id, cultivo_id: cultivoId,
nombre: ins.nombre, categoria: ins.categoria,
cantidad: ins.cantidad, unidad: ins.unidad,
precio: ins.precio, stock_min: ins.stockMin || 0,
origen_presupuesto: ins.origenPresupuesto || null
});
if (error) console.error('Error guardando insumo:', error);
}
async function eliminarInsumoSupabase(id) {
const { error } = await sb.from('cultivo_almacen').delete().eq('id', id);
if (error) console.error('Error eliminando insumo:', error);
}
async function actualizarCantidadInsumo(insumoId, nuevaCantidad) {
const { error } = await sb.from('cultivo_almacen')
.update({ cantidad: nuevaCantidad })
.eq('id', insumoId);
if (error) console.error('Error actualizando insumo:', error);
}
// Guardar cierre semanal
async function guardarCierreSupabase(cierre, cultivoId) {
// 1. Guardar cierre principal
const { error: errCierre } = await sb.from('cultivo_cierres').insert({
id: cierre.id, cultivo_id: cultivoId,
semana: cierre.semana, fecha: cierre.fecha,
tipo_cambio: cierre.tipoCambio || 0,
total_gastos: cierre.totalGastos, gasto_extra: cierre.gastoExtra || 0,
nota_extra: cierre.notaExtra || null,
cosecha_total: cierre.cosechaTotal, primera_calidad: cierre.primeraCalidad,
segunda_calidad: cierre.segundaCalidad, merma: cierre.merma,
valor_consumo: cierre.valorConsumo || 0
});
if (errCierre) { console.error('Error guardando cierre:', errCierre); return; }
// 2. Gastos del cierre
if (cierre.gastos && cierre.gastos.length > 0) {
const gastos = cierre.gastos.map(g => ({
cierre_id: cierre.id,
categoria_id: g.categoriaId || null,
categoria_nombre: g.categoriaNombre,
monto: g.monto, moneda: g.moneda
}));
await sb.from('cultivo_cierre_gastos').insert(gastos);
}
// 3. Consumos del cierre
if (cierre.consumos && cierre.consumos.length > 0) {
const consumos = cierre.consumos.map(co => ({
cierre_id: cierre.id,
insumo_id: co.insumoId || null,
cantidad: co.cantidad, valor: co.valor
}));
await sb.from('cultivo_cierre_consumos').insert(consumos);
}
}
async function eliminarCierreSupabase(id) {
// CASCADE elimina gastos y consumos automáticamente
const { error } = await sb.from('cultivo_cierres').delete().eq('id', id);
if (error) console.error('Error eliminando cierre:', error);
}
// Guardar venta
async function guardarVentaSupabase(venta, cultivoId) {
const { error } = await sb.from('cultivo_ventas').upsert({
id: venta.id, cultivo_id: cultivoId,
presentacion_id: venta.presentacionId || null,
presentacion_nombre: venta.presentacionNombre || null,
fecha: venta.fecha, cliente: venta.cliente,
cantidad_kg: venta.cantidad, unidades: venta.unidades || 0,
venta_bruta: venta.bruta, venta_neta: venta.neta,
liquidada: venta.liquidada || false,
fecha_liquidacion: venta.fechaLiquidacion || null
});
if (error) console.error('Error guardando venta:', error);
}
async function eliminarVentaSupabase(id) {
const { error } = await sb.from('cultivo_ventas').delete().eq('id', id);
if (error) console.error('Error eliminando venta:', error);
}
async function guardarDescuentosVenta(ventaId, descuentos) {
// Borrar descuentos anteriores y reinsertar
await sb.from('cultivo_venta_descuentos').delete().eq('venta_id', ventaId);
if (descuentos && descuentos.length > 0) {
const rows = descuentos.map(d => ({
venta_id: ventaId, motivo: d.motivo, monto: d.monto
}));
await sb.from('cultivo_venta_descuentos').insert(rows);
}
}
// Guardar deuda
async function guardarDeudaSupabase(deuda) {
if (!usuarioActual) return;
const { error } = await sb.from('deudas').upsert({
id: deuda.id, user_id: usuarioActual.id,
cultivo_id: deuda.cultivoId || '__NINGUNO__',
nombre: deuda.nombre, tipo: deuda.tipo,
monto_original: deuda.montoOriginal, saldo_actual: deuda.saldoActual,
tasa: deuda.tasa, pago_mensual: deuda.pagoMensual,
periodicidad: deuda.periodicidad || 'mensual',
tipo_interes: deuda.tipoInteres || 'compuesto',
vencimiento: deuda.vencimiento || null,
notas: deuda.notas || null
});
if (error) console.error('Error guardando deuda:', error);
}
async function eliminarDeudaSupabase(id) {
const { error } = await sb.from('deudas').delete().eq('id', id);
if (error) console.error('Error eliminando deuda:', error);
}
// Guardar apertura
async function guardarAperturaSupabase(ap, cultivoId) {
const { error } = await sb.from('cultivo_apertura').upsert({
cultivo_id: cultivoId,
semana_inicio: ap.semanaInicio,
gastos_anteriores: ap.gastosAnteriores,
ventas_anteriores: ap.ventasAnteriores,
produccion_anterior: ap.produccionAnterior
}, { onConflict: 'cultivo_id' });
if (error) console.error('Error guardando apertura:', error);
}
async function eliminarAperturaSupabase(cultivoId) {
const { error } = await sb.from('cultivo_apertura').delete().eq('cultivo_id', cultivoId);
if (error) console.error('Error eliminando apertura:', error);
}
// Guardar planeación
async function guardarPlaneacionSupabase(plan, cultivoId, distribucion) {
const { error } = await sb.from('cultivo_planeacion').upsert({
cultivo_id: cultivoId,
semanas_totales: plan.semanasTotales,
semanas_pre_cosecha: plan.semanasPreCosecha,
produccion_meta: plan.produccionMeta || 0,
presupuesto_total: plan.presupuestoTotal || 0,
porc_preparacion: plan.porcPreparacion,
porc_crecimiento: plan.porcCrecimiento,
porc_cosecha: plan.porcCosecha
}, { onConflict: 'cultivo_id' });
if (error) { console.error('Error guardando planeación:', error); return; }
// Upsert distribución semanal
if (distribucion && distribucion.length > 0) {
const rows = distribucion.map(s => ({
cultivo_id: cultivoId,
semana: s.semana, fase: s.fase || null,
produccion: s.produccion || 0,
precio: s.precio || 18,
gasto: s.gasto || 0
}));
await sb.from('cultivo_planeacion_semanas').upsert(rows, { onConflict: 'cultivo_id,semana' });
}
}
// Guardar crédito
async function guardarCreditoSupabase(credito, cultivoId) {
const { error } = await sb.from('cultivo_credito').upsert({
cultivo_id: cultivoId,
monto: credito.monto, tasa_anual: credito.tasaAnual,
plazo: credito.plazo, cuota: credito.cuota,
amortizacion: credito.amortizacion || null
}, { onConflict: 'cultivo_id' });
if (error) console.error('Error guardando crédito:', error);
}
// Actualizar presupuesto total del cultivo
async function actualizarPresupuestoTotalSupabase(cultivoId, valor) {
const { error } = await sb.from('cultivos')
.update({ presupuesto_total: valor })
.eq('id', cultivoId);
if (error) console.error('Error actualizando presupuesto:', error);
}
// ========================================
// FUNCIONES LEGACY (guardarDatos / cargarDatos)
// Se mantienen vacías para no romper llamadas existentes.
// Las operaciones reales ya son async en cada función.
// ========================================
function guardarDatos() {
// No-op: los datos se guardan en Supabase directamente
// en cada función específica (guardarCultivoSupabase, etc.)
}
function cargarDatos() {
// No-op: los datos se cargan en cargarDatosSupabase()
}
function exportarDatos() {
const backup = {
version: 'ZLYX_v7.0',
fecha: new Date().toISOString(),
datos: { cultivos, conceptos, almacen, presentaciones, proyecciones, deudas }
};
const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ZLYX_respaldo_' + new Date().toISOString().split('T')[0] + '.json';
a.click();
URL.revokeObjectURL(url);
alert('✅ Respaldo JSON descargado.');
}
function importarDatos() {
alert('Para migrar datos desde un respaldo local a Supabase, contacta al administrador.');
}
async function borrarTodosLosDatos() {
if (!confirm('⚠️ ¿BORRAR TODOS LOS DATOS?\n\n¡Esta acción NO se puede deshacer!')) return;
if (!confirm('🚨 SEGUNDA CONFIRMACIÓN\n\n¿Estás absolutamente seguro?')) return;
if (!usuarioActual) return;
// Borrar en orden correcto (hijos antes que padres)
const uid = usuarioActual.id;
await sb.from('cultivos').delete().eq('user_id', uid); // CASCADE elimina todo lo demás
await sb.from('conceptos').delete().eq('user_id', uid);
await sb.from('presentaciones').delete().eq('user_id', uid);
await sb.from('deudas').delete().eq('user_id', uid);
location.reload();
}
// ========================================
// LOADING OVERLAY SIMPLE
// ========================================
function mostrarLoadingGlobal(show) {
let el = document.getElementById('loadingOverlay');
if (!el) {
el = document.createElement('div');
el.id = 'loadingOverlay';
el.style.cssText = 'position:fixed;inset:0;background:rgba(239,233,221,0.85);z-index:8000;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:12px;';
el.innerHTML = '
Cargando datos...
';
document.body.appendChild(el);
// Agregar keyframe si no existe
if (!document.getElementById('spinStyle')) {
const s = document.createElement('style');
s.id = 'spinStyle';
s.textContent = '@keyframes spin{to{transform:rotate(360deg)}}';
document.head.appendChild(s);
}
}
el.style.display = show ? 'flex' : 'none';
}
// ========================================
// INICIALIZACIÓN (ahora asíncrona via onAuthStateChange)
// ========================================
function inicializarUI() {
// Crear conceptos predeterminados en memoria si no existen
if (conceptos.length === 0) {
conceptos = [
{ id: 'conc_1', nombre: 'Insumos', color: '#2E7D32' },
{ id: 'conc_2', nombre: 'Labor', color: '#1565C0' },
{ id: 'conc_3', nombre: 'Operativos', color: '#F57C00' },
{ id: 'conc_4', nombre: 'Ventas', color: '#7B1FA2' },
{ id: 'conc_5', nombre: 'Otros', color: '#455A64' }
];
}
migrarAlmacenACultivos();
cargarCategoriasInsumos();
actualizarSelectorCultivos();
actualizarSelectorAlmacen();
actualizarSelectorDeudaCultivo();
actualizarTablaCultivos();
actualizarTablaConceptos();
actualizarTablaAlmacen();
actualizarTablaPresentaciones();
actualizarTablaProyectado();
actualizarTablaDeudas();
actualizarResumenDeudas();
if (cultivos.length > 0) {
document.getElementById('selectorCultivo').value = cultivos[0].id;
seleccionarCultivo(cultivos[0].id);
} else {
cultivoActual = null;
document.getElementById('dashboardGlobal').style.display = 'block';
document.getElementById('dashboardCultivo').style.display = 'none';
actualizarDashboardGlobal();
limpiarTablasDelCultivo();
// Mensaje de bienvenida para usuario nuevo sin cultivos
if (!document.getElementById('welcomeMsg')) {
const w = document.createElement('div');
w.id = 'welcomeMsg';
w.style.cssText = 'position:fixed;bottom:32px;right:32px;background:var(--verde-raiz);color:white;padding:20px 24px;border-radius:var(--radio-lg);box-shadow:0 8px 24px rgba(47,93,58,0.3);max-width:300px;z-index:500;animation:modalIn 0.3s ease;';
w.innerHTML = '👋 ¡Bienvenido a ZLYX!
' +
'Para empezar, crea tu primer cultivo desde Configuración → Mis Cultivos .
' +
'Crear mi primer cultivo → ' +
'Cerrar ';
document.body.appendChild(w);
}
}
console.log('ZLYX v7.0 Pro + Supabase iniciado');
}
async function inicializar() {
// 1. Detectar si viene de un link de recovery en la URL
const hash = window.location.hash;
if (hash.includes('type=recovery') || hash.includes('type=invite')) {
document.getElementById('modalAuth').style.display = 'none';
return;
}
// 2. Verificar sesión existente
const { data } = await sb.auth.getSession();
if (!data.session) {
document.getElementById('modalAuth').style.display = 'flex';
}
}
// Fix Chrome: al volver a la pestaña, verificar si sigue cargando
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible' && usuarioActual) {
// Si quedó atascado en "cargando", resetear flag
if (_cargandoDatos) {
_cargandoDatos = false;
}
// Verificar que la sesión sigue activa
const { data } = await sb.auth.getSession();
if (!data.session) {
usuarioActual = null;
document.getElementById('modalAuth').style.display = 'flex';
}
}
});
function cargarCategoriasInsumos() {
const categoriasDefault = ['Insumos', 'Empaque', 'Herramientas', 'Fertilizantes', 'Semillas'];
cultivos.forEach(cultivo => {
if (cultivo.almacen) {
cultivo.almacen.forEach(ins => {
if (!categoriasDefault.includes(ins.categoria) && ins.categoria !== '__OTRA__') {
agregarCategoriaAlSelect(ins.categoria);
}
});
}
});
almacen.forEach(ins => {
if (!categoriasDefault.includes(ins.categoria) && ins.categoria !== '__OTRA__') {
agregarCategoriaAlSelect(ins.categoria);
}
});
}
document.addEventListener('DOMContentLoaded', inicializar);