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