Testing Alpha
This commit is contained in:
@ -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;
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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>`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
@ -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
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
134
resources/assets/js/maps/LeafletMapHelper.js
Normal file
134
resources/assets/js/maps/LeafletMapHelper.js
Normal 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;
|
@ -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();
|
||||
}
|
17
resources/assets/js/notifications/drivers/notyf-driver.js
Normal file
17
resources/assets/js/notifications/drivers/notyf-driver.js
Normal 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,
|
||||
});
|
||||
},
|
||||
};
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
15
resources/assets/js/notifications/drivers/toastify-driver.js
Normal file
15
resources/assets/js/notifications/drivers/toastify-driver.js
Normal 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();
|
||||
},
|
||||
};
|
87
resources/assets/js/notifications/drivers/toastr-driver.js
Normal file
87
resources/assets/js/notifications/drivers/toastr-driver.js
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
47
resources/assets/js/notifications/drivers/vuexy-driver.js
Normal file
47
resources/assets/js/notifications/drivers/vuexy-driver.js
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
67
resources/assets/js/notifications/notify-channel-service.js
Normal file
67
resources/assets/js/notifications/notify-channel-service.js
Normal 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;
|
7
resources/assets/js/notifications/notify-loader.js
Normal file
7
resources/assets/js/notifications/notify-loader.js
Normal 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();
|
||||
});
|
94
resources/assets/js/notifications/vuexy/vuexy-notify.js
Normal file
94
resources/assets/js/notifications/vuexy/vuexy-notify.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user