diff --git a/public/js/chatbot.js b/public/js/chatbot.js new file mode 100644 index 0000000..357489c --- /dev/null +++ b/public/js/chatbot.js @@ -0,0 +1,227 @@ +/* +EVENTO: DOMContentLoaded + +Se ejecuta cuando el HTML termina de cargarse + +Esto asegura que todos los elementos del DOM existan antes de intentar utilizarlos desde JavaScript +*/ + +document.addEventListener("DOMContentLoaded", () => { + + /* + REFERENCIAS DEL DOM + + Obtenemos: + - form -> formulario del chat + - input -> input donde escribe el usuario + - chatBox -> contenedor de mensajes + */ + + const form = document.getElementById("chat-form"); + const input = document.getElementById("mensaje"); + const chatBox = document.getElementById("chat-box"); + + // Se detiene el script si no existe algún elemento + + if (!form || !input || !chatBox) return; + + // EVENTO SUBMIT -> se ejecuta cuando el usuario envía el formulario + + form.addEventListener("submit", async (e) => { + + // PREVENIR RECARGA -> Evita que el formulario recargue la página + + e.preventDefault(); + + // OBTENER MENSAJE + + const mensaje = input.value.trim(); + + // VALIDAR MENSAJE + + if (!mensaje) return; + + // MOSTRAR MENSAJE DEL USUARIO -> Se agrega inmediatamente al chat + + agregarMensaje("user", mensaje); + + // LIMPIAR INPUT + + input.value = ""; + + // MOSTRAR TYPING (ESCRIBIENDO...) + + mostrarTyping(); + + try { + + /* + FETCH AJAX + + Realizamos una petición POST hacia chatbot.php + */ + + const response = await fetch( + window.location.origin + "/ajax/chatbot.php", + { + method: "POST", + + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + + /* + BODY + + encodeURIComponent(): + - Escapa caracteres especiales + */ + + body: + "mensaje=" + encodeURIComponent(mensaje) + } + ); + + // ELIMINAR TYPING + + removerTyping(); + + // CONVERTIR RESPUESTA JSON + + const data = await response.json(); + + // ERROR BACKEND -> success === false + + if (!data.success) { + + console.error(data.error); + + agregarMensaje( + "bot", + "Ocurrió un error al procesar la consulta." + ); + + return; + } + + // RESPUESTA IA + + agregarMensaje( + "bot", + data.respuesta + ); + + } catch (error) { + + // ERROR FETCH / CONEXIÓN + + removerTyping(); + + console.error(error); + + agregarMensaje( + "bot", + "Error de conexión." + ); + } + }); + + // Evita inyección HTML/XSS + + function escaparHTML(texto) + { + return texto + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + // AGREGAR MENSAJE VISUAL AL CHAT -> tipo: user / bot + + function agregarMensaje(tipo, texto) + { + + // CREAR DIV + + const div = document.createElement("div"); + + // CLASE CSS + + div.className = + tipo === "user" ? "mensaje-user" : "mensaje-bot"; + + div.innerHTML = ` +
+ ${escaparHTML(texto).replace(/\n/g, "
")} +
+ `; + + // INSERTAR MENSAJE + + chatBox.appendChild(div); + + // AUTO SCROLL + + scrollBottom(); + } + + // MOSTRAR TYPING + + function mostrarTyping() + { + const div = document.createElement("div"); + + div.id = "typing"; + + div.className = "mensaje-bot"; + + div.innerHTML = ` +
+ Escribiendo... +
+ `; + + chatBox.appendChild(div); + + scrollBottom(); + } + + // ELIMINAR TYPING + + function removerTyping() + { + const typing = document.getElementById("typing"); + + if (typing) { + typing.remove(); + } + } + + // AUTO SCROLL -> Baja automáticamente el scroll del chat + + function scrollBottom() + { + chatBox.scrollTop = chatBox.scrollHeight; + } +}); + +/* +EVENTO: load + +Se ejecuta cuando toda la página termina de cargar: +HTML + CSS + imágenes + recursos + +Se usa para restaurar el scroll abajo al refrescar la página +*/ + +window.addEventListener("load", () => { + + const chatBox = document.getElementById("chat-box"); + + if (chatBox) { + + chatBox.scrollTop = chatBox.scrollHeight; + } +}); \ No newline at end of file diff --git a/public/js/clientes/clienteTableLista.js b/public/js/clientes/clienteTableLista.js new file mode 100644 index 0000000..fd6065b --- /dev/null +++ b/public/js/clientes/clienteTableLista.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaClientes').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar cliente...", + zeroRecords: "No se encontraron clientes", + info: "Mostrando _START_ a _END_ de _TOTAL_ clientes", + infoEmpty: "No hay clientes para mostrar", + infoFiltered: "(filtrado de _MAX_ clientes totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center') + .append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/clientes/clienteValidator.js b/public/js/clientes/clienteValidator.js new file mode 100644 index 0000000..ead4861 --- /dev/null +++ b/public/js/clientes/clienteValidator.js @@ -0,0 +1,273 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-crear-cliente") || document.getElementById("form-editar-cliente"); + if (!form) return; + + // Funciones utilitarias + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function isValidEmail(email) { + return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email); + } + + function isValidName(value) { + return /^[A-ZÁÉÍÓÚÑ][a-záéíóúñ]+(?: [A-ZÁÉÍÓÚÑ][a-záéíóúñ]+)*$/.test(value); + } + + // Submit + + form.addEventListener("submit", (e) => { + + let errores = []; + + const nombre = form.querySelector("input[name='nombre']"); + const apellido = form.querySelector("input[name='apellido']"); + const telefono = form.querySelector("input[name='telefono']"); + const email = form.querySelector("input[name='email']"); + + // Limpiar estados previos + + form.querySelectorAll(".is-invalid").forEach(el => + el.classList.remove("is-invalid") + ); + + // NOMBRE + + if (nombre) { + if (isEmpty(nombre.value)) { + errores.push("El nombre es obligatorio."); + nombre.classList.add("is-invalid"); + } else if (!isValidName(nombre.value)) { + errores.push("El nombre debe comenzar con mayúscula y solo contener letras."); + nombre.classList.add("is-invalid"); + } else if (nombre.value.length < 2) { + errores.push("El nombre debe tener al menos 2 caracteres."); + nombre.classList.add("is-invalid"); + } else if (nombre.value.length > 50) { + errores.push("El nombre no puede superar los 50 caracteres."); + nombre.classList.add("is-invalid"); + } + } + + // APELLIDO + + if (apellido) { + if (isEmpty(apellido.value)) { + errores.push("El apellido es obligatorio."); + apellido.classList.add("is-invalid"); + } else if (!isValidName(apellido.value)) { + errores.push("El apellido debe comenzar con mayúscula y solo contener letras."); + apellido.classList.add("is-invalid"); + } else if (apellido.value.length < 2) { + errores.push("El apellido debe tener al menos 2 caracteres."); + apellido.classList.add("is-invalid"); + } else if (apellido.value.length > 50) { + errores.push("El apellido no puede superar los 50 caracteres."); + apellido.classList.add("is-invalid"); + } + } + + // TELÉFONO + + if (telefono) { + if (isEmpty(telefono.value)) { + errores.push("El teléfono es obligatorio."); + telefono.classList.add("is-invalid"); + } else if (telefono.value.length != 10) { + errores.push("El teléfono debe poseer 10 dígitos."); + telefono.classList.add("is-invalid"); + } + } + + // EMAIL + + if (email) { + if (isEmpty(email.value)) { + errores.push("El email es obligatorio."); + email.classList.add("is-invalid"); + } else if (!isValidEmail(email.value)) { + errores.push("El email no tiene un formato válido."); + email.classList.add("is-invalid"); + } else if (email.value.length > 100) { + errores.push("El email no puede superar los 100 caracteres."); + email.classList.add("is-invalid"); + } + } + + // CONDICIÓN IVA + + const condIVA = form.querySelector("select[name='cond_iva']"); + + const condicionesPermitidas = [ + "consumidor_final", + "monotributo", + "responsable_inscripto" + ]; + + if (condIVA) { + + if (!condicionesPermitidas.includes(condIVA.value)) { + + errores.push("Condición de IVA inválida."); + condIVA.classList.add("is-invalid"); + } + } + + // CUIT + + const cuit1 = form.querySelector("input[name='cuit_1']"); + const cuit2 = form.querySelector("input[name='cuit_2']"); + const cuit3 = form.querySelector("input[name='cuit_3']"); + + if (cuit1 && cuit2 && cuit3) { + + const cuit = cuit1.value.trim() + cuit2.value.trim() + cuit3.value.trim(); + + // Monotributo y Responsable Inscripto: + // CUIT obligatorio + + if (condIVA && condIVA.value !== "consumidor_final" && cuit === "") { + + errores.push("Debe ingresar el CUIT."); + + cuit1.classList.add("is-invalid"); + cuit2.classList.add("is-invalid"); + cuit3.classList.add("is-invalid"); + } + + // Si se ingresó algo, validar formato + + if (cuit !== "") { + + if (!/^\d{11}$/.test(cuit)) { + + errores.push( + "El CUIT debe contener exactamente 11 dígitos." + ); + + cuit1.classList.add("is-invalid"); + cuit2.classList.add("is-invalid"); + cuit3.classList.add("is-invalid"); + } + } + } + + // Resultado + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + } + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file diff --git a/public/js/configuracion/configuracionValidator.js b/public/js/configuracion/configuracionValidator.js new file mode 100644 index 0000000..98f3bfe --- /dev/null +++ b/public/js/configuracion/configuracionValidator.js @@ -0,0 +1,336 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-config-empresa"); + if (!form) return; + + // Funciones utilitarias + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function isValidNombre(value) { + return /^[A-Za-zÁÉÍÓÚáéíóú0-9\s.\-]{2,100}$/.test(value); + } + + function isValidTexto(value) { + return /^[A-Za-zÁÉÍÓÚáéíóúñÑ\s]{2,100}$/.test(value); + } + + function isValidEmail(value) { + return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value); + } + + function isValidTelefono(value) { + return /^[0-9]{10}$/.test(value); + } + + function isValidCP(value) { + return /^[0-9]{4}$/.test(value); + } + + function isValidCuit(value) { + return /^\d{10,11}$/.test(value); + } + + // Submit + + form.addEventListener("submit", (e) => { + + let errores = []; + + const nombre = form.querySelector("input[name='nombre']"); + const direccion = form.querySelector("input[name='direccion']"); + const ciudad = form.querySelector("input[name='ciudad']"); + const codigo_postal = form.querySelector("input[name='codigo_postal']"); + const provincia = form.querySelector("input[name='provincia']"); + const telefono = form.querySelector("input[name='telefono']"); + const email = form.querySelector("input[name='email']"); + const cuit = form.querySelector("input[name='cuit']"); + const iva = form.querySelector("input[name='iva']"); + const cargo = form.querySelector("input[name='cargo_cancelacion_servicio']"); + const email_emisor = form.querySelector("input[name='email_emisor']"); + + const apM = form.querySelector("input[name='hora_apertura_maniana']"); + const ciM = form.querySelector("input[name='hora_cierre_maniana']"); + const apT = form.querySelector("input[name='hora_apertura_tarde']"); + const ciT = form.querySelector("input[name='hora_cierre_tarde']"); + + // Limpiar estados previos + + form.querySelectorAll(".is-invalid").forEach(el => + el.classList.remove("is-invalid") + ); + + // NOMBRE + + if (nombre) { + if (isEmpty(nombre.value)) { + errores.push("El nombre es obligatorio."); + nombre.classList.add("is-invalid"); + } else if (!isValidNombre(nombre.value)) { + errores.push("El nombre contiene caracteres inválidos."); + nombre.classList.add("is-invalid"); + } else if (nombre.value.length < 2) { + errores.push("El nombre debe tener al menos 2 caracteres."); + nombre.classList.add("is-invalid"); + } else if (nombre.value.length > 50) { + errores.push("El nombre no puede superar los 50 caracteres."); + nombre.classList.add("is-invalid"); + } + } + + // DIRECCIÓN + + if (direccion) { + if (isEmpty(direccion.value)) { + errores.push("La dirección es obligatoria."); + direccion.classList.add("is-invalid"); + } else if (direccion.value.length < 3) { + errores.push("La dirección debe tener al menos 3 caracteres."); + direccion.classList.add("is-invalid"); + } else if (direccion.value.length > 100) { + errores.push("La dirección no puede superar los 100 caracteres."); + direccion.classList.add("is-invalid"); + } + } + + // CIUDAD + + if (ciudad) { + if (isEmpty(ciudad.value)) { + errores.push("La ciudad es obligatoria."); + ciudad.classList.add("is-invalid"); + } else if (!isValidTexto(ciudad.value)) { + errores.push("La ciudad solo puede contener letras y espacios."); + ciudad.classList.add("is-invalid"); + } + } + + // CÓDIGO POSTAL + + if (codigo_postal) { + if (isEmpty(codigo_postal.value)) { + errores.push("El código postal es obligatorio."); + codigo_postal.classList.add("is-invalid"); + } else if (!isValidCP(codigo_postal.value)) { + errores.push("El código postal debe tener 4 dígitos."); + codigo_postal.classList.add("is-invalid"); + } + } + + // PROVINCIA + + if (provincia) { + if (isEmpty(provincia.value)) { + errores.push("La provincia es obligatoria."); + provincia.classList.add("is-invalid"); + } else if (!isValidTexto(provincia.value)) { + errores.push("La provincia contiene caracteres inválidos."); + provincia.classList.add("is-invalid"); + } + } + + // TELÉFONO + + if (telefono) { + if (isEmpty(telefono.value)) { + errores.push("El teléfono es obligatorio."); + telefono.classList.add("is-invalid"); + } else if (!isValidTelefono(telefono.value)) { + errores.push("El teléfono debe tener 10 dígitos."); + telefono.classList.add("is-invalid"); + } + } + + // EMAIL + + if (email) { + if (isEmpty(email.value)) { + errores.push("El email es obligatorio."); + email.classList.add("is-invalid"); + } else if (!isValidEmail(email.value)) { + errores.push("El email no tiene un formato válido."); + email.classList.add("is-invalid"); + } else if (email.value.length > 100) { + errores.push("El email no puede superar los 100 caracteres."); + email.classList.add("is-invalid"); + } + } + + // CUIT + + if (cuit) { + if (!isValidCuit(cuit.value)) { + errores.push("CUIT inválido."); + cuit.classList.add("is-invalid"); + } + } + + // IVA + + if (iva) { + if (iva.value && isNaN(iva.value)) { + errores.push("El IVA debe ser numérico."); + iva.classList.add("is-invalid"); + } + } + + // CARGO CANCELACIÓN + + if (cargo) { + if (cargo.value && isNaN(cargo.value)) { + errores.push("El cargo por cancelación debe ser numérico."); + cargo.classList.add("is-invalid"); + } + } + + // EMAIL EMISOR + + if (email_emisor) { + if (isEmpty(email_emisor.value)) { + errores.push("El email emisor es obligatorio."); + email_emisor.classList.add("is-invalid"); + } else if (!isValidEmail(email_emisor.value)) { + errores.push("El email emisor no tiene un formato válido."); + email_emisor.classList.add("is-invalid"); + } else if (email_emisor.value.length > 100) { + errores.push("El email emisor no puede superar los 100 caracteres."); + email_emisor.classList.add("is-invalid"); + } + } + + // HORARIOS + + if (apM && ciM) { + if (apM.value && ciM.value && apM.value >= ciM.value) { + errores.push("El horario de apertura de la mañana debe ser menor al de cierre."); + apM.classList.add("is-invalid"); + ciM.classList.add("is-invalid"); + } + } + + if (apT && ciT) { + if (apT.value && ciT.value && apT.value >= ciT.value) { + errores.push("El horario de apertura de la tarde debe ser menor al de cierre."); + apT.classList.add("is-invalid"); + ciT.classList.add("is-invalid"); + } + } + + // Resultado + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + } + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file diff --git a/public/js/globalModal.js b/public/js/globalModal.js new file mode 100644 index 0000000..325659b --- /dev/null +++ b/public/js/globalModal.js @@ -0,0 +1,113 @@ +// Guarda referencia al form original (si existe) + +let formConfirmacion = null; + +// Acción dinámica a ejecutar (opt del backend) + +let action = null; + +// Objeto donde se copian todos los data-* del botón + +let dataset = {}; + +// Recorre todos los botones que disparan el modal + +document.querySelectorAll(".btnConfirmar").forEach(boton => { + + boton.addEventListener("click", function () { + + // Inserta el mensaje en el modal + + document.getElementById("modalMensaje").textContent = this.dataset.mensaje; + + // Inserta el título en el modal + + document.getElementById("modalTitulo").textContent = this.dataset.titulo; + + // Busca si el botón pertenece a un form existente + + formConfirmacion = this.closest("form"); + + // Obtiene la acción dinámica (si existe) + + action = this.dataset.action || null; + + // Copia TODOS los data-* del botón a un objeto JS + + dataset = { ...this.dataset }; + + }); +}); + +// Evento del botón "Confirmar" dentro del modal + +document.getElementById("modalBtnConfirmar").addEventListener("click", function (e) { + + // Evita el submit automático del botón + + e.preventDefault(); + + // CASO 1: FORM EXISTENTE + + // Si hay un form padre y no hay acción dinámica + + if (formConfirmacion && !action) { + + // Envía directamente ese form + + formConfirmacion.submit(); + return; + } + + // CASO 2: FORM DINÁMICO + + if (action) { + + // Form global del modal + + const form = document.getElementById("formConfirmacionGlobal"); + + // Contenedor donde se insertan inputs ocultos + + const container = document.getElementById("modalDynamicInputs"); + + // Define la URL de envío con el parámetro opt + + form.action = `index.php?opt=${action}`; + + // Limpia inputs anteriores para evitar duplicados + + container.innerHTML = ''; + + // Recorre todas las claves del dataset + + Object.keys(dataset).forEach(key => { + + // Ignora atributos que no deben enviarse + + if ([ + 'action', + 'mensaje', + 'titulo', + 'bsToggle', + 'bsTarget' + ].includes(key)) return; + + // Crea un input hidden por cada data-* + + const input = document.createElement('input'); + + input.type = 'hidden'; // oculto + input.name = key; // nombre (ej: mano_obra, etc) + input.value = dataset[key]; // valor correspondiente + + // Lo agrega al form + + container.appendChild(input); + }); + + // Envía el form dinámico + + form.submit(); + } +}); \ No newline at end of file diff --git a/public/js/informes.js b/public/js/informes.js new file mode 100644 index 0000000..b34cd3b --- /dev/null +++ b/public/js/informes.js @@ -0,0 +1,284 @@ +// Variable global para almacenar la instancia del gráfico, Chart.js no permite reutilizar el canvas sin destruir el anterior + +let grafico; + +// FUNCIÓN PARA ACTUALIZAR KPIS + +function actualizarKpis(kpis) { + + // KPI: INGRESOS + // Formateo a moneda argentina con separador de miles + + document.getElementById('kpiIngresos').innerText = '$' + kpis.totalIngresos.toLocaleString('es-AR'); + + // KPI: EGRESOS + + document.getElementById('kpiEgresos').innerText = '$' + kpis.totalCostos.toLocaleString('es-AR'); + + // KPI: RESULTADO NETO + + const resultadoEl = document.getElementById('kpiResultado'); + + resultadoEl.innerText = '$' + kpis.resultado.toLocaleString('es-AR'); + + // Se ajusta el color dinámicamente, verde si hay ganancia y rojo si hay pérdida + + resultadoEl.classList.remove('text-success', 'text-danger'); + resultadoEl.classList.add( + kpis.resultado >= 0 ? 'text-success' : 'text-danger' + ); + + // KPI: MEJOR MES (calculado desde backend) + + document.getElementById('kpiMejorMes').innerText = kpis.mejorMes; +} + +// FUNCIÓN PARA CARGAR BALANCE + +async function cargarBalance(anio) { + + // Llamada al backend (controller PHP), se pasa el año como parámetro + + const response = await fetch(`index.php?opt=generar_balance_anual&anio=${anio}`); + + // Convertimos la respuesta a JSON + + const responseJson = await response.json(); + + // TRANSFORMACIÓN DE DATOS + + // El backend devuelve los meses como números, acá se convierten a nombres abreviados + + const meses = responseJson.meses.map(m => { + const nombres = ["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"]; + return nombres[m - 1]; + }); + + // Parseo de datos, convertimos todos los valores a números (por seguridad) + + const ventas = responseJson.ventas.map(v => parseFloat(v)); + const servicios = responseJson.servicios.map(s => parseFloat(s)); + const costos = responseJson.costos.map(c => parseFloat(c)); + const utilidad = responseJson.utilidad.map(u => parseFloat(u)); + + // Obtenemos el contexto del canvas + + const ctx = document.getElementById('graficoBalance').getContext('2d'); + + // Destruir gráfico anterior + + if (grafico) { + grafico.destroy(); + } + + // CREAR NUEVO GRÁFICO + + grafico = new Chart(ctx, { + type: 'bar', // Tipo base (gráfico de barras) + data: { + labels: meses, // Etiquetas (meses) + datasets: [ + + // Dataset VENTAS + { + label: 'Ventas', + data: ventas + }, + + // Dataset SERVICIOS + { + label: 'Servicios', + data: servicios + }, + + // Dataset COSTOS + { + label: 'Costos', + data: costos + }, + + // Dataset UTILIDAD (linea) + { + label: 'Utilidad', + data: utilidad, + type: 'line' + } + ] + }, + + options: { + + // Hace que el gráfico se adapte automáticamente al tamanio del contenedor + responsive: true, + + // Permite mostrar todos los valores al pasar el mouse + interaction: { + mode: 'index', // Muestra todos los datos + intersect: false // No hace falta tocar la línea (con estar cerca ya muestra los datos) + }, + + plugins: { + + // Tooltip personalizado + tooltip: { + callbacks: { + label: function(context) { // CONTEXT: objeto que da Chart.js que contiene info del punto actual + return context.dataset.label + ': $' + context.parsed.y.toLocaleString('es-AR'); + } + } + } + }, + + scales: { + + // Eje Y (valores) + y: { + beginAtZero: true, // Fuerza a que el eje empiece desde cero + + // Formateo de valores como moneda + ticks: { + callback: function(value) { + return '$' + value.toLocaleString('es-AR'); + } + } + } + } + } + }); + + // Actualizar KPIs + + actualizarKpis(responseJson.kpis); +} + +// FUNCIÓN CARGAR DINERO A COBRAR + +async function cargarACobrar(anio) { + + // Llamada al backend + + const response = await fetch(`index.php?opt=generar_cuentas_por_cobrar&anio=${anio}`); + const responseJson = await response.json(); + + // Transformación de meses + + const meses = responseJson.meses.map(m => { + const nombres = ["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"]; + return nombres[m - 1]; + }); + + // Datos (totales a cobrar por mes) + + const totales = responseJson.totales.map(t => parseFloat(t)); + + const ctx = document.getElementById('graficoBalance').getContext('2d'); + + // Destruimos gráfico anterior + + if (grafico) { + grafico.destroy(); + } + + // Creamos nuevo gráfico + + grafico = new Chart(ctx, { + type: 'bar', // Del tipo barra + data: { + labels: meses, + datasets: [ + { + label: 'Dinero a Cobrar', + data: totales + } + ] + }, + + options: { + responsive: true, + interaction: { + mode: 'index', + intersect: false + }, + + plugins: { + tooltip: { + callbacks: { + label: function(context) { + return context.dataset.label + ': $' + context.parsed.y.toLocaleString('es-AR'); + } + } + } + }, + + scales: { + y: { + beginAtZero: true, + ticks: { + callback: function(value) { + return '$' + value.toLocaleString('es-AR'); + } + } + } + } + } + }); + + // KPIs SIMPLIFICADOS + + // Solo mostramos el total a cobrar + + document.getElementById('kpiIngresos').innerText = '$' + responseJson.kpis.totalACobrar.toLocaleString('es-AR'); + + // En este contexto los demas KPIs no aplican + + document.getElementById('kpiEgresos').innerText = '-'; + document.getElementById('kpiResultado').innerText = '-'; + document.getElementById('kpiMejorMes').innerText = '-'; +} + +// EVENTO: CAMBIO DE ANIO + +document.getElementById('anio').addEventListener('change', function() { + + // Se obtiene el tipo de informe actual + + const tipo = document.getElementById('tipoInforme').value; + + // Segú el tipo, se llama a la función correspondiente + + if (tipo === 'balance') { + cargarBalance(this.value); + } else { + cargarACobrar(this.value); + } +}); + +// EVENTO: CAMBIO DE TIPO DE INFORME + +document.getElementById('tipoInforme').addEventListener('change', function() { + + const anio = document.getElementById('anio').value; + + if (this.value === 'balance') { + + // Cambia el título dinámicamente + + document.getElementById('tituloInforme').innerHTML = ' Balance Anual'; + cargarBalance(anio); + + } else { + document.getElementById('tituloInforme').innerHTML = ' Dinero a Cobrar'; + cargarACobrar(anio); + } + +}); + +// INICIALIZACIÓN + +// Se obtiene el año actual automáticamente + +const anioInicial = new Date().getFullYear(); + +// Se carga el balance al inciar la vista + +cargarBalance(anioInicial); \ No newline at end of file diff --git a/public/js/login/logInValidator.js b/public/js/login/logInValidator.js new file mode 100644 index 0000000..9f4247a --- /dev/null +++ b/public/js/login/logInValidator.js @@ -0,0 +1,204 @@ +document.addEventListener("DOMContentLoaded", () => { + + // Mostrar errores del servidor si existen + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-login"); + const usuarioInput = document.querySelector("input[name='usuario']"); + const passInput = document.querySelector("input[name='password']"); + + if (!form || !usuarioInput || !passInput) return; + + // Protección básica de intentos (localStorage) + + function verificarIntentos() { + const ahora = Date.now(); + const intentosData = JSON.parse(localStorage.getItem("login_intentos")) || { c: 0, t: ahora }; + + if (ahora - intentosData.t > 60000) { + intentosData.c = 0; + intentosData.t = ahora; + } + + localStorage.setItem("login_intentos", JSON.stringify(intentosData)); + return intentosData; + } + + // Funciones utilitarias + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function hasNoSpaces(value) { + return !value.includes(' '); + } + + function isValidUsername(value) { + return /^(?=.*[a-zA-Z])(?!.*[._]{2})[a-zA-Z0-9._]{5,30}$/.test(value); + } + + function isValidPassword(value) { + return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{6,}$/.test(value); + } + + form.addEventListener("submit", (e) => { + let errores = []; + + const usuario = usuarioInput.value.trim(); + const password = passInput.value.trim(); + const intentos = verificarIntentos(); + + usuarioInput.classList.remove("is-invalid"); + passInput.classList.remove("is-invalid"); + + // INTENTOS (se verifica primero para cortar rápido) + + if (intentos.c >= 3) { + e.preventDefault(); + mostrarErrores(["Demasiados intentos. Espere 1 minuto antes de volver a intentar."]); + return; + } + + // USUARIO + + if (isEmpty(usuario)) { + errores.push("El usuario es obligatorio."); + usuarioInput.classList.add("is-invalid"); + } else if (!isValidUsername(usuario)) { + errores.push("El usuario debe tener 5–30 caracteres, incluir al menos una letra y solo usar letras, números, puntos o guiónes bajos no consecutivos."); + usuarioInput.classList.add("is-invalid"); + } + + // PASSWORD + + if (isEmpty(password)) { + errores.push("La contraseña es obligatoria."); + passInput.classList.add("is-invalid"); + } else if (!hasNoSpaces(password)) { + errores.push("La contraseña no puede contener espacios."); + passInput.classList.add("is-invalid"); + } else if (!isValidPassword(password)) { + errores.push("La contraseña debe tener al menos una mayúscula, una minúscula y un número, y mínimo 6 caracteres."); + passInput.classList.add("is-invalid"); + } + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + + intentos.c++; + localStorage.setItem("login_intentos", JSON.stringify(intentos)); + return; + } + + localStorage.removeItem("login_intentos"); + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } +}); \ No newline at end of file diff --git a/public/js/pedidos/pedidoTableDetalle.js b/public/js/pedidos/pedidoTableDetalle.js new file mode 100644 index 0000000..fe0318f --- /dev/null +++ b/public/js/pedidos/pedidoTableDetalle.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaDetallePedido').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: 0} + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar producto...", + zeroRecords: "No se encontraron productos", + info: "Mostrando _START_ a _END_ de _TOTAL_ productos", + infoEmpty: "No hay productos para mostrar", + infoFiltered: "(filtrado de _MAX_ productos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center') + .append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/pedidos/pedidoTableLista.js b/public/js/pedidos/pedidoTableLista.js new file mode 100644 index 0000000..fbe311e --- /dev/null +++ b/public/js/pedidos/pedidoTableLista.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaPedidos').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar pedido...", + zeroRecords: "No se encontraron pedidos", + info: "Mostrando _START_ a _END_ de _TOTAL_ pedidos", + infoEmpty: "No hay pedidos para mostrar", + infoFiltered: "(filtrado de _MAX_ pedidos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center') + .append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/pedidos/pedidoTableProducto.js b/public/js/pedidos/pedidoTableProducto.js new file mode 100644 index 0000000..83e0c3a --- /dev/null +++ b/public/js/pedidos/pedidoTableProducto.js @@ -0,0 +1,105 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaProductos').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 6, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: -1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar productos...", + zeroRecords: "No se encontraron productos", + info: "Mostrando _START_ a _END_ de _TOTAL_ productos", + infoEmpty: "No hay productos para mostrar", + infoFiltered: "(filtrado de _MAX_ productos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Mover buscador al bloque izquierdo + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); + + // (Para poder guardar todos los datos en caso de que el producto no esté visible) + + $('form').on('submit', function () { + + const form = this; + + // Eliminar hidden generados anteriormente + $(form).find('.dt-hidden').remove(); + + // Recorrer TODAS las filas del DataTable + table.rows().every(function () { + + const row = $(this.node()); + const checkbox = row.find('input[name="ids[]"]'); + + if (checkbox.is(':checked')) { + + const id = checkbox.val(); + const cantidad = row.find('input[name="cantidades[' + id + ']"]').val(); + const costo = row.find('input[name="costos[' + id + ']"]').val(); + + // ID + $('') + .attr('type', 'hidden') + .attr('name', 'ids[]') + .val(id) + .addClass('dt-hidden') + .appendTo(form); + + // Cantidad + $('') + .attr('type', 'hidden') + .attr('name', 'cantidades[' + id + ']') + .val(cantidad) + .addClass('dt-hidden') + .appendTo(form); + + // Costo + $('') + .attr('type', 'hidden') + .attr('name', 'costos[' + id + ']') + .val(costo) + .addClass('dt-hidden') + .appendTo(form); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/pedidos/pedidoTableProveedor.js b/public/js/pedidos/pedidoTableProveedor.js new file mode 100644 index 0000000..320ce34 --- /dev/null +++ b/public/js/pedidos/pedidoTableProveedor.js @@ -0,0 +1,58 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaProveedores').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar proveedores...", + zeroRecords: "No se encontraron proveedores", + info: "Mostrando _START_ a _END_ de _TOTAL_ proveedores", + infoEmpty: "No hay proveedores para mostrar", + infoFiltered: "(filtrado de _MAX_ proveedores totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/pedidos/pedidoValidator.js b/public/js/pedidos/pedidoValidator.js new file mode 100644 index 0000000..c7ca6a9 --- /dev/null +++ b/public/js/pedidos/pedidoValidator.js @@ -0,0 +1,110 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file diff --git a/public/js/presupuestos/presupuestoTableCliente.js b/public/js/presupuestos/presupuestoTableCliente.js new file mode 100644 index 0000000..ce111d7 --- /dev/null +++ b/public/js/presupuestos/presupuestoTableCliente.js @@ -0,0 +1,58 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaClientes').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar clientes...", + zeroRecords: "No se encontraron clientes", + info: "Mostrando _START_ a _END_ de _TOTAL_ clientes", + infoEmpty: "No hay clientes para mostrar", + infoFiltered: "(filtrado de _MAX_ clientes totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/presupuestos/presupuestoTableProducto.js b/public/js/presupuestos/presupuestoTableProducto.js new file mode 100644 index 0000000..5b52f55 --- /dev/null +++ b/public/js/presupuestos/presupuestoTableProducto.js @@ -0,0 +1,83 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaProductos').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 6, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: [- 1, - 2] } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar productos...", + zeroRecords: "No se encontraron productos", + info: "Mostrando _START_ a _END_ de _TOTAL_ productos", + infoEmpty: "No hay productos para mostrar", + infoFiltered: "(filtrado de _MAX_ productos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Mover buscador al bloque izquierdo + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); + + // SUBMIT (problema de checkbox) + + $('form').on('submit', function () { + + const form = this; + + // Obtener TODOS los checkboxes marcados del DataTable (no solo los visibles) + + const checked = table.$('input[name="ids[]"]:checked'); + + // Eliminar cualquier ids[] previo agregado dinámicamente + + $(form).find('input[name="ids[]"][type="hidden"]').remove(); + + // Agregar hidden inputs reales al form + + checked.each(function () { + $('') + .attr('type', 'hidden') + .attr('name', 'ids[]') + .val($(this).val()) + .appendTo(form); + }); + }); +}); \ No newline at end of file diff --git a/public/js/presupuestos/presupuestoValidator.js b/public/js/presupuestos/presupuestoValidator.js new file mode 100644 index 0000000..f72bc12 --- /dev/null +++ b/public/js/presupuestos/presupuestoValidator.js @@ -0,0 +1,191 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-crear-presupuesto"); + if (!form) return; + + // Utilidades + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function toFloat(value) { + return parseFloat(value) || 0; + } + + // Submit + + form.addEventListener("submit", (e) => { + + let errores = []; + + const condIva = form.querySelector("[name='cond_iva']"); + const cuit = form.querySelector("[name='cuit']"); + const formaPago = form.querySelector("[name='forma_pago']"); + const tipoPago = form.querySelector("[name='tipo_pago']"); + const descuento = form.querySelector("[name='descuento']"); + const recargo = form.querySelector("[name='recargo']"); + + // limpiar estados previos + + form.querySelectorAll(".is-invalid").forEach(el => + el.classList.remove("is-invalid") + ); + + // FORMA Y TIPO DE PAGO + + const reglasPago = { + 'efectivo': ['pago_unico'], + 'transferencia': ['pago_unico'], + 'echeq': ['pago_unico'], + 'debito': ['pago_unico'], + 'credito': ['pago_unico', '3_cuotas', '6_cuotas', '12_cuotas'], + }; + + if (formaPago && tipoPago) { + + const tiposValidos = reglasPago[formaPago.value] || []; + + if (!tiposValidos.includes(tipoPago.value)) { + errores.push("Combinación de forma y tipo de pago inválida."); + formaPago.classList.add("is-invalid"); + tipoPago.classList.add("is-invalid"); + } + } + + // DESCUENTO / RECARGO + + if (descuento) { + const d = toFloat(descuento.value); + + if (d < 0 || d > 100) { + errores.push("El descuento debe estar entre 0 y 100%."); + descuento.classList.add("is-invalid"); + } + } + + if (recargo) { + const r = toFloat(recargo.value); + + if (r < 0 || r > 100) { + errores.push("El recargo debe estar entre 0 y 100%."); + recargo.classList.add("is-invalid"); + } + } + + // Resultado + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + } + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file diff --git a/public/js/productos/categoriaTableLista.js b/public/js/productos/categoriaTableLista.js new file mode 100644 index 0000000..f680923 --- /dev/null +++ b/public/js/productos/categoriaTableLista.js @@ -0,0 +1,58 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaCategorias').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar categorías...", + zeroRecords: "No se encontraron categorías", + info: "Mostrando _START_ a _END_ de _TOTAL_ categorías", + infoEmpty: "No hay categorías para mostrar", + infoFiltered: "(filtrado de _MAX_ categorías totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/productos/categoriaValidator.js b/public/js/productos/categoriaValidator.js new file mode 100644 index 0000000..7b8c951 --- /dev/null +++ b/public/js/productos/categoriaValidator.js @@ -0,0 +1,211 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-crear-categoria") || document.getElementById("form-editar-categoria"); + if (!form) return; + + // Utilidades + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function isValidName(value) { + return /^[A-Za-zÁÉÍÓÚÑáéíóúñ0-9]+(?: [A-Za-zÁÉÍÓÚÑáéíóúñ0-9]+)*$/.test(value); + } + + function isValidNumero(value) { + return /^\d{1,3}(\.\d{1,2})?$/.test(value); + } + + // Submit + + form.addEventListener("submit", (e) => { + + let errores = []; + + const nombre = form.querySelector("input[name='nombre']"); + const ganancia = form.querySelector("input[name='porcentaje_ganancia']"); + const descuento = form.querySelector("input[name='descuento']"); + + // limpiar errores previos + + form.querySelectorAll(".is-invalid").forEach(el => + el.classList.remove("is-invalid") + ); + + // NOMBRE + + if (nombre) { + if (isEmpty(nombre.value)) { + errores.push("El nombre es obligatorio."); + nombre.classList.add("is-invalid"); + } else if (!isValidName(nombre.value)) { + errores.push("El nombre solo puede contener letras, números y espacios."); + nombre.classList.add("is-invalid"); + } else if (nombre.value.length < 2) { + errores.push("El nombre debe tener al menos 2 caracteres."); + nombre.classList.add("is-invalid"); + } else if (nombre.value.length > 50) { + errores.push("El nombre no puede superar los 50 caracteres."); + nombre.classList.add("is-invalid"); + } + } + + // GANANCIA (opcional) + + if (ganancia) { + const value = ganancia.value.trim(); + + if (value !== '') { + + if (!isValidNumero(value)) { + errores.push("La ganancia debe ser un número válido con hasta 2 decimales."); + ganancia.classList.add("is-invalid"); + } else { + const num = Number(value); + + if (num <= 0 || num > 100) { + errores.push("La ganancia debe ser mayor a 0 y hasta 100."); + ganancia.classList.add("is-invalid"); + } + } + } + } + + // DESCUENTO (opcional) + + if (descuento) { + const value = descuento.value.trim(); + + if (value !== '') { + + if (!isValidNumero(value)) { + errores.push("El descuento debe ser un número válido con hasta 2 decimales."); + descuento.classList.add("is-invalid"); + } else { + const num = Number(value); + + if (num < 0 || num > 100) { + errores.push("El descuento debe estar entre 0 y 100."); + descuento.classList.add("is-invalid"); + } + } + } + } + + // Resultado + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + } + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file diff --git a/public/js/productos/productoTableLista.js b/public/js/productos/productoTableLista.js new file mode 100644 index 0000000..477bb13 --- /dev/null +++ b/public/js/productos/productoTableLista.js @@ -0,0 +1,58 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaProductos').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar productos...", + zeroRecords: "No se encontraron productos", + info: "Mostrando _START_ a _END_ de _TOTAL_ productos", + infoEmpty: "No hay productos para mostrar", + infoFiltered: "(filtrado de _MAX_ productos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/productos/productoTableListaInactivos.js b/public/js/productos/productoTableListaInactivos.js new file mode 100644 index 0000000..720ca46 --- /dev/null +++ b/public/js/productos/productoTableListaInactivos.js @@ -0,0 +1,58 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaProductosInactivos').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar productos...", + zeroRecords: "No se encontraron productos", + info: "Mostrando _START_ a _END_ de _TOTAL_ productos", + infoEmpty: "No hay productos para mostrar", + infoFiltered: "(filtrado de _MAX_ productos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/productos/productoValidator.js b/public/js/productos/productoValidator.js new file mode 100644 index 0000000..e668da6 --- /dev/null +++ b/public/js/productos/productoValidator.js @@ -0,0 +1,276 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-crear-producto") || document.getElementById("form-editar-producto"); + if (!form) return; + + // Desactivar campo descuento si el campo tipo es "servicio" + + const selectTipo = form.querySelector("[name='tipo']"); + const inputDescuento = form.querySelector("input[name='descuento']"); + + const TIPO_SERVICIO_ID = "2"; // ID de tipo "servicio" + + let descuentoPrevio = ''; + + function toggleDescuento() { + if (!selectTipo || !inputDescuento) return; + + if (selectTipo.value === TIPO_SERVICIO_ID) { + + // Guardar valor actual antes de limpiar + descuentoPrevio = inputDescuento.value; + + inputDescuento.value = ''; + inputDescuento.readOnly = true; + inputDescuento.classList.add('campo-readonly'); + + } else { + + inputDescuento.readOnly = false; + inputDescuento.classList.remove('campo-readonly'); + + // Restaurar si había algo antes + if (descuentoPrevio !== '') { + inputDescuento.value = descuentoPrevio; + } + } + } + + // Evento change + + if (selectTipo) { + selectTipo.addEventListener("change", toggleDescuento); + } + + // Ejecutar al cargar + + toggleDescuento(); + + // Utilidades + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function isValidMarca(value) { + return /^[A-Za-zÁÉÍÓÚÑáéíóúñ0-9]+(?: [A-Za-zÁÉÍÓÚÑáéíóúñ0-9]+)*$/.test(value); + } + + function isValidModelo(value) { + return /^[A-Za-zÁÉÍÓÚÑáéíóúñ0-9.\-]+(?: [A-Za-zÁÉÍÓÚÑáéíóúñ0-9.\-]+)*$/.test(value); + } + + function isValidNumero(value) { + return /^\d{1,3}(\.\d{1,2})?$/.test(value); + } + + // Submit + + form.addEventListener("submit", (e) => { + + let errores = []; + + const marca = form.querySelector("input[name='marca']"); + const modelo = form.querySelector("input[name='modelo']"); + const descuento = form.querySelector("input[name='descuento']"); + const categoria = form.querySelector("[name='categoria']"); + const tipo = form.querySelector("[name='tipo']"); + + // Limpiar errores previos + + form.querySelectorAll(".is-invalid").forEach(el => + el.classList.remove("is-invalid") + ); + + // MARCA + + if (marca) { + if (isEmpty(marca.value)) { + errores.push("La marca es obligatoria."); + marca.classList.add("is-invalid"); + } else if (!isValidMarca(marca.value)) { + errores.push("La marca solo puede contener letras, números y espacios."); + marca.classList.add("is-invalid"); + } else if (marca.value.length < 2) { + errores.push("La marca debe tener al menos 2 caracteres."); + marca.classList.add("is-invalid"); + } else if (marca.value.length > 50) { + errores.push("La marca no puede superar los 50 caracteres."); + marca.classList.add("is-invalid"); + } + } + + // MODELO + + if (modelo) { + if (isEmpty(modelo.value)) { + errores.push("El modelo es obligatorio."); + modelo.classList.add("is-invalid"); + } else if (!isValidModelo(modelo.value)) { + errores.push("El modelo solo puede contener letras, números, espacios, guiones y puntos."); + modelo.classList.add("is-invalid"); + } else if (modelo.value.length < 2) { + errores.push("El modelo debe tener al menos 2 caracteres."); + modelo.classList.add("is-invalid"); + } else if (modelo.value.length > 50) { + errores.push("El modelo no puede superar los 50 caracteres."); + modelo.classList.add("is-invalid"); + } + } + + // DESCUENTO + + if (descuento && tipo && tipo.value !== TIPO_SERVICIO_ID) { + + const value = descuento.value.trim(); + + if (value !== '') { + + if (!isValidNumero(value)) { + errores.push("El descuento debe ser un número válido con hasta 2 decimales."); + descuento.classList.add("is-invalid"); + } else { + const num = Number(value); + + if (num < 0 || num > 100) { + errores.push("El descuento debe estar entre 0 y 100."); + descuento.classList.add("is-invalid"); + } + } + } + } + + // CATEGORIA + + if (categoria) { + if (isEmpty(categoria.value)) { + errores.push("La categoría es obligatoria."); + categoria.classList.add("is-invalid"); + } + } + + // TIPO + + if (tipo) { + if (isEmpty(tipo.value)) { + errores.push("El tipo es obligatorio."); + tipo.classList.add("is-invalid"); + } + } + + // Resultado + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + } + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file diff --git a/public/js/proveedores/proveedorTableLista.js b/public/js/proveedores/proveedorTableLista.js new file mode 100644 index 0000000..3533637 --- /dev/null +++ b/public/js/proveedores/proveedorTableLista.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaProveedores').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar proveedor...", + zeroRecords: "No se encontraron proveedores", + info: "Mostrando _START_ a _END_ de _TOTAL_ proveedores", + infoEmpty: "No hay proveedores para mostrar", + infoFiltered: "(filtrado de _MAX_ proveedores totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center') + .append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/proveedores/proveedorValidator.js b/public/js/proveedores/proveedorValidator.js new file mode 100644 index 0000000..2e4315d --- /dev/null +++ b/public/js/proveedores/proveedorValidator.js @@ -0,0 +1,254 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-crear-proveedor") || document.getElementById("form-editar-proveedor"); + if (!form) return; + + // Funciones utilitarias + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function isValidEmail(email) { + return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email); + } + + function isValidName(value) { + return /^[A-Za-zÁÉÍÓÚÑáéíóúñ0-9&',.\-]+(?: [A-Za-zÁÉÍÓÚÑáéíóúñ0-9&',.\-]+)*(?: (S\.A\.|S\.R\.L\.|S\.C\.|S\.C\.P\.|S\.A\.S\.))?$/.test(value); + } + + // Submit + + form.addEventListener("submit", (e) => { + + let errores = []; + + const razon_social = form.querySelector("input[name='razon_social']"); + const telefono = form.querySelector("input[name='telefono']"); + const email = form.querySelector("input[name='email']"); + + // Limpiar estados previos + + form.querySelectorAll(".is-invalid").forEach(el => + el.classList.remove("is-invalid") + ); + + // RAZÓN SOCIAL + + if (razon_social) { + if (isEmpty(razon_social.value)) { + errores.push("La razón social es obligatorio."); + razon_social.classList.add("is-invalid"); + } else if (!isValidName(razon_social.value)) { + errores.push("La razón social contiene caracteres inválidos o la abreviatura legal final no es correcta."); + razon_social.classList.add("is-invalid"); + } else if (razon_social.value.length < 2) { + errores.push("La razón social debe tener al menos 2 caracteres."); + razon_social.classList.add("is-invalid"); + } else if (razon_social.value.length > 50) { + errores.push("La razón social no puede superar los 50 caracteres."); + razon_social.classList.add("is-invalid"); + } + } + + // TELÉFONO + + if (telefono) { + if (isEmpty(telefono.value)) { + errores.push("El teléfono es obligatorio."); + telefono.classList.add("is-invalid"); + } else if (telefono.value.length != 10) { + errores.push("El teléfono debe poseer 10 dígitos."); + telefono.classList.add("is-invalid"); + } + } + + // EMAIL + + if (email) { + if (isEmpty(email.value)) { + errores.push("El email es obligatorio."); + email.classList.add("is-invalid"); + } else if (!isValidEmail(email.value)) { + errores.push("El email no tiene un formato válido."); + email.classList.add("is-invalid"); + } else if (email.value.length > 100) { + errores.push("El email no puede superar los 100 caracteres."); + email.classList.add("is-invalid"); + } + } + + // CONDICIÓN IVA + + const condIVA = form.querySelector("select[name='cond_iva']"); + + const condicionesPermitidas = [ + "consumidor_final", + "monotributo", + "responsable_inscripto" + ]; + + if (condIVA) { + + if (!condicionesPermitidas.includes(condIVA.value)) { + + errores.push("Condición de IVA inválida."); + condIVA.classList.add("is-invalid"); + } + } + + // CUIT + + const cuit1 = form.querySelector("input[name='cuit_1']"); + const cuit2 = form.querySelector("input[name='cuit_2']"); + const cuit3 = form.querySelector("input[name='cuit_3']"); + + if (cuit1 && cuit2 && cuit3) { + + const cuit = cuit1.value.trim() + cuit2.value.trim() + cuit3.value.trim(); + + // Monotributo y Responsable Inscripto: + // CUIT obligatorio + + if (condIVA && condIVA.value !== "consumidor_final" && cuit === "") { + + errores.push("Debe ingresar el CUIT."); + + cuit1.classList.add("is-invalid"); + cuit2.classList.add("is-invalid"); + cuit3.classList.add("is-invalid"); + } + + // Si se ingresó algo, validar formato + + if (cuit !== "") { + + if (!/^\d{11}$/.test(cuit)) { + + errores.push( + "El CUIT debe contener exactamente 11 dígitos." + ); + + cuit1.classList.add("is-invalid"); + cuit2.classList.add("is-invalid"); + cuit3.classList.add("is-invalid"); + } + } + } + + // Resultado + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + } + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file diff --git a/public/js/servicios/servicioTableCliente.js b/public/js/servicios/servicioTableCliente.js new file mode 100644 index 0000000..ce111d7 --- /dev/null +++ b/public/js/servicios/servicioTableCliente.js @@ -0,0 +1,58 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaClientes').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar clientes...", + zeroRecords: "No se encontraron clientes", + info: "Mostrando _START_ a _END_ de _TOTAL_ clientes", + infoEmpty: "No hay clientes para mostrar", + infoFiltered: "(filtrado de _MAX_ clientes totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/servicios/servicioTableDetalleInsumos.js b/public/js/servicios/servicioTableDetalleInsumos.js new file mode 100644 index 0000000..3488af9 --- /dev/null +++ b/public/js/servicios/servicioTableDetalleInsumos.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaDetalleVenta').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false} + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar insumo...", + zeroRecords: "No se encontraron insumos", + info: "Mostrando _START_ a _END_ de _TOTAL_ insumos", + infoEmpty: "No hay insumos para mostrar", + infoFiltered: "(filtrado de _MAX_ insumos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center') + .append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/servicios/servicioTableInsumo.js b/public/js/servicios/servicioTableInsumo.js new file mode 100644 index 0000000..efd9040 --- /dev/null +++ b/public/js/servicios/servicioTableInsumo.js @@ -0,0 +1,83 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaInsumos').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 6, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: [- 1, - 2] } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar insumo...", + zeroRecords: "No se encontraron insumos", + info: "Mostrando _START_ a _END_ de _TOTAL_ insumos", + infoEmpty: "No hay insumos para mostrar", + infoFiltered: "(filtrado de _MAX_ insumos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Mover buscador al bloque izquierdo + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); + + // SUBMIT (problema de checkbox) + + $('form').on('submit', function () { + + const form = this; + + // Obtener TODOS los checkboxes marcados del DataTable (no solo los visibles) + + const checked = table.$('input[name="ids[]"]:checked'); + + // Eliminar cualquier ids[] previo agregado dinámicamente + + $(form).find('input[name="ids[]"][type="hidden"]').remove(); + + // Agregar hidden inputs reales al form + + checked.each(function () { + $('') + .attr('type', 'hidden') + .attr('name', 'ids[]') + .val($(this).val()) + .appendTo(form); + }); + }); +}); \ No newline at end of file diff --git a/public/js/servicios/servicioTableLista.js b/public/js/servicios/servicioTableLista.js new file mode 100644 index 0000000..c9dc73d --- /dev/null +++ b/public/js/servicios/servicioTableLista.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaServicios').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar servicio...", + zeroRecords: "No se encontraron servicios", + info: "Mostrando _START_ a _END_ de _TOTAL_ servicios", + infoEmpty: "No hay servicios para mostrar", + infoFiltered: "(filtrado de _MAX_ servicios totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center') + .append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/servicios/servicioValidator.js b/public/js/servicios/servicioValidator.js new file mode 100644 index 0000000..67d8840 --- /dev/null +++ b/public/js/servicios/servicioValidator.js @@ -0,0 +1,225 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-crear-servicio"); + if (!form) return; + + // Funciones utilitarias + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function isValidEmail(value) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); + } + + // Submit + + form.addEventListener("submit", (e) => { + + let errores = []; + + const descripcionEquipo = form.querySelector("input[name='descripcion_equipo']"); + const contraseniaEquipo = form.querySelector("input[name='contrasenia_equipo']"); + const cuenta = form.querySelector("input[name='cuenta']"); + const contraseniaCuenta = form.querySelector("input[name='contrasenia_cuenta']"); + const descripcionProblema = form.querySelector("textarea[name='descripcion_problema']"); + const observacion = form.querySelector("textarea[name='observacion']"); + + // Limpiar estados previos + + form.querySelectorAll(".is-invalid").forEach(el => + el.classList.remove("is-invalid") + ); + + // DESCRIPCIÓN DEL EQUIPO + + if (descripcionEquipo) { + if (isEmpty(descripcionEquipo.value)) { + errores.push("La descripción del equipo es obligatoria."); + descripcionEquipo.classList.add("is-invalid"); + } else if (descripcionEquipo.value.length > 100) { + errores.push("La descripción del equipo es demasiado larga."); + descripcionEquipo.classList.add("is-invalid"); + } + } + + // DESCRIPCIÓN DEL PROBLEMA + + if (descripcionProblema) { + if (isEmpty(descripcionProblema.value)) { + errores.push("Debe describir el problema."); + descripcionProblema.classList.add("is-invalid"); + } else if (descripcionProblema.value.length > 2000) { + errores.push("La descripción del problema es demasiado larga."); + descripcionProblema.classList.add("is-invalid"); + } + } + + // CONTRASENIA EQUIPO + + if (contraseniaEquipo) { + if (contraseniaEquipo.value.length > 100) { + errores.push("Contraseña demasiado larga."); + contraseniaEquipo.classList.add("is-invalid"); + } + + if (!isEmpty(contraseniaEquipo.value) && isEmpty(descripcionEquipo.value)) { + errores.push("Debe indicar el equipo si ingresa contraseña."); + descripcionEquipo.classList.add("is-invalid"); + } + } + + // CUENTA + + if (cuenta) { + if (cuenta.value.length > 100) { + errores.push("Cuenta demasiado larga."); + cuenta.classList.add("is-invalid"); + } + + if (!isEmpty(cuenta.value) && !isValidEmail(cuenta.value)) { + errores.push("El email de la cuenta no es válido."); + cuenta.classList.add("is-invalid"); + } + } + + // CONTRASENIA CUENTA + + if (contraseniaCuenta) { + if (contraseniaCuenta.value.length > 100) { + errores.push("Contraseña de cuenta demasiado larga."); + contraseniaCuenta.classList.add("is-invalid"); + } + + if (!isEmpty(contraseniaCuenta.value) && isEmpty(cuenta.value)) { + errores.push("Debe indicar la cuenta si ingresa contraseña."); + cuenta.classList.add("is-invalid"); + } + } + + // OBSERVACIÓN + + if (observacion) { + if (observacion.value.length > 500) { + errores.push("La descripción de la observación es demasiada larga."); + observacion.classList.add("is-invalid"); + } + } + + // Resultado + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + } + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file diff --git a/public/js/usuarios/usuarioTableLista.js b/public/js/usuarios/usuarioTableLista.js new file mode 100644 index 0000000..14bdd31 --- /dev/null +++ b/public/js/usuarios/usuarioTableLista.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaUsuarios').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar proveedor...", + zeroRecords: "No se encontraron proveedores", + info: "Mostrando _START_ a _END_ de _TOTAL_ proveedores", + infoEmpty: "No hay proveedores para mostrar", + infoFiltered: "(filtrado de _MAX_ usuarios totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center') + .append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/usuarios/usuarioValidator.js b/public/js/usuarios/usuarioValidator.js new file mode 100644 index 0000000..d46a0c8 --- /dev/null +++ b/public/js/usuarios/usuarioValidator.js @@ -0,0 +1,256 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-crear-usuario") || document.getElementById("form-editar-usuario"); + if (!form) return; + + // Si no hay submit (operador), no validar + + if (!form.querySelector("button[type='submit']")) return; + + // Funciones utilitarias + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function hasNoSpaces(value) { + return !value.includes(' '); + } + + function isValidEmail(email) { + return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email); + } + + function isValidName(value) { + return /^[A-ZÁÉÍÓÚÑ][a-záéíóúñ]+(?: [A-ZÁÉÍÓÚÑ][a-záéíóúñ]+)*$/.test(value); + } + + function isValidUsername(value) { + return /^(?=.*[a-zA-Z])(?!.*[._]{2})[a-zA-Z0-9._]{5,30}$/.test(value); + } + + function isValidPassword(value) { + return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{6,}$/.test(value); + } + + // Submit + + form.addEventListener("submit", (e) => { + + let errores = []; + + const nombre = form.querySelector("input[name='nombre']"); + const apellido = form.querySelector("input[name='apellido']"); + const usuario = form.querySelector("input[name='usuario']"); + const telefono = form.querySelector("input[name='telefono']"); + const email = form.querySelector("input[name='email']"); + const password = form.querySelector("input[name='password']"); + + // Limpiar estados previos + + form.querySelectorAll(".is-invalid").forEach(el => + el.classList.remove("is-invalid") + ); + + // NOMBRE + + if (nombre) { + if (isEmpty(nombre.value)) { + errores.push("El nombre es obligatorio."); + nombre.classList.add("is-invalid"); + } else if (!isValidName(nombre.value)) { + errores.push("El nombre debe comenzar con mayúscula y solo contener letras."); + nombre.classList.add("is-invalid"); + } else if (nombre.value.length < 2) { + errores.push("El nombre debe tener al menos 2 caracteres."); + nombre.classList.add("is-invalid"); + } else if (nombre.value.length > 50) { + errores.push("El nombre no puede superar los 50 caracteres."); + nombre.classList.add("is-invalid"); + } + } + + // APELLIDO + + if (apellido) { + if (isEmpty(apellido.value)) { + errores.push("El apellido es obligatorio."); + apellido.classList.add("is-invalid"); + } else if (!isValidName(apellido.value)) { + errores.push("El apellido debe comenzar con mayúscula y solo contener letras."); + apellido.classList.add("is-invalid"); + } else if (apellido.value.length < 2) { + errores.push("El apellido debe tener al menos 2 caracteres."); + apellido.classList.add("is-invalid"); + } else if (apellido.value.length > 50) { + errores.push("El apellido no puede superar los 50 caracteres."); + apellido.classList.add("is-invalid"); + } + } + + // USUARIO + + if (isEmpty(usuario)) { + errores.push("El usuario es obligatorio."); + usuarioInput.classList.add("is-invalid"); + } else if (!isValidUsername(usuario)) { + errores.push("El usuario debe tener 5–30 caracteres, incluir al menos una letra y solo usar letras, números, puntos o guiónes bajos no consecutivos."); + usuarioInput.classList.add("is-invalid"); + } + + // TELÉFONO + + if (telefono) { + if (isEmpty(telefono.value)) { + errores.push("El teléfono es obligatorio."); + telefono.classList.add("is-invalid"); + } else if (telefono.value.length != 10) { + errores.push("El teléfono debe poseer 10 dígitos."); + telefono.classList.add("is-invalid"); + } + } + + // EMAIL + + if (email) { + if (isEmpty(email.value)) { + errores.push("El email es obligatorio."); + email.classList.add("is-invalid"); + } else if (!isValidEmail(email.value)) { + errores.push("El email no tiene un formato válido."); + email.classList.add("is-invalid"); + } else if (email.value.length > 100) { + errores.push("El email no puede superar los 100 caracteres."); + email.classList.add("is-invalid"); + } + } + + // PASSWORD + + if (isEmpty(password)) { + errores.push("La contraseña es obligatoria."); + passInput.classList.add("is-invalid"); + } else if (!hasNoSpaces(password)) { + errores.push("La contraseña no puede contener espacios."); + passInput.classList.add("is-invalid"); + } else if (!isValidPassword(password)) { + errores.push("La contraseña debe tener al menos una mayúscula, una minúscula y un número, y mínimo 6 caracteres."); + passInput.classList.add("is-invalid"); + } + + // Resultado + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + } + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file diff --git a/public/js/ventas/ventaTableCliente.js b/public/js/ventas/ventaTableCliente.js new file mode 100644 index 0000000..ce111d7 --- /dev/null +++ b/public/js/ventas/ventaTableCliente.js @@ -0,0 +1,58 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaClientes').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar clientes...", + zeroRecords: "No se encontraron clientes", + info: "Mostrando _START_ a _END_ de _TOTAL_ clientes", + infoEmpty: "No hay clientes para mostrar", + infoFiltered: "(filtrado de _MAX_ clientes totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/ventas/ventaTableDetalle.js b/public/js/ventas/ventaTableDetalle.js new file mode 100644 index 0000000..1796967 --- /dev/null +++ b/public/js/ventas/ventaTableDetalle.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaDetalleVenta').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false} + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar producto...", + zeroRecords: "No se encontraron productos", + info: "Mostrando _START_ a _END_ de _TOTAL_ productos", + infoEmpty: "No hay productos para mostrar", + infoFiltered: "(filtrado de _MAX_ productos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center') + .append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/ventas/ventaTableLista.js b/public/js/ventas/ventaTableLista.js new file mode 100644 index 0000000..d3162ee --- /dev/null +++ b/public/js/ventas/ventaTableLista.js @@ -0,0 +1,56 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaVentas').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 7, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: - 1 } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar venta...", + zeroRecords: "No se encontraron ventas", + info: "Mostrando _START_ a _END_ de _TOTAL_ ventas", + infoEmpty: "No hay ventas para mostrar", + infoFiltered: "(filtrado de _MAX_ ventas totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Insertamos el select al lado del buscador + + $('.dataTables_filter') + .addClass('d-flex align-items-center') + .append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); +}); \ No newline at end of file diff --git a/public/js/ventas/ventaTableProducto.js b/public/js/ventas/ventaTableProducto.js new file mode 100644 index 0000000..488ac70 --- /dev/null +++ b/public/js/ventas/ventaTableProducto.js @@ -0,0 +1,83 @@ +document.addEventListener('DOMContentLoaded', function () { + + const table = $('#tablaProductos').DataTable({ + paging: true, + searching: true, + ordering: true, + pageLength: 6, + lengthChange: false, + info: true, + autoWidth: false, + + columnDefs: [ + { orderable: false, targets: [-1, -2] } + ], + + dom: + "<'row mb-3'<'col-md-8'f><'col-md-4 text-end'l>>" + + "<'row'<'col-12'tr>>" + + "<'row mt-3'<'col-md-5'i><'col-md-7'p>>", + + language: { + search: "", + searchPlaceholder: "Buscar productos...", + zeroRecords: "No se encontraron productos", + info: "Mostrando _START_ a _END_ de _TOTAL_ productos", + infoEmpty: "No hay productos para mostrar", + infoFiltered: "(filtrado de _MAX_ productos totales)", + paginate: { + next: "›", + previous: "‹" + } + } + }); + + // Mover buscador al bloque izquierdo + + $('.dataTables_filter') + .addClass('d-flex align-items-center gap-2') + .appendTo('#filtrosIzquierda'); + + $('#filtrosIzquierda').append($('#columnFilter')); + + const searchInput = $('.dataTables_filter input'); + + $('#columnFilter').on('change', function () { + const colIndex = $(this).val(); + + table.search('').columns().search('').draw(); + + searchInput.off('keyup').on('keyup', function () { + if (colIndex === "") { + table.search(this.value).draw(); + } else { + table.column(colIndex).search(this.value).draw(); + } + }); + }); + + // SUBMIT (problema de checkbox) + + $('form').on('submit', function () { + + const form = this; + + // Obtener TODOS los checkboxes marcados del DataTable (no solo los visibles) + + const checked = table.$('input[name="ids[]"]:checked'); + + // Eliminar cualquier ids[] previo agregado dinámicamente + + $(form).find('input[name="ids[]"][type="hidden"]').remove(); + + // Agregar hidden inputs reales al form + + checked.each(function () { + $('') + .attr('type', 'hidden') + .attr('name', 'ids[]') + .val($(this).val()) + .appendTo(form); + }); + }); +}); \ No newline at end of file diff --git a/public/js/ventas/ventaValidator.js b/public/js/ventas/ventaValidator.js new file mode 100644 index 0000000..e2f1783 --- /dev/null +++ b/public/js/ventas/ventaValidator.js @@ -0,0 +1,193 @@ +document.addEventListener("DOMContentLoaded", () => { + + if (serverErrors.length > 0) mostrarErrores(serverErrors); + if (serverSuccess.length > 0) mostrarSuccess(serverSuccess); + + const form = document.getElementById("form-crear-venta"); + if (!form) return; + + // Utilidades + + function isEmpty(value) { + return !value || value.trim() === ''; + } + + function toFloat(value) { + return parseFloat(value) || 0; + } + + // Submit + + form.addEventListener("submit", (e) => { + + let errores = []; + + const condIva = form.querySelector("[name='cond_iva']"); + const cuit = form.querySelector("[name='cuit']"); + const formaPago = form.querySelector("[name='forma_pago']"); + const tipoPago = form.querySelector("[name='tipo_pago']"); + const descuento = form.querySelector("[name='descuento']"); + const recargo = form.querySelector("[name='recargo']"); + + // limpiar estados previos + + form.querySelectorAll(".is-invalid").forEach(el => + el.classList.remove("is-invalid") + ); + + // FORMA Y TIPO DE PAGO + + const reglasPago = { + 'efectivo': ['pago_unico'], + 'transferencia': ['pago_unico'], + 'echeq': ['pago_unico'], + 'debito': ['pago_unico'], + 'credito': ['pago_unico', '3_cuotas', '6_cuotas', '12_cuotas'], + }; + + if (formaPago && tipoPago) { + + const tiposValidos = reglasPago[formaPago.value] || []; + + if (!tiposValidos.includes(tipoPago.value)) { + errores.push("Combinación de forma y tipo de pago inválida."); + formaPago.classList.add("is-invalid"); + tipoPago.classList.add("is-invalid"); + } + } + + // DESCUENTO / RECARGO + + if (descuento) { + const d = toFloat(descuento.value); + + if (d < 0 || d > 100) { + errores.push("El descuento debe estar entre 0 y 100%."); + descuento.classList.add("is-invalid"); + } + } + + // RECARGO + + if (recargo) { + const r = toFloat(recargo.value); + + if (r < 0 || r > 100) { + errores.push("El recargo debe estar entre 0 y 100%."); + recargo.classList.add("is-invalid"); + } + } + + // Resultado + + if (errores.length > 0) { + e.preventDefault(); + mostrarErrores(errores); + } + }); + + // FUNCIÓN PARA MOSTRAR ERRORES + + function mostrarErrores(errores) { + + // Buscar si hay un toast previo + + const viejo = document.getElementById("toastErrores"); + + // Si existe, se elimina (evita duplicados) + + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID para identificarlo + + toast.id = "toastErrores"; + + // Clases de estilo (Bootstrap + CSS) + + toast.className = "toast-flotante alert alert-danger"; + + // Inserta el contenido dinámico (lista de errores) + + toast.innerHTML = ` + Se encontraron errores: + + `; + + // Agrega el toast al body (lo hace visible) + + document.body.appendChild(toast); + + // Auto-cierre + + setTimeout(() => { + + // Agrega clase para animación de salida (CSS) + + toast.classList.add("toast-out"); + + // Elimina el elemento después de la animación + + setTimeout(() => { + toast.remove(); // Elimina el DOM + }, 350); + + }, 5000); // Visible durante 5 segundos + } + }); + + // FUNCIÓN PARA MOSTRAR SUCCESS + + function mostrarSuccess(mensajes) { + + // Elimina toast previo de éxito si existe + + const viejo = document.getElementById("toastSuccess"); + if (viejo) viejo.remove(); + + // Crea el contenedor del toast + + const toast = document.createElement("div"); + + // Se le asigna ID único + + toast.id = "toastSuccess"; + + // Estilo + + toast.className = "toast-flotante alert alert-success"; + + // Inserta mensajes dinámicos + + toast.innerHTML = ` + Mensajes de éxito: + + `; + + // Agrega al DOM + + document.body.appendChild(toast); + + // Auto-cierre a los 5 segundos + + setTimeout(() => { + + // Animación de salida + + toast.classList.add("toast-out"); + + // Eliminación final + + setTimeout(() => { + toast.remove(); + }, 350); + + }, 5000); + } \ No newline at end of file