Carpeta js
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
EVENTO: DOMContentLoaded
|
||||
|
||||
Se ejecuta cuando el HTML termina de cargarse
|
||||
|
||||
Esto asegura que todos los elementos del DOM existan antes de intentar utilizarlos desde JavaScript
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
/*
|
||||
REFERENCIAS DEL DOM
|
||||
|
||||
Obtenemos:
|
||||
- form -> formulario del chat
|
||||
- input -> input donde escribe el usuario
|
||||
- chatBox -> contenedor de mensajes
|
||||
*/
|
||||
|
||||
const form = document.getElementById("chat-form");
|
||||
const input = document.getElementById("mensaje");
|
||||
const chatBox = document.getElementById("chat-box");
|
||||
|
||||
// Se detiene el script si no existe algún elemento
|
||||
|
||||
if (!form || !input || !chatBox) return;
|
||||
|
||||
// EVENTO SUBMIT -> se ejecuta cuando el usuario envía el formulario
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
|
||||
// PREVENIR RECARGA -> Evita que el formulario recargue la página
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// OBTENER MENSAJE
|
||||
|
||||
const mensaje = input.value.trim();
|
||||
|
||||
// VALIDAR MENSAJE
|
||||
|
||||
if (!mensaje) return;
|
||||
|
||||
// MOSTRAR MENSAJE DEL USUARIO -> Se agrega inmediatamente al chat
|
||||
|
||||
agregarMensaje("user", mensaje);
|
||||
|
||||
// LIMPIAR INPUT
|
||||
|
||||
input.value = "";
|
||||
|
||||
// MOSTRAR TYPING (ESCRIBIENDO...)
|
||||
|
||||
mostrarTyping();
|
||||
|
||||
try {
|
||||
|
||||
/*
|
||||
FETCH AJAX
|
||||
|
||||
Realizamos una petición POST hacia chatbot.php
|
||||
*/
|
||||
|
||||
const response = await fetch(
|
||||
window.location.origin + "/ajax/chatbot.php",
|
||||
{
|
||||
method: "POST",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
|
||||
/*
|
||||
BODY
|
||||
|
||||
encodeURIComponent():
|
||||
- Escapa caracteres especiales
|
||||
*/
|
||||
|
||||
body:
|
||||
"mensaje=" + encodeURIComponent(mensaje)
|
||||
}
|
||||
);
|
||||
|
||||
// ELIMINAR TYPING
|
||||
|
||||
removerTyping();
|
||||
|
||||
// CONVERTIR RESPUESTA JSON
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// ERROR BACKEND -> success === false
|
||||
|
||||
if (!data.success) {
|
||||
|
||||
console.error(data.error);
|
||||
|
||||
agregarMensaje(
|
||||
"bot",
|
||||
"Ocurrió un error al procesar la consulta."
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// RESPUESTA IA
|
||||
|
||||
agregarMensaje(
|
||||
"bot",
|
||||
data.respuesta
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
// ERROR FETCH / CONEXIÓN
|
||||
|
||||
removerTyping();
|
||||
|
||||
console.error(error);
|
||||
|
||||
agregarMensaje(
|
||||
"bot",
|
||||
"Error de conexión."
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Evita inyección HTML/XSS
|
||||
|
||||
function escaparHTML(texto)
|
||||
{
|
||||
return texto
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// AGREGAR MENSAJE VISUAL AL CHAT -> tipo: user / bot
|
||||
|
||||
function agregarMensaje(tipo, texto)
|
||||
{
|
||||
|
||||
// CREAR DIV
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
// CLASE CSS
|
||||
|
||||
div.className =
|
||||
tipo === "user" ? "mensaje-user" : "mensaje-bot";
|
||||
|
||||
div.innerHTML = `
|
||||
<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;
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
@@ -0,0 +1,204 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
// Mostrar errores del servidor si existen
|
||||
|
||||
if (serverErrors.length > 0) mostrarErrores(serverErrors);
|
||||
if (serverSuccess.length > 0) mostrarSuccess(serverSuccess);
|
||||
|
||||
const form = document.getElementById("form-login");
|
||||
const usuarioInput = document.querySelector("input[name='usuario']");
|
||||
const passInput = document.querySelector("input[name='password']");
|
||||
|
||||
if (!form || !usuarioInput || !passInput) return;
|
||||
|
||||
// Protección básica de intentos (localStorage)
|
||||
|
||||
function verificarIntentos() {
|
||||
const ahora = Date.now();
|
||||
const intentosData = JSON.parse(localStorage.getItem("login_intentos")) || { c: 0, t: ahora };
|
||||
|
||||
if (ahora - intentosData.t > 60000) {
|
||||
intentosData.c = 0;
|
||||
intentosData.t = ahora;
|
||||
}
|
||||
|
||||
localStorage.setItem("login_intentos", JSON.stringify(intentosData));
|
||||
return intentosData;
|
||||
}
|
||||
|
||||
// Funciones utilitarias
|
||||
|
||||
function isEmpty(value) {
|
||||
return !value || value.trim() === '';
|
||||
}
|
||||
|
||||
function hasNoSpaces(value) {
|
||||
return !value.includes(' ');
|
||||
}
|
||||
|
||||
function isValidUsername(value) {
|
||||
return /^(?=.*[a-zA-Z])(?!.*[._]{2})[a-zA-Z0-9._]{5,30}$/.test(value);
|
||||
}
|
||||
|
||||
function isValidPassword(value) {
|
||||
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{6,}$/.test(value);
|
||||
}
|
||||
|
||||
form.addEventListener("submit", (e) => {
|
||||
let errores = [];
|
||||
|
||||
const usuario = usuarioInput.value.trim();
|
||||
const password = passInput.value.trim();
|
||||
const intentos = verificarIntentos();
|
||||
|
||||
usuarioInput.classList.remove("is-invalid");
|
||||
passInput.classList.remove("is-invalid");
|
||||
|
||||
// INTENTOS (se verifica primero para cortar rápido)
|
||||
|
||||
if (intentos.c >= 3) {
|
||||
e.preventDefault();
|
||||
mostrarErrores(["Demasiados intentos. Espere 1 minuto antes de volver a intentar."]);
|
||||
return;
|
||||
}
|
||||
|
||||
// USUARIO
|
||||
|
||||
if (isEmpty(usuario)) {
|
||||
errores.push("El usuario es obligatorio.");
|
||||
usuarioInput.classList.add("is-invalid");
|
||||
} else if (!isValidUsername(usuario)) {
|
||||
errores.push("El usuario debe tener 5–30 caracteres, incluir al menos una letra y solo usar letras, números, puntos o guiónes bajos no consecutivos.");
|
||||
usuarioInput.classList.add("is-invalid");
|
||||
}
|
||||
|
||||
// PASSWORD
|
||||
|
||||
if (isEmpty(password)) {
|
||||
errores.push("La contraseña es obligatoria.");
|
||||
passInput.classList.add("is-invalid");
|
||||
} else if (!hasNoSpaces(password)) {
|
||||
errores.push("La contraseña no puede contener espacios.");
|
||||
passInput.classList.add("is-invalid");
|
||||
} else if (!isValidPassword(password)) {
|
||||
errores.push("La contraseña debe tener al menos una mayúscula, una minúscula y un número, y mínimo 6 caracteres.");
|
||||
passInput.classList.add("is-invalid");
|
||||
}
|
||||
|
||||
if (errores.length > 0) {
|
||||
e.preventDefault();
|
||||
mostrarErrores(errores);
|
||||
|
||||
intentos.c++;
|
||||
localStorage.setItem("login_intentos", JSON.stringify(intentos));
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem("login_intentos");
|
||||
});
|
||||
|
||||
// FUNCIÓN PARA MOSTRAR ERRORES
|
||||
|
||||
function mostrarErrores(errores) {
|
||||
|
||||
// Buscar si hay un toast previo
|
||||
|
||||
const viejo = document.getElementById("toastErrores");
|
||||
|
||||
// Si existe, se elimina (evita duplicados)
|
||||
|
||||
if (viejo) viejo.remove();
|
||||
|
||||
// Crea el contenedor del toast
|
||||
|
||||
const toast = document.createElement("div");
|
||||
|
||||
// Se le asigna ID para identificarlo
|
||||
|
||||
toast.id = "toastErrores";
|
||||
|
||||
// Clases de estilo (Bootstrap + CSS)
|
||||
|
||||
toast.className = "toast-flotante alert alert-danger";
|
||||
|
||||
// Inserta el contenido dinámico (lista de errores)
|
||||
|
||||
toast.innerHTML = `
|
||||
<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 = $('#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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
if (serverErrors.length > 0) mostrarErrores(serverErrors);
|
||||
if (serverSuccess.length > 0) mostrarSuccess(serverSuccess);
|
||||
|
||||
const form = document.getElementById("form-crear-usuario") || document.getElementById("form-editar-usuario");
|
||||
if (!form) return;
|
||||
|
||||
// Si no hay submit (operador), no validar
|
||||
|
||||
if (!form.querySelector("button[type='submit']")) return;
|
||||
|
||||
// Funciones utilitarias
|
||||
|
||||
function isEmpty(value) {
|
||||
return !value || value.trim() === '';
|
||||
}
|
||||
|
||||
function hasNoSpaces(value) {
|
||||
return !value.includes(' ');
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email);
|
||||
}
|
||||
|
||||
function isValidName(value) {
|
||||
return /^[A-ZÁÉÍÓÚÑ][a-záéíóúñ]+(?: [A-ZÁÉÍÓÚÑ][a-záéíóúñ]+)*$/.test(value);
|
||||
}
|
||||
|
||||
function isValidUsername(value) {
|
||||
return /^(?=.*[a-zA-Z])(?!.*[._]{2})[a-zA-Z0-9._]{5,30}$/.test(value);
|
||||
}
|
||||
|
||||
function isValidPassword(value) {
|
||||
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{6,}$/.test(value);
|
||||
}
|
||||
|
||||
// Submit
|
||||
|
||||
form.addEventListener("submit", (e) => {
|
||||
|
||||
let errores = [];
|
||||
|
||||
const nombre = form.querySelector("input[name='nombre']");
|
||||
const apellido = form.querySelector("input[name='apellido']");
|
||||
const usuario = form.querySelector("input[name='usuario']");
|
||||
const telefono = form.querySelector("input[name='telefono']");
|
||||
const email = form.querySelector("input[name='email']");
|
||||
const password = form.querySelector("input[name='password']");
|
||||
|
||||
// Limpiar estados previos
|
||||
|
||||
form.querySelectorAll(".is-invalid").forEach(el =>
|
||||
el.classList.remove("is-invalid")
|
||||
);
|
||||
|
||||
// NOMBRE
|
||||
|
||||
if (nombre) {
|
||||
if (isEmpty(nombre.value)) {
|
||||
errores.push("El nombre es obligatorio.");
|
||||
nombre.classList.add("is-invalid");
|
||||
} else if (!isValidName(nombre.value)) {
|
||||
errores.push("El nombre debe comenzar con mayúscula y solo contener letras.");
|
||||
nombre.classList.add("is-invalid");
|
||||
} else if (nombre.value.length < 2) {
|
||||
errores.push("El nombre debe tener al menos 2 caracteres.");
|
||||
nombre.classList.add("is-invalid");
|
||||
} else if (nombre.value.length > 50) {
|
||||
errores.push("El nombre no puede superar los 50 caracteres.");
|
||||
nombre.classList.add("is-invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// APELLIDO
|
||||
|
||||
if (apellido) {
|
||||
if (isEmpty(apellido.value)) {
|
||||
errores.push("El apellido es obligatorio.");
|
||||
apellido.classList.add("is-invalid");
|
||||
} else if (!isValidName(apellido.value)) {
|
||||
errores.push("El apellido debe comenzar con mayúscula y solo contener letras.");
|
||||
apellido.classList.add("is-invalid");
|
||||
} else if (apellido.value.length < 2) {
|
||||
errores.push("El apellido debe tener al menos 2 caracteres.");
|
||||
apellido.classList.add("is-invalid");
|
||||
} else if (apellido.value.length > 50) {
|
||||
errores.push("El apellido no puede superar los 50 caracteres.");
|
||||
apellido.classList.add("is-invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// USUARIO
|
||||
|
||||
if (isEmpty(usuario)) {
|
||||
errores.push("El usuario es obligatorio.");
|
||||
usuarioInput.classList.add("is-invalid");
|
||||
} else if (!isValidUsername(usuario)) {
|
||||
errores.push("El usuario debe tener 5–30 caracteres, incluir al menos una letra y solo usar letras, números, puntos o guiónes bajos no consecutivos.");
|
||||
usuarioInput.classList.add("is-invalid");
|
||||
}
|
||||
|
||||
// TELÉFONO
|
||||
|
||||
if (telefono) {
|
||||
if (isEmpty(telefono.value)) {
|
||||
errores.push("El teléfono es obligatorio.");
|
||||
telefono.classList.add("is-invalid");
|
||||
} else if (telefono.value.length != 10) {
|
||||
errores.push("El teléfono debe poseer 10 dígitos.");
|
||||
telefono.classList.add("is-invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// EMAIL
|
||||
|
||||
if (email) {
|
||||
if (isEmpty(email.value)) {
|
||||
errores.push("El email es obligatorio.");
|
||||
email.classList.add("is-invalid");
|
||||
} else if (!isValidEmail(email.value)) {
|
||||
errores.push("El email no tiene un formato válido.");
|
||||
email.classList.add("is-invalid");
|
||||
} else if (email.value.length > 100) {
|
||||
errores.push("El email no puede superar los 100 caracteres.");
|
||||
email.classList.add("is-invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// PASSWORD
|
||||
|
||||
if (isEmpty(password)) {
|
||||
errores.push("La contraseña es obligatoria.");
|
||||
passInput.classList.add("is-invalid");
|
||||
} else if (!hasNoSpaces(password)) {
|
||||
errores.push("La contraseña no puede contener espacios.");
|
||||
passInput.classList.add("is-invalid");
|
||||
} else if (!isValidPassword(password)) {
|
||||
errores.push("La contraseña debe tener al menos una mayúscula, una minúscula y un número, y mínimo 6 caracteres.");
|
||||
passInput.classList.add("is-invalid");
|
||||
}
|
||||
|
||||
// Resultado
|
||||
|
||||
if (errores.length > 0) {
|
||||
e.preventDefault();
|
||||
mostrarErrores(errores);
|
||||
}
|
||||
});
|
||||
|
||||
// FUNCIÓN PARA MOSTRAR ERRORES
|
||||
|
||||
function mostrarErrores(errores) {
|
||||
|
||||
// Buscar si hay un toast previo
|
||||
|
||||
const viejo = document.getElementById("toastErrores");
|
||||
|
||||
// Si existe, se elimina (evita duplicados)
|
||||
|
||||
if (viejo) viejo.remove();
|
||||
|
||||
// Crea el contenedor del toast
|
||||
|
||||
const toast = document.createElement("div");
|
||||
|
||||
// Se le asigna ID para identificarlo
|
||||
|
||||
toast.id = "toastErrores";
|
||||
|
||||
// Clases de estilo (Bootstrap + CSS)
|
||||
|
||||
toast.className = "toast-flotante alert alert-danger";
|
||||
|
||||
// Inserta el contenido dinámico (lista de errores)
|
||||
|
||||
toast.innerHTML = `
|
||||
<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 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,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);
|
||||
}
|
||||
Reference in New Issue
Block a user