Testing Alpha

This commit is contained in:
2025-05-11 14:14:50 -06:00
parent 988b86a33d
commit a7002701f5
1903 changed files with 77534 additions and 36485 deletions

View File

@ -1,16 +1,15 @@
import '../../vendor/libs/bootstrap-table/bootstrap-table';
import '../notifications/LivewireNotification';
import '@vuexy-admin/assets/vendor/libs/bootstrap-table/bootstrap-table';
class BootstrapTableManager {
constructor(bootstrapTableWrap, config = {}) {
const defaultConfig = {
header: [],
format: [],
formatters: [],
search_columns: [],
actionColumn: false,
height: 'auto',
minHeight: 300,
bottomMargin : 195,
bottomMargin : 35,
search: true,
showColumns: true,
showColumnsToggleAll: true,
@ -18,7 +17,7 @@ class BootstrapTableManager {
exportfileName: 'datatTable',
exportWithDatetime: true,
showFullscreen: true,
showPaginationSwitch: true,
showPaginationSwitch: false,
showRefresh: true,
showToggle: true,
/*
@ -49,7 +48,7 @@ class BootstrapTableManager {
pageList: [25, 50, 100, 500, 1000],
sortName: 'id',
sortOrder: 'asc',
cookie: false,
cookie: true,
cookieExpire: '365d',
cookieIdTable: 'myTableCookies', // Nombre único para las cookies de la tabla
cookieStorage: 'localStorage',
@ -77,7 +76,10 @@ class BootstrapTableManager {
* Calcula la altura de la tabla.
*/
getTableHeight() {
const btHeight = window.innerHeight - this.$toolbar.height() - this.bottomMargin;
let container = document.querySelector('.container-p-y'),
toolbat = document.querySelector('.bt-toolbar');
let btHeight = container?.offsetHeight - toolbat?.offsetHeight - this.config.bottomMargin;
return btHeight < this.config.minHeight ? this.config.minHeight : btHeight;
}
@ -106,8 +108,7 @@ class BootstrapTableManager {
* Carga los formatters dinámicamente
*/
async loadFormatters() {
//const formattersModules = import.meta.glob('../../../../../**/resources/assets/js/bootstrap-table/*Formatters.js');
const formattersModules = import.meta.glob('/vendor/koneko/laravel-vuexy-admin/resources/assets/js/bootstrap-table/*Formatters.js');
const formattersModules = import.meta.glob('/vendor/koneko/**/resources/assets/js/bootstrap-table/*Formatters.js');
const formatterPromises = Object.entries(formattersModules).map(async ([path, importer]) => {
const module = await importer();
@ -121,7 +122,7 @@ class BootstrapTableManager {
const columns = [];
Object.entries(this.config.header).forEach(([key, value]) => {
const columnFormat = this.config.format[key] || {};
const columnFormat = this.config.formatters[key] || {};
if (typeof columnFormat.formatter === 'object') {
const formatterName = columnFormat.formatter.name;

View File

@ -29,6 +29,13 @@ export const booleanStatusCatalog = {
trueClass: 'text-green-800',
falseClass: '',
},
simpleCheck: {
trueText: '',
trueIcon: 'ti ti-check',
falseIcon: '',
trueClass: 'text-green-900',
falseText: '',
},
checkbox: {
trueIcon: 'ti ti-checkbox',
falseIcon: '',
@ -130,3 +137,20 @@ export const statusIntBadgeBgCatalog = {
12: 'Cancelado',
};
/**
* Genera la URL del avatar basado en iniciales o devuelve la foto de perfil si está disponible.
* @param {string} fullName - Nombre completo del usuario.
* @param {string|null} profilePhoto - Ruta de la foto de perfil.
* @returns {string} - URL del avatar.
*/
export const getAvatarUrl = (fullName, profilePhoto) => {
const baseUrl = window.baseUrl || '';
if (profilePhoto) {
return `${baseUrl}storage/profile-photos/${profilePhoto}`;
}
const name = fullName.replace(/\s+/g, '-').toLowerCase();
return `${baseUrl}admin/usuario/avatar/?name=${fullName}`;
}

View File

@ -1,5 +1,4 @@
import { booleanStatusCatalog, statusIntBadgeBgCatalogCss, statusIntBadgeBgCatalog } from './globalConfig';
import {routes} from '@vuexy-admin/bootstrap-table/globalConfig.js';
import { routes, getAvatarUrl, booleanStatusCatalog, statusIntBadgeBgCatalogCss, statusIntBadgeBgCatalog } from '@vuexy-admin/assets/js/bootstrap-table/globalConfig';
export const userActionFormatter = (value, row, index) => {
if (!row.id) return '';
@ -9,7 +8,7 @@ export const userActionFormatter = (value, row, index) => {
const deleteUrl = routes['admin.user.delete'].replace(':id', row.id);
return `
<div class="flex space-x-2">
<div class="flex justify-center space-x-2">
<a href="${editUrl}" title="Editar" class="icon-button hover:text-slate-700">
<i class="ti ti-edit"></i>
</a>
@ -23,6 +22,25 @@ export const userActionFormatter = (value, row, index) => {
`.trim();
};
export const userLoginActionFormatter = (value, row, index) => {
if (!row.user_id) return '';
//const showUrl = routes['admin.user-login.show'].replace(':id', row.user_id);
const showUrl = '#';
return `
<div class="flex justify-center space-x-2">
<a href="${showUrl}" title="Ver" class="icon-button hover:text-slate-700">
<i class="ti ti-eye"></i>
</a>
</div>
`.trim();
};
export const dynamicBooleanFormatter = (value, row, index, options = {}) => {
const { tag = 'default', customOptions = {} } = options;
const catalogConfig = booleanStatusCatalog[tag] || {};
@ -50,6 +68,8 @@ export const dynamicBooleanFormatter = (value, row, index, options = {}) => {
};
export const dynamicBadgeFormatter = (value, row, index, options = {}) => {
if (!value) return '';
const {
color = 'primary', // Valor por defecto
textColor = '', // Permite agregar color de texto si es necesario
@ -59,6 +79,11 @@ export const dynamicBadgeFormatter = (value, row, index, options = {}) => {
return `<span class="badge bg-${color} ${textColor} ${additionalClass}">${value}</span>`;
};
export const statusIntBadgeBgFormatter = (value, row, index) => {
return value
? `<span class="badge bg-label-${statusIntBadgeBgCatalogCss[value]}">${statusIntBadgeBgCatalog[value]}</span>`
@ -71,95 +96,240 @@ export const textNowrapFormatter = (value, row, index) => {
}
export const toCurrencyFormatter = (value, row, index) => {
return isNaN(value) ? '' : Number(value).toCurrency();
}
export const numberFormatter = (value, row, index) => {
return isNaN(value) ? '' : Number(value);
}
export const textXsFormatter = (value, row, index) => {
if (!value) return '';
return `<span class="text-xs">${value}</span>`;
};
export const monthFormatter = (value, row, index) => {
switch (parseInt(value)) {
case 1:
return 'Enero';
case 2:
return 'Febrero';
case 3:
return 'Marzo';
case 4:
return 'Abril';
case 5:
return 'Mayo';
case 6:
return 'Junio';
case 7:
return 'Julio';
case 8:
return 'Agosto';
case 9:
return 'Septiembre';
case 10:
return 'Octubre';
case 11:
return 'Noviembre';
case 12:
return 'Diciembre';
}
}
export const humaneTimeFormatter = (value, row, index) => {
return isNaN(value) ? '' : Number(value).humaneTime();
}
/**
* Genera la URL del avatar basado en iniciales o devuelve la foto de perfil si está disponible.
* @param {string} fullName - Nombre completo del usuario.
* @param {string|null} profilePhoto - Ruta de la foto de perfil.
* @returns {string} - URL del avatar.
*/
function getAvatarUrl(fullName, profilePhoto) {
const baseUrl = window.baseUrl || '';
if (profilePhoto) {
return `${baseUrl}storage/profile-photos/${profilePhoto}`;
export const dateClassicFormatter = (value) => {
if (!value) return '';
const date = new Date(value);
if (isNaN(date)) return value;
const fecha = date.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
const hora = value.length == 19? date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }): false;
let fechaHora = hora
? `<span class="inline-flex items-center gap-1 text-blue-700 text-xs px-2 py-0.5 me-1">
<i class="ti ti-calendar-event text-[14px]"></i> ${fecha}
</span>`
: `<span class="inline-flex items-center gap-1 text-blue-700 px-2 py-1">
<i class="ti ti-calendar-event text-[16px]"></i> ${fecha}
</span>`;
if (hora) {
fechaHora += `
<span class="inline-flex items-center gap-1 text-gray-700 text-xs px-2 py-0.5">
<i class="ti ti-clock text-[14px]"></i> ${hora}
</span>
`.trim();
}
return `${baseUrl}admin/usuario/avatar/?name=${fullName}`;
}
return fechaHora.trim();
};
export const dateLimeFormatter = (value) => {
if (!value) return '';
const date = new Date(value);
if (isNaN(date)) return value;
const fecha = date.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
const hora = value.length == 19? date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }): false;
let fechaHora = hora
? `<span class="inline-flex items-center gap-1 text-lime-800 text-xs px-2 py-0.5 me-1">
<i class="ti ti-calendar-event text-[14px]"></i> ${fecha}
</span>`
: `<span class="inline-flex items-center gap-1 text-lime-800 px-2 py-1">
<i class="ti ti-calendar-event text-[16px]"></i> ${fecha}
</span>`;
if (hora) {
fechaHora += `
<span class="inline-flex items-center gap-1 text-lime-600 text-xs px-2 py-0.5">
<i class="ti ti-clock text-[14px]"></i> ${hora}
</span>
`.trim();
}
return fechaHora.trim();
};
export const dateBadgeBlueFormatter = (value) => {
if (!value) return '';
const date = new Date(value);
if (isNaN(date)) return value;
const fecha = date.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
const hora = value.length == 19? date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }): false;
let fechaHora = hora
? `<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 text-blue-700 text-xs px-2 py-0.5 me-1">
<i class="ti ti-calendar-event text-[14px]"></i> ${fecha}
</span>`
: `<span class="inline-flex items-center gap-1 rounded-full bg-blue-50 text-blue-700 px-2 py-1">
<i class="ti ti-calendar-event text-[16px]"></i> ${fecha}
</span>`;
if (hora) {
fechaHora += `
<span class="inline-flex items-center gap-1 rounded-full bg-gray-100 text-gray-700 text-xs px-2 py-0.5">
<i class="ti ti-clock text-[14px]"></i> ${hora}
</span>
`.trim();
}
return fechaHora.trim();
};
export const dateBadgeAmberFormatter = (value) => {
if (!value) return '';
const date = new Date(value);
if (isNaN(date)) return value;
const fecha = date.toLocaleDateString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric' });
const hora = value.length == 19? date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }): false;
let fechaHora = hora
? `<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 text-amber-700 text-xs px-2 py-0.5 me-1">
<i class="ti ti-calendar-event text-[14px]"></i> ${fecha}
</span>`
: `<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 text-amber-700 px-2 py-1">
<i class="ti ti-calendar-event text-[16px]"></i> ${fecha}
</span>`;
if (hora) {
fechaHora += `
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 text-amber-600 text-xs px-2 py-0.5">
<i class="ti ti-clock text-[14px]"></i> ${hora}
</span>
`.trim();
}
return fechaHora.trim();
};
export const emailFormatter = (value, row, index) => {
if (!value) return '';
return `
<a href="mailto:${value}" class="flex items-center space-x-2 text-blue-600">
<i class="ti ti-mail-filled"></i>
<span class="whitespace-nowrap hover:underline">${value}</span>
</a>
`.trim();
};
/**
* Formatea la columna del perfil de usuario con avatar, nombre y correo.
* Formatter para mostrar duración de sesión del usuario.
*
* @param {string} row.created_at - Fecha de inicio de sesión.
* @param {string|null} row.logout_at - Fecha de cierre de sesión o null si aún activa.
* @returns {string} Duración de la sesión formateada o estado de sesión activa.
*/
export const userProfileFormatter = (value, row, index) => {
export const sessionDurationFormatter = (value, row, index) => {
if (!row.created_at) return '<span class="text-muted">-</span>';
const start = new Date(row.created_at);
const end = row.logout_at ? new Date(row.logout_at) : new Date();
const diffMs = end - start;
if (diffMs < 0) return '<span class="text-danger">Invalido</span>';
const diffSeconds = Math.floor(diffMs / 1000);
const hours = Math.floor(diffSeconds / 3600);
const minutes = Math.floor((diffSeconds % 3600) / 60);
const seconds = diffSeconds % 60;
const formattedTime = [
hours ? `${hours}h` : '',
minutes ? `${minutes}m` : '',
`${seconds}s`
].filter(Boolean).join(' ');
return row.logout_at
? `<span class="text-xs font-medium text-slate-700">${formattedTime}</span>`
: `<span class="text-xs font-semibold text-success-600">🟢 Sesión activa (${formattedTime})</span>`;
};
/**
* @param {int} row.id - Identificador del usuario
* @param {string} row.full_name - Nombre completo del usuario
* @param {string} row.profile_photo_path - Ruta de la foto de perfil
* @param {string} row.email - Correo electrónico del usuario
* @returns {string} HTML con el perfil del usuario
*/
export const userIdProfileFormatter = (value, row, index) => {
if (!row.id) return '';
const avatar = getAvatarUrl(row.full_name, row.profile_photo_path);
const email = row.email ? row.email : 'Sin correo';
const profileUrl = routes['admin.user.show'].replace(':id', row.id);
const avatar = getAvatarUrl(row.full_name, row.profile_photo_path);
const email = row.email ? row.email : 'Sin correo';
return formatterProfileElement(row.id, row.full_name, avatar, email, profileUrl);
};
const formatterProfileElement = (id, name, avatar, email, profileUrl) => {
return `
<div class="flex items-center space-x-3" style="min-width: 240px">
<a href="${profileUrl}" class="flex-shrink-0">
<img src="${avatar}" alt="Avatar" class="w-10 h-10 rounded-full border border-gray-300 shadow-sm hover:scale-105 transition-transform">
</a>
<div class="truncate">
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.full_name}</a>
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${name}</a>
<small class="text-muted block truncate">${email}</small>
</div>
</div>
`;
`.trim();
};
/**
* Formatea la columna del perfil de contacto con avatar, nombre y correo.
* @param {int} row.user_id - Identificador del usuario
* @param {string} row.user_name - Nombre del usuario
* @param {string} row.user_profile_photo_path - Ruta de la foto de perfil
* @param {string} row.user_email - Correo electrónico del usuario
* @returns {string} HTML con el perfil del usuario
*/
export const contactProfileFormatter = (value, row, index) => {
if (!row.id) return '';
export const userProfileFormatter = (value, row, index) => {
if (!row.user_id) return '';
const profileUrl = routes['admin.contact.show'].replace(':id', row.id);
const avatar = getAvatarUrl(row.full_name, row.profile_photo_path);
const email = row.email ? row.email : 'Sin correo';
const profileUrl = routes['admin.user.show'].replace(':id', row.user_id);
const avatar = getAvatarUrl(row.user_name, row.user_profile_photo_path);
const email = row.user_email ? row.user_email : 'Sin correo';
return `
<div class="flex items-center space-x-3" style="min-width: 240px">
@ -167,27 +337,101 @@ export const contactProfileFormatter = (value, row, index) => {
<img src="${avatar}" alt="Avatar" class="w-10 h-10 rounded-full border border-gray-300 shadow-sm hover:scale-105 transition-transform">
</a>
<div class="truncate">
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.full_name}</a>
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.user_name}</a>
<small class="text-muted block truncate">${email}</small>
</div>
</div>
`;
`.trim();
};
export const creatorFormatter = (value, row, index) => {
if (!row.creator) return '';
const email = row.creator_email || 'Sin correo';
const showUrl = routes['admin.user.show'].replace(':id', row.id);
export const creatorProfileFormatter = (value, row, index) => {
if (!row.created_by) return '';
const profileUrl = routes['admin.user.show'].replace(':id', row.created_by);
const avatar = getAvatarUrl(row.creator_name, row.creator_profile_photo_path);
const email = row.creator_email ? row.creator_email : 'Sin correo';
return `
<div class="flex flex-col">
<a href="${showUrl}" class="font-medium text-slate-600 hover:underline block text-wrap">${row.creator}</a>
<small class="text-muted">${email}</small>
<div class="flex items-center space-x-3" style="min-width: 240px">
<a href="${profileUrl}" class="flex-shrink-0">
<img src="${avatar}" alt="Avatar" class="w-10 h-10 rounded-full border border-gray-300 shadow-sm hover:scale-105 transition-transform">
</a>
<div class="truncate">
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.creator_name}</a>
<small class="text-muted block truncate">${email}</small>
</div>
</div>
`;
`.trim();
};
export const updaterProfileFormatter = (value, row, index) => {
if (!row.updated_by) return '';
const profileUrl = routes['admin.user.show'].replace(':id', row.updated_by);
const avatar = getAvatarUrl(row.updater_name, row.updater_profile_photo_path);
const email = row.updater_email ? row.updater_email : 'Sin correo';
return `
<div class="flex items-center space-x-3" style="min-width: 240px">
<a href="${profileUrl}" class="flex-shrink-0">
<img src="${avatar}" alt="Avatar" class="w-10 h-10 rounded-full border border-gray-300 shadow-sm hover:scale-105 transition-transform">
</a>
<div class="truncate">
<a href="${profileUrl}" class="font-medium text-slate-700 hover:underline block text-wrap">${row.updater_name}</a>
<small class="text-muted block truncate">${email}</small>
</div>
</div>
`.trim();
};
/**
* Convierte bytes en un formato legible por humanos.
*
* @param {int} value - Valor en bytes
* @returns {string} Tamaño en formato legible
*/
export const bytesToHumanReadable = (value, row, index) => {
if (!value || value === 0) return;
const precision = 2; // define aquí la precisión que desees
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const base = Math.floor(Math.log(value) / Math.log(1024));
return (value / Math.pow(1024, base)).toFixed(precision) + ' ' + units[base];
};
export const userRoleFormatter = (value, row, index) => {
if (!value) return '';
const rolesStyles = window.userRoleStyles || {};
return value.split('|').map(role => {
const trimmedRole = role.trim();
const color = rolesStyles[trimmedRole] || 'secondary'; // fallback si no existe
return `<span class="badge bg-label-${color} mr-2 mb-2">${trimmedRole}</span>`;
}).join('');
};
export const booleanStatusFormatter = (value, row, index) => {
return `<span class="badge bg-label-${value ? 'success' : 'danger'}">${value ? 'Activo' : 'Inactivo'}</span>`;
};

View File

@ -335,19 +335,18 @@ export default class FormCanvasHelper {
return str.charAt(0).toUpperCase() + str.slice(1);
}
const singularName = this.liveWireInstance.get('singularName');
const columnNameLabel = this.liveWireInstance.get('columnNameLabel');
const editName = this.liveWireInstance.get(columnNameLabel);
const singularName = this.liveWireInstance.get('singularName');
const displayName = this.liveWireInstance.get('displayName');
switch (mode) {
case 'create':
this.offcanvasTitleEl.innerHTML = `<i class="ti ti-plus ml-2"></i> ${capitalizeFirstLetter(singularName)} `;
break;
case 'edit':
this.offcanvasTitleEl.innerHTML = `${editName} <i class="ti ti-lg ti-pencil ml-2 text-success"></i>`;
this.offcanvasTitleEl.innerHTML = `${displayName} <i class="ti ti-lg ti-pencil ml-2 text-success"></i>`;
break;
case 'delete':
this.offcanvasTitleEl.innerHTML = `${editName} <i class="ti ti-lg ti-eraser ml-2 text-danger"></i>`;
this.offcanvasTitleEl.innerHTML = `${displayName} <i class="ti ti-lg ti-eraser ml-2 text-danger"></i>`;
break;
}
}
@ -432,13 +431,13 @@ export default class FormCanvasHelper {
* Hace focus en el elemento con el selector dado.
*/
focusOnOpen() {
const focusSelector = this.liveWireInstance.get('focusOnOpen'); // Obtiene el selector de Livewire
const focusSelector = this.liveWireInstance.get('focusColumnOnOpen'); // Obtiene el selector de Livewire
if (!focusSelector) return;
setTimeout(() => {
// Buscar el elemento real en el DOM
const focusElement = document.getElementById(focusSelector);
const focusElement = document.querySelector(`[name="${focusSelector}"]`);
// Si existe, hacer focus
if (focusElement) {

View File

@ -48,6 +48,5 @@ window.config = {
window.assetsPath = document.documentElement.getAttribute('data-assets-path');
window.baseUrl = document.documentElement.getAttribute('data-base-url');
window.quicklinksUpdateUrl = document.documentElement.getAttribute('data-quicklinks-update-url');
window.templateName = document.documentElement.getAttribute('data-template');
window.rtlSupport = false; // set true for rtl support (rtl + ltr), false for ltr only.

View File

@ -1,11 +1,13 @@
import './layout/quicklinks-navbar.js';
import './layout/search-navbar.js';
import './search-navbar'
('use strict');
/**
* Main
*/
'use strict';
window.isRtl = window.Helpers.isRtl();
window.isDarkStyle = window.Helpers.isDarkStyle();
let menu,
animate,
isHorizontalLayout = false;
@ -47,8 +49,8 @@ if (document.getElementById('layout-menu')) {
showDropdownOnHover: localStorage.getItem('templateCustomizer-' + templateName + '--ShowDropdownOnHover') // If value(showDropdownOnHover) is set in local storage
? localStorage.getItem('templateCustomizer-' + templateName + '--ShowDropdownOnHover') === 'true' // Use the local storage value
: window.templateCustomizer !== undefined // If value is set in config.js
? window.templateCustomizer.settings.defaultShowDropdownOnHover // Use the config.js value
: true // Use this if you are not using the config.js and want to set value directly from here
? window.templateCustomizer.settings.defaultShowDropdownOnHover // Use the config.js value
: true // Use this if you are not using the config.js and want to set value directly from here
});
// Change parameter to true if you want scroll animation
window.Helpers.scrollToActive((animate = false));
@ -69,9 +71,7 @@ if (document.getElementById('layout-menu')) {
String(window.Helpers.isCollapsed())
);
// Update customizer checkbox state on click of menu toggler
let layoutCollapsedCustomizerOptions = document.querySelector(
'.template-customizer-layouts-options'
);
let layoutCollapsedCustomizerOptions = document.querySelector('.template-customizer-layouts-options');
if (layoutCollapsedCustomizerOptions) {
let layoutCollapsedVal = window.Helpers.isCollapsed() ? 'collapsed' : 'expanded';
layoutCollapsedCustomizerOptions.querySelector(`input[value="${layoutCollapsedVal}"]`).click();
@ -303,7 +303,7 @@ if (document.getElementById('layout-menu')) {
// On window resize listener
// -------------------------
document.addEventListener(
window.addEventListener(
'resize',
function (event) {
// Hide open search input and set value blank

View File

@ -1,56 +0,0 @@
/**
* Quicklinks Navbar
*/
'use strict';
$(function () {
// Navbar Quicklinks with autosuggest (typeahead)
const $dropdownShortcuts = $('.dropdown-shortcuts'),
$dropdownShortcutsAdd = $('.dropdown-shortcuts-add'),
$dropdownShortcutsRemove = $('.dropdown-shortcuts-remove');
const route = document.documentElement.getAttribute('data-route');
if ($dropdownShortcuts.length) {
$dropdownShortcutsAdd.on('click', function () {
$.ajax({
url: quicklinksUpdateUrl,
method: 'POST',
data: {
action: 'update',
route: route
},
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
success: function (response) {
location.reload();
},
error: function (xhr) {
console.error(xhr.responseJSON.message);
}
});
});
$dropdownShortcutsRemove.on('click', function () {
$.ajax({
url: quicklinksUpdateUrl,
method: 'POST',
data: {
action: 'remove',
route: route
},
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
success: function (response) {
location.reload();
},
error: function (xhr) {
console.error(xhr.responseJSON.message);
}
});
});
}
});

View File

@ -4,42 +4,43 @@
'use strict';
$(function () {
window.Helpers.initSidebarToggle();
// Toggle Universal Sidebar
// ! Removed following code if you do't wish to use jQuery. Remember that navbar search functionality will stop working on removal.
if (typeof $ !== 'undefined') {
$(function () {
// ! TODO: Required to load after DOM is ready, did this now with jQuery ready.
window.Helpers.initSidebarToggle();
// Toggle Universal Sidebar
// Navbar Search with autosuggest (typeahead)
var searchToggler = $('.search-toggler'),
searchInputWrapper = $('.search-input-wrapper'),
searchInput = $('.search-input'),
contentBackdrop = $('.content-backdrop');
// Navbar Search with autosuggest (typeahead)
// ? You can remove the following JS if you don't want to use search functionality.
//----------------------------------------------------------------------------------
// Open search input on click of search icon
if (searchToggler.length) {
searchToggler.on('click', function () {
if (searchInputWrapper.length) {
searchInputWrapper.toggleClass('d-none');
searchInput.trigger('focus');
}
});
var searchToggler = $('.search-toggler'),
searchInputWrapper = $('.search-input-wrapper'),
searchInput = $('.search-input'),
contentBackdrop = $('.content-backdrop');
document.addEventListener('keydown', function (event) {
const ctrlKey = event.ctrlKey;
const slashKey = event.key === '/'; // Usa 'key' para obtener la tecla como texto
// Open search input on click of search icon
if (searchToggler.length) {
searchToggler.on('click', function () {
if (searchInputWrapper.length) {
searchInputWrapper.toggleClass('d-none');
searchInput.focus();
}
});
}
// Open search on 'CTRL+/'
$(document).on('keydown', function (event) {
let ctrlKey = event.ctrlKey,
slashKey = event.which === 191;
if (ctrlKey && slashKey) {
const searchInputWrapper = document.querySelector('.search-input-wrapper');
const searchInput = document.querySelector('.search-input');
if (searchInputWrapper) {
searchInputWrapper.classList.toggle('d-none'); // Alterna la visibilidad
if (searchInput) {
searchInput.focus(); // Coloca el foco en el input
}
if (searchInputWrapper.length) {
searchInputWrapper.toggleClass('d-none');
searchInput.focus();
}
}
});
// Note: Following code is required to update container class of typeahead dropdown width on focus of search input. setTimeout is required to allow time to initiate Typeahead UI.
setTimeout(function () {
var twitterTypeahead = $('.twitter-typeahead');
@ -48,154 +49,262 @@ $(function () {
if (searchInputWrapper.hasClass('container-xxl')) {
searchInputWrapper.find(twitterTypeahead).addClass('container-xxl');
twitterTypeahead.removeClass('container-fluid');
} else if (searchInputWrapper.hasClass('container-fluid')) {
searchInputWrapper.find(twitterTypeahead).addClass('container-fluid');
twitterTypeahead.removeClass('container-xxl');
}
});
}, 10);
}
if (searchInput.length) {
// Función para normalizar cadenas (eliminar acentos)
function normalizeString(str) {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
}
// Filter config con soporte para ignorar acentos
var filterConfig = function (data) {
return function findMatches(q, cb) {
let matches = [];
// Normalizar la consulta
const normalizedQuery = normalizeString(q);
data.filter(function (i) {
const normalizedName = normalizeString(i.name);
if (normalizedName.startsWith(normalizedQuery)) {
matches.push(i);
} else if (
!normalizedName.startsWith(normalizedQuery) &&
normalizedName.includes(normalizedQuery)
) {
matches.push(i);
// Ordenar por coincidencia secundaria
matches.sort(function (a, b) {
return b.name < a.name ? 1 : -1;
});
}
});
cb(matches);
if (searchInput.length) {
// Filter config
var filterConfig = function (data) {
return function findMatches(q, cb) {
let matches;
matches = [];
data.filter(function (i) {
if (i.name.toLowerCase().startsWith(q.toLowerCase())) {
matches.push(i);
} else if (
!i.name.toLowerCase().startsWith(q.toLowerCase()) &&
i.name.toLowerCase().includes(q.toLowerCase())
) {
matches.push(i);
matches.sort(function (a, b) {
return b.name < a.name ? 1 : -1;
});
} else {
return [];
}
});
cb(matches);
};
};
};
// Search JSON
var searchJson = 'search-navbar'; // For vertical layout
if ($('#layout-menu').hasClass('menu-horizontal')) {
// Search JSON
var searchJson = 'search-navbar'; // For vertical layout
}
// Search API AJAX call
var searchData = $.ajax({
url: assetsPath + '../../admin/' + searchJson, //? Use your own search api instead
dataType: 'json',
async: false
}).responseJSON;
if ($('#layout-menu').hasClass('menu-horizontal')) {
var searchJson = 'search-navbar'; // For vertical layout
}
// Init typeahead on searchInput
searchInput.each(function () {
var $this = $(this);
// Search API AJAX call
var searchData = $.ajax({
url: assetsPath + '../../admin/navbar/' + searchJson, //? Use your own search api instead
dataType: 'json',
async: false
}).responseJSON;
searchInput
.typeahead(
{
hint: false,
classNames: {
menu: 'tt-menu navbar-search-suggestion',
cursor: 'active',
suggestion: 'suggestion d-flex justify-content-between px-4 py-2 w-100'
// Init typeahead on searchInput
searchInput.each(function () {
var $this = $(this);
searchInput
.typeahead(
{
hint: false,
classNames: {
menu: 'tt-menu navbar-search-suggestion',
cursor: 'active',
suggestion: 'suggestion d-flex justify-content-between px-4 py-2 w-100'
}
},
// ? Add/Update blocks as per need
// Pages
{
name: 'pages',
display: 'name',
limit: 6,
source: filterConfig(searchData.pages),
templates: {
header: '<h6 class="suggestions-header text-primary mb-0 mx-4 mt-3 pb-2">Pages</h6>',
suggestion: function ({ url, icon, name, path }) {
return (
'<a href="' +
//baseUrl +
url +
'">' +
'<div>' +
'<i class="ti ' +
icon +
' me-2"></i>' +
'<span class="align-middle">' +
path +
' / ' +
name +
'</span>' +
'</div>' +
'</a>'
);
},
notFound:
'<div class="not-found px-4 py-2">' +
'<h6 class="suggestions-header text-primary mb-2">Pages</h6>' +
'<p class="py-2 mb-0"><i class="ti ti-alert-circle ti-xs me-2"></i> No se encontro resultados</p>' +
'</div>'
}
},
// Categories
{
name: 'categories',
display: 'name',
limit: 6,
source: filterConfig(searchData.categories),
templates: {
header: '<h6 class="suggestions-header text-primary mb-0 mx-4 mt-3 pb-2">Categorías</h6>',
suggestion: function ({ slug, icon, name, path }) {
return (
'<a href="' +
//baseUrl +
'admin/f/' +
slug +
'">' +
'<div>' +
'<i class="ti ' +
icon +
' me-2"></i>' +
'<span class="align-middle">' +
path +
(path? ' / ': '') +
name +
'</span>' +
'</div>' +
'</a>'
);
},
notFound:
'<div class="not-found px-4 py-2">' +
'<h6 class="suggestions-header text-primary mb-2">Pages</h6>' +
'<p class="py-2 mb-0"><i class="ti ti-alert-circle ti-xs me-2"></i> No se encontro resultados</p>' +
'</div>'
}
},
// Files
/*
{
name: 'files',
display: 'name',
limit: 4,
source: filterConfig(searchData.files),
templates: {
header: '<h6 class="suggestions-header text-primary mb-0 mx-4 mt-3 pb-2">Files</h6>',
suggestion: function ({ src, name, subtitle, meta }) {
return (
'<a href="javascript:;">' +
'<div class="d-flex w-50">' +
'<img class="me-3" src="' +
assetsPath +
src +
'" alt="' +
name +
'" height="32">' +
'<div class="w-75">' +
'<h6 class="mb-0">' +
name +
'</h6>' +
'<small class="text-muted">' +
subtitle +
'</small>' +
'</div>' +
'</div>' +
'<small class="text-muted">' +
meta +
'</small>' +
'</a>'
);
},
notFound:
'<div class="not-found px-4 py-2">' +
'<h6 class="suggestions-header text-primary mb-2">Files</h6>' +
'<p class="py-2 mb-0"><i class="ti ti-alert-circle ti-xs me-2"></i> No se encontro resultados</p>' +
'</div>'
}
},
// Members
{
name: 'members',
display: 'name',
limit: 4,
source: filterConfig(searchData.members),
templates: {
header: '<h6 class="suggestions-header text-primary mb-0 mx-4 mt-3 pb-2">Members</h6>',
suggestion: function ({ name, src, subtitle }) {
return (
'<a href="' +
baseUrl +
'app/user/view/account">' +
'<div class="d-flex align-items-center">' +
'<img class="rounded-circle me-3" src="' +
assetsPath +
src +
'" alt="' +
name +
'" height="32">' +
'<div class="user-info">' +
'<h6 class="mb-0">' +
name +
'</h6>' +
'<small class="text-muted">' +
subtitle +
'</small>' +
'</div>' +
'</div>' +
'</a>'
);
},
notFound:
'<div class="not-found px-4 py-2">' +
'<h6 class="suggestions-header text-primary mb-2">Members</h6>' +
'<p class="py-2 mb-0"><i class="ti ti-alert-circle ti-xs me-2"></i> No se encontro resultados</p>' +
'</div>'
}
}
},
// Páginas
{
name: 'pages',
display: 'name',
limit: 8,
source: filterConfig(searchData.pages),
templates: {
header: '<h6 class="suggestions-header text-primary mb-0 mx-4 mt-3 pb-2">Páginas</h6>',
suggestion: function ({ url, icon, name }) {
return (
'<a href="' +
url +
'">' +
'<div>' +
'<i class="ti ' +
icon +
' me-2"></i>' +
'<span class="align-middle">' +
name +
'</span>' +
'</div>' +
'</a>'
);
},
notFound:
'<div class="not-found px-4 py-2">' +
'<h6 class="suggestions-header text-primary mb-2">Páginas</h6>' +
'<p class="py-2 mb-0"><i class="ti ti-alert-circle ti-xs me-2"></i> No se encontro resultados</p>' +
'</div>'
*/
)
//On typeahead result render.
.bind('typeahead:render', function () {
// Show content backdrop,
contentBackdrop.addClass('show').removeClass('fade');
})
// On typeahead select
.bind('typeahead:select', function (ev, suggestion) {
// Open selected page
if (suggestion.url !== 'javascript:;') {
window.location = baseUrl + suggestion.url;
}
})
// On typeahead close
.bind('typeahead:close', function () {
// Clear search
searchInput.val('');
$this.typeahead('val', '');
// Hide search input wrapper
searchInputWrapper.addClass('d-none');
// Fade content backdrop
contentBackdrop.addClass('fade').removeClass('show');
});
// On searchInput keyup, Fade content backdrop if search input is blank
searchInput.on('keyup', function () {
if (searchInput.val() == '') {
contentBackdrop.addClass('fade').removeClass('show');
}
)
//On typeahead result render.
.on('typeahead:render', function () {
// Show content backdrop,
contentBackdrop.addClass('show').removeClass('fade');
})
// On typeahead select
.on('typeahead:select', function (ev, suggestion) {
// Open selected page
if (suggestion.url !== 'javascript:;') window.location = suggestion.url;
})
// On typeahead close
.on('typeahead:close', function () {
// Clear search
searchInput.val('');
$this.typeahead('val', '');
// Hide search input wrapper
searchInputWrapper.addClass('d-none');
// Fade content backdrop
contentBackdrop.addClass('fade').removeClass('show');
});
});
// Init PerfectScrollbar in search result
var psSearch;
$('.navbar-search-suggestion').each(function () {
psSearch = new PerfectScrollbar($(this)[0], {
wheelPropagation: false,
suppressScrollX: true
});
});
// On searchInput keyup, Fade content backdrop if search input is blank
searchInput.on('keyup', function () {
if (searchInput.val() == '') contentBackdrop.addClass('fade').removeClass('show');
psSearch.update();
});
});
// Init PerfectScrollbar in search result
var psSearch;
$('.navbar-search-suggestion').each(function () {
psSearch = new PerfectScrollbar($(this)[0], {
wheelPropagation: false,
suppressScrollX: true
});
});
searchInput.on('keyup', function () {
psSearch.update();
});
}
});
}
});
}

View File

@ -0,0 +1,134 @@
import '@vuexy-admin/assets/vendor/libs/leaflet/leaflet'
//import './../../vendor/libs/leaflet/leaflet'
export const LeafletMapHelper = (() => {
let mapInstance, markerInstance;
const DEFAULT_COORDS = [19.4326, -99.1332]; // Coordenadas de CDMX por defecto
// Valida coordenadas
const isValidCoordinate = (lat, lng) => {
return lat && !isNaN(lat) && lat >= -90 && lat <= 90 && lat !== 0 &&
lng && !isNaN(lng) && lng >= -180 && lng <= 180 && lng !== 0;
};
// Crea opciones del mapa según el modo
const getMapOptions = (mode) => ({
scrollWheelZoom: mode !== 'delete',
dragging: mode !== 'delete',
doubleClickZoom: mode !== 'delete',
boxZoom: mode !== 'delete',
keyboard: mode !== 'delete',
zoomControl: mode !== 'delete',
touchZoom: mode !== 'delete'
});
// Destruir el mapa existente
const destroyMap = () => {
if (mapInstance) {
mapInstance.off();
mapInstance.remove();
mapInstance = null;
}
removeMarker();
};
// Crear marcador en el mapa
const createMarker = (lat, lng, draggable = false, onDragEnd) => {
if (isValidCoordinate(lat, lng)) {
markerInstance = L.marker([lat, lng], { draggable }).addTo(mapInstance)
.bindPopup('<b>Ubicación seleccionada</b>').openPopup();
if (draggable && onDragEnd) {
markerInstance.on('dragend', (e) => {
const { lat, lng } = e.target.getLatLng();
onDragEnd(lat, lng);
});
}
}
};
// Eliminar marcador
const removeMarker = () => {
if (markerInstance) {
markerInstance.remove();
markerInstance = null;
}
};
// Actualizar coordenadas en formulario
const updateCoordinates = (lat, lng, latSelector, lngSelector, livewireInstance) => {
const latInput = document.querySelector(latSelector);
const lngInput = document.querySelector(lngSelector);
if (!latInput || !lngInput) {
console.warn(`⚠️ No se encontró el elemento del DOM para latitud (${latSelector}) o longitud (${lngSelector})`);
return;
}
latInput.value = lat ? lat.toFixed(6) : '';
lngInput.value = lng ? lng.toFixed(6) : '';
if (livewireInstance) {
livewireInstance.lat = lat ? lat.toFixed(6) : null;
livewireInstance.lng = lng ? lng.toFixed(6) : null;
}
};
// Inicializar el mapa
const initializeMap = (locationInputs, mode = 'create', livewireInstance = null) => {
const mapElement = document.getElementById(locationInputs.mapId);
if (!mapElement) {
console.error(`❌ No se encontró el contenedor del mapa con ID: ${locationInputs.mapId}`);
return;
}
let latElement = document.querySelector(locationInputs.lat);
let lngElement = document.querySelector(locationInputs.lng);
if (!latElement || !lngElement) {
console.error(`❌ No se encontraron los campos de latitud (${locationInputs.lat}) o longitud (${locationInputs.lng})`);
return;
}
let lat = parseFloat(latElement.value);
let lng = parseFloat(lngElement.value);
const mapCoords = isValidCoordinate(lat, lng) ? [lat, lng] : DEFAULT_COORDS;
const zoomLevel = isValidCoordinate(lat, lng) ? 16 : 13;
if (mapInstance) destroyMap();
mapInstance = L.map(locationInputs.mapId, getMapOptions(mode)).setView(mapCoords, zoomLevel);
L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png').addTo(mapInstance);
if (mode !== 'create') createMarker(lat, lng, mode === 'edit', (lat, lng) => updateCoordinates(lat, lng, locationInputs.lat, locationInputs.lng, livewireInstance));
if (mode !== 'delete') {
mapInstance.on('click', (e) => {
const { lat, lng } = e.latlng;
removeMarker();
createMarker(lat, lng, true, (lat, lng) => updateCoordinates(lat, lng, locationInputs.lat, locationInputs.lng, livewireInstance));
updateCoordinates(lat, lng, locationInputs.lat, locationInputs.lng, livewireInstance);
});
}
/*
const btnClearElement = document.querySelector(locationInputs.btnClear);
if(!btnClearElement){
console.error(`❌ No se encontró el botón de limpiar con ID: ${locationInputs.btnClear}`);return;
}
*/
};
return {
initializeMap,
clearCoordinates: () => {
removeMarker();
},
};
})();
window.LeafletMapHelper = LeafletMapHelper;

View File

@ -1,207 +0,0 @@
export default class LivewireNotification {
constructor(config = {}) {
const defaultConfig = {
delay: 9000, // Tiempo predeterminado para las notificaciones
onNotificationShown: null, // Callback al mostrar una notificación
onNotificationRemoved: null, // Callback al eliminar una notificación
onNotificationClosed: null // Callback al cerrar una notificación mediante botón
};
this.config = { ...defaultConfig, ...config };
this.initLivewireNotification();
}
/**
* Inicializa la escucha de notificaciones desde Livewire.
*/
initLivewireNotification() {
// Mostrar notificación almacenada después de la recarga
const storedNotification = localStorage.getItem('pendingNotification');
if (storedNotification) {
const event = JSON.parse(storedNotification);
this.showStoredNotification(event);
localStorage.removeItem('pendingNotification'); // Limpiar después de mostrar
}
// Escuchar nuevas notificaciones desde Livewire
Livewire.on('notification', event => {
if (event.deferReload) {
// Guardar la notificación en localStorage para mostrar después de la recarga
localStorage.setItem('pendingNotification', JSON.stringify(event));
window.location.reload();
} else {
// Mostrar la notificación inmediatamente
this.showNotification(event);
}
});
// Escuchar evento personalizado para almacenar la notificación en localStorage
document.addEventListener('store-notification', (event) => {
const notification = {
type: event.detail.type || 'info',
message: event.detail.message || 'Notificación',
delay: event.detail.delay || 5000,
target: event.detail.target || 'body'
};
localStorage.setItem('pendingNotification', JSON.stringify(notification));
});
}
/**
* Método para emitir notificaciones desde JavaScript.
* @param {Object} options - Opciones de la notificación.
* @param {Function} callback - Callback opcional que se ejecutará después de mostrar la notificación.
* @param {number} customTimeout - Timeout personalizado (opcional).
*/
emitNotification(options, callback, customTimeout) {
const event = {
target: options.target || 'body',
message: options.message || 'Notificación',
type: options.type || 'info',
deferReload: options.deferReload || false,
delay: customTimeout || options.delay || this.config.delay // Usar el timeout personalizado o el predeterminado
};
// Mostrar la notificación
this.showNotification(event);
// Ejecutar callback si está definido
if (typeof callback === 'function') {
callback(event);
}
}
/**
* Muestra una notificación almacenada.
* @param {Object} event - Datos del evento de notificación.
*/
showStoredNotification(event) {
this.showNotification(event);
}
/**
* Muestra una notificación.
* @param {Object} event - Datos del evento de notificación.
*/
showNotification(event) {
setTimeout(() => {
const targetElement = document.querySelector(event.target);
if (!targetElement) {
console.error(`Target ${event.target} no encontrado. Mostrando en el contenedor global.`);
this.showInGlobalContainer(event);
return;
}
// Crear un contenedor para notificaciones si no existe
if (!targetElement.querySelector('.notification-container')) {
const container = document.createElement('div');
container.className = 'notification-container';
targetElement.appendChild(container);
}
const notificationContainer = targetElement.querySelector('.notification-container');
const notificationElement = this.renderNotification(notificationContainer, event);
// Callback opcional al mostrar la notificación
if (typeof this.config.onNotificationShown === 'function') {
this.config.onNotificationShown(notificationElement, event);
}
// Configurar el timeout para eliminar la notificación
this.setdelay(notificationElement, event);
// Configurar el evento para el botón de cierre
this.setupCloseButton(notificationElement, event);
}, 5);
}
/**
* Renderiza una notificación en el contenedor global (body).
* @param {Object} event - Datos del evento de notificación.
*/
showInGlobalContainer(event) {
const globalContainer = document.body;
if (!globalContainer.querySelector('.notification-container')) {
const container = document.createElement('div');
container.className = 'notification-container';
globalContainer.appendChild(container);
}
const notificationContainer = globalContainer.querySelector('.notification-container');
const notificationElement = this.renderNotification(notificationContainer, event);
if (typeof this.config.onNotificationShown === 'function') {
this.config.onNotificationShown(notificationElement, event);
}
this.setdelay(notificationElement, event);
this.setupCloseButton(notificationElement, event);
}
/**
* Renderiza una notificación en el contenedor.
* @param {HTMLElement} container - Contenedor de notificaciones.
* @param {Object} event - Evento de notificación con tipo y mensaje.
* @returns {HTMLElement} - Elemento de la notificación recién creada.
*/
renderNotification(container, event) {
const notificationElement = document.createElement('div');
notificationElement.className = `alert alert-${event.type} alert-dismissible fade show`;
notificationElement.role = 'alert';
notificationElement.innerHTML = `${event.message} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>`;
container.appendChild(notificationElement);
return notificationElement;
}
/**
* Configura un timeout para limpiar una notificación específica.
* @param {HTMLElement} notificationElement - Elemento de la notificación.
* @param {Object} event - Evento asociado a la notificación.
*/
setdelay(notificationElement, event) {
const timeout = event.delay || this.config.delay;
setTimeout(() => {
if (notificationElement && notificationElement.parentElement) {
notificationElement.remove();
// Callback opcional al eliminar la notificación
if (typeof this.config.onNotificationRemoved === 'function') {
this.config.onNotificationRemoved(notificationElement, event);
}
}
}, timeout);
}
/**
* Configura el cierre manual de una notificación mediante el botón "Cerrar".
* @param {HTMLElement} notificationElement - Elemento de la notificación.
* @param {Object} event - Evento asociado a la notificación.
*/
setupCloseButton(notificationElement, event) {
const closeButton = notificationElement.querySelector('.btn-close');
if (closeButton) {
closeButton.addEventListener('click', () => {
notificationElement.remove();
// Callback opcional al cerrar la notificación manualmente
if (typeof this.config.onNotificationClosed === 'function') {
this.config.onNotificationClosed(notificationElement, event);
}
});
}
}
}
if(!window.livewireNotification) {
window.livewireNotification = new LivewireNotification();
}

View File

@ -0,0 +1,17 @@
import { Notyf } from 'notyf';
import 'notyf/notyf.min.css';
const notyf = new Notyf({
duration: 5000,
ripple: true,
position: { x: 'right', y: 'top' },
});
export const NotyfDriver = {
notify({ type = 'success', message = '' }) {
notyf.open({
type,
message,
});
},
};

View File

@ -0,0 +1,15 @@
export const SweetAlertDriver = {
async notify(options = {}) {
const Swal = (await import('sweetalert2')).default;
Swal.fire({
icon: options.type || 'info',
title: options.title || '',
text: options.message || '',
timer: options.delay || 5000,
timerProgressBar: true,
showConfirmButton: false,
...options,
});
},
};

View File

@ -0,0 +1,15 @@
import Toastify from 'toastify-js';
import 'toastify-js/src/toastify.css';
export const ToastifyDriver = {
notify({ message = '', type = 'default', duration = 4000, position = 'right' }) {
Toastify({
text: message,
duration: duration,
gravity: 'top',
position: position,
className: `toastify-${type}`,
close: true,
}).showToast();
},
};

View File

@ -0,0 +1,87 @@
// resources/js/vuexy/notifications/vuexy-toastr.js
import toastr from 'toastr';
export const ToastrDriver = {
defaultOptions: {
closeButton: true,
progressBar: true,
tapToDismiss: true,
newestOnTop: true,
positionClass: 'toast-top-right',
timeOut: 5000,
extendedTimeOut: 1000,
showDuration: 300,
hideDuration: 300,
showMethod: 'fadeIn',
hideMethod: 'fadeOut',
},
/**
* Notifica usando toastr con opciones personalizadas
*/
notify({
type = 'success',
message = '',
title = '',
delay = 5000,
position = 'toast-top-right',
closeButton = true,
progressBar = true,
iconClass = null,
extraOptions = {}
} = {}) {
const timeOut = delay;
const extendedTimeOut = delay + 1000;
toastr.options = {
...this.defaultOptions,
closeButton,
progressBar,
timeOut,
extendedTimeOut,
positionClass: position,
...extraOptions
};
if (iconClass) {
toastr.options.iconClass = iconClass;
}
if (toastr[type]) {
toastr[type](message, title);
} else {
toastr.info(message || 'Sin mensaje');
}
if (import.meta.env.DEV) {
console.debug(`[TOAST ${type.toUpperCase()}] ${title}: ${message}`);
}
},
success(message, title = 'Éxito', delay = 4000, options = {}) {
this.notify({ type: 'success', message, title, delay, ...options });
},
error(message, title = 'Error', delay = 6000, options = {}) {
this.notify({ type: 'error', message, title, delay, ...options });
},
warning(message, title = 'Advertencia', delay = 5000, options = {}) {
this.notify({ type: 'warning', message, title, delay, ...options });
},
info(message, title = 'Información', delay = 5000, options = {}) {
this.notify({ type: 'info', message, title, delay, ...options });
},
/**
* Inicializa listeners para eventos globales como vuexy:notify
*/
listenToGlobalEvents() {
window.addEventListener('vuexy:notify', (event) => {
const detail = event.detail || {};
this.notify(detail);
});
}
};

View File

@ -0,0 +1,47 @@
// drivers/vuexy-driver.js
export const VuexyDriver = {
notify({ message, type = 'info', target = 'body', delay = 3000 }) {
const event = new CustomEvent('vuexy:notify', {
detail: { type, message, target, delay }
});
window.dispatchEvent(event);
},
success(message, target = 'body', delay = 3000) {
this.notify({ type: 'success', message, target, delay });
},
error(message, target = 'body', delay = 3000) {
this.notify({ type: 'error', message, target, delay });
},
info(message, target = 'body', delay = 3000) {
this.notify({ type: 'info', message, target, delay });
},
warning(message, target = 'body', delay = 3000) {
this.notify({ type: 'warning', message, target, delay });
},
fromStorage() {
const storageData = localStorage.getItem('vuexy_notification');
if (storageData) {
try {
this.notify(JSON.parse(storageData));
localStorage.removeItem('vuexy_notification');
} catch (e) {
console.error('[VuexyDriver] Error parseando notificación', e);
}
}
},
fromSession() {
if (window.vuexySessionNotification) {
this.notify(window.vuexySessionNotification);
window.vuexySessionNotification = null;
}
}
};

View File

@ -0,0 +1,67 @@
// notify-channel-service.js
import { VuexyNotifyService } from './vuexy/vuexy-notify';
import { ToastifyDriver } from './drivers/toastify-driver';
import { NotyfDriver } from './drivers/notyf-driver';
import { ToastrDriver } from './drivers/toastr-driver';
import { VuexyDriver } from './drivers/vuexy-driver';
const NotifyChannelService = {
async notify({ channel = 'toastr', ...payload }) {
switch (channel) {
case 'toastr':
ToastrDriver.notify(payload);
break;
case 'notyf':
NotyfDriver.notify(payload);
break;
case 'toastify':
ToastifyDriver.notify(payload);
break;
case 'sweetalert': {
const { SweetAlertDriver } = await import('./drivers/sweetalert-driver');
SweetAlertDriver.notify(payload);
break;
}
case 'vuexy':
VuexyDriver.notify(payload);
break;
case 'custom':
if (typeof window.VuexyNotify === 'function') {
window.VuexyNotify(payload);
}
break;
default:
console.warn(`[Notify] Canal desconocido: ${channel}`);
break;
}
},
fromLocalStorage() {
const data = localStorage.getItem('vuexy_notification');
if (data) {
try {
const payload = JSON.parse(data);
NotifyChannelService.notify(payload);
localStorage.removeItem('vuexy_notification');
} catch (e) {
console.error('❌ Error parseando notificación de storage', e);
}
}
},
fromSession() {
if (window.vuexySessionNotification) {
NotifyChannelService.notify(window.vuexySessionNotification);
window.vuexySessionNotification = null;
}
}
};
export { NotifyChannelService };
window.vuexyNotify = VuexyNotifyService;

View File

@ -0,0 +1,7 @@
// resources/assets/js/notify-loader.js
import { NotifyChannelService } from './notify-channel-service.js';
document.addEventListener('DOMContentLoaded', () => {
NotifyChannelService.fromLocalStorage();
NotifyChannelService.fromSession();
});

View File

@ -0,0 +1,94 @@
export const VuexyNotifyService = {
flash({ message, type = 'info', target = 'body', delay = 3000 }) {
const event = new CustomEvent('vuexy:notify', {
detail: { type, message, target, delay }
});
window.dispatchEvent(event);
},
success(message, target = 'body', delay = 3000) {
this.flash({ type: 'success', message, target, delay });
},
error(message, target = 'body', delay = 3000) {
this.flash({ type: 'error', message, target, delay });
},
info(message, target = 'body', delay = 3000) {
this.flash({ type: 'info', message, target, delay });
},
warning(message, target = 'body', delay = 3000) {
this.flash({ type: 'warning', message, target, delay });
},
fromLocalStorage() {
const data = localStorage.getItem('vuexy_notification');
if (data) {
try {
this.flash(JSON.parse(data));
localStorage.removeItem('vuexy_notification');
} catch (e) {
console.error('❌ Error parseando notificación de storage', e);
}
}
},
fromSession() {
if (window.vuexySessionNotification) {
this.flash(window.vuexySessionNotification);
window.vuexySessionNotification = null;
}
},
dispatch(eventName, payload = {}) {
const event = new CustomEvent(eventName, { detail: payload });
window.dispatchEvent(event);
}
};
document.addEventListener('DOMContentLoaded', function () {
window.addEventListener('vuexy:notify', function (event) {
// Si viene como array, tomar el primer objeto
const detail = Array.isArray(event.detail) ? event.detail[0] : event.detail;
const { type, message, target, delay } = detail || {};
const defaultTarget = 'body .notification-container';
let realTarget = target == 'body' ? defaultTarget : target;
let targetElement = document.querySelector(realTarget);
if (!targetElement) {
console.warn('⚠️ Target para notificación no encontrado:', realTarget, ', usando ', defaultTarget);
targetElement = document.querySelector(defaultTarget);
}
if(!targetElement) {
console.error('🛑 Target por defecto no encontrado:', defaultTarget);
return;
}
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show mt-2`;
alert.setAttribute('role', 'alert');
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`.trim();
targetElement.appendChild(alert);
setTimeout(() => {
alert.remove();
}, delay || 6000);
});
});