Carpeta js

This commit is contained in:
Axel Axt
2026-08-04 19:45:50 -03:00
parent 8bd815f888
commit 15281e9244
34 changed files with 4417 additions and 0 deletions
+227
View File
@@ -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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// 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 = `
<div class="mensaje-contenido">
${escaparHTML(texto).replace(/\n/g, "<br>")}
</div>
`;
// 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 = `
<div class="mensaje-contenido">
Escribiendo...
</div>
`;
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;
}
});
+56
View File
@@ -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();
}
});
});
});
+273
View File
@@ -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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
@@ -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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
+113
View File
@@ -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();
}
});
+284
View File
@@ -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 = '<i class="fas fa-chart-line me-2"></i> Balance Anual';
cargarBalance(anio);
} else {
document.getElementById('tituloInforme').innerHTML = '<i class="fas fa-money-bill-wave me-2"></i> 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);
+204
View File
@@ -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 530 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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
});
+56
View File
@@ -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();
}
});
});
});
+56
View File
@@ -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();
}
});
});
});
+105
View File
@@ -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
$('<input>')
.attr('type', 'hidden')
.attr('name', 'ids[]')
.val(id)
.addClass('dt-hidden')
.appendTo(form);
// Cantidad
$('<input>')
.attr('type', 'hidden')
.attr('name', 'cantidades[' + id + ']')
.val(cantidad)
.addClass('dt-hidden')
.appendTo(form);
// Costo
$('<input>')
.attr('type', 'hidden')
.attr('name', 'costos[' + id + ']')
.val(costo)
.addClass('dt-hidden')
.appendTo(form);
}
});
});
});
+58
View File
@@ -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();
}
});
});
});
+110
View File
@@ -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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
@@ -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();
}
});
});
});
@@ -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 () {
$('<input>')
.attr('type', 'hidden')
.attr('name', 'ids[]')
.val($(this).val())
.appendTo(form);
});
});
});
@@ -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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
@@ -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();
}
});
});
});
+211
View File
@@ -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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
+58
View File
@@ -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();
}
});
});
});
@@ -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();
}
});
});
});
+276
View File
@@ -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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
@@ -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();
}
});
});
});
+254
View File
@@ -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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
@@ -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();
}
});
});
});
@@ -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();
}
});
});
});
@@ -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 () {
$('<input>')
.attr('type', 'hidden')
.attr('name', 'ids[]')
.val($(this).val())
.appendTo(form);
});
});
});
+56
View File
@@ -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();
}
});
});
});
+225
View File
@@ -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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
+56
View File
@@ -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();
}
});
});
});
+256
View File
@@ -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 530 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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}
+58
View File
@@ -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();
}
});
});
});
+56
View File
@@ -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();
}
});
});
});
+56
View File
@@ -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();
}
});
});
});
+83
View File
@@ -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 () {
$('<input>')
.attr('type', 'hidden')
.attr('name', 'ids[]')
.val($(this).val())
.appendTo(form);
});
});
});
+193
View File
@@ -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 = `
<strong>Se encontraron errores:</strong>
<ul class="mb-0 mt-2">
${errores.map(e => `<li>${e}</li>`).join("")} <!-- convierte array a <li> -->
</ul>
`;
// 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 = `
<strong>Mensajes de éxito:</strong>
<ul class="mb-0 mt-2">
${mensajes.map(m => `<li>${m}</li>`).join("")}
</ul>
`;
// 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);
}