All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 47s
Implementa funcionalidad de copia en tres secciones del Informe: 📋 Funcionalidades agregadas: 1. Lista de Ingresos - Copiar Texto: Formato WhatsApp con emojis y legible - Copiar JSON: Formato estructurado para sistemas 2. Top 10 Clientes - Copiar Texto: Ranking formateado con métricas - Copiar JSON: Array de objetos con datos completos 3. Serie Temporal Acumulada - Copiar Texto: Evolución temporal con emojis - Copiar JSON: Datos completos para análisis ✨ Características: - Botones con iconos (i-lucide-copy y i-lucide-code) - Disabled cuando no hay datos disponibles - Alertas de confirmación al copiar - Formato texto optimizado para WhatsApp - Incluye metadata: rango de fechas y timestamp Uso: - Copiar Texto → Compartir en WhatsApp/Telegram - Copiar JSON → Integración con otros sistemas
240 lines
6.5 KiB
TypeScript
240 lines
6.5 KiB
TypeScript
/**
|
|
* Composable para gestionar el sistema de temas de Analítica Núcleo
|
|
*
|
|
* Proporciona funciones para cargar, aplicar, guardar y resetear temas personalizados.
|
|
* El tema se guarda en localStorage y se sincroniza entre pestañas del navegador.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const { theme, applyTheme, loadTheme, saveTheme, resetTheme } = useTheme()
|
|
*
|
|
* // Cargar tema guardado
|
|
* onMounted(() => loadTheme())
|
|
*
|
|
* // Modificar tema
|
|
* theme.value.primary = '#ff5733'
|
|
* applyTheme()
|
|
* saveTheme()
|
|
* ```
|
|
*/
|
|
|
|
export interface ThemeColors {
|
|
bg: string
|
|
surface: string
|
|
border: string
|
|
primary: string
|
|
primaryStrong: string
|
|
accent: string
|
|
text: string
|
|
textMuted: string
|
|
}
|
|
|
|
/**
|
|
* Tema por defecto - Café
|
|
*/
|
|
export const defaultTheme: ThemeColors = {
|
|
bg: '#14100b',
|
|
surface: '#1f180f',
|
|
border: '#3a2a16',
|
|
primary: '#e0c080',
|
|
primaryStrong: '#c08040',
|
|
accent: '#ffe0a0',
|
|
text: '#fef9f0',
|
|
textMuted: '#d8c7a6'
|
|
}
|
|
|
|
/**
|
|
* Clave de localStorage donde se guarda el tema
|
|
*/
|
|
const STORAGE_KEY = 'custom-theme'
|
|
|
|
/**
|
|
* Hook principal del sistema de temas
|
|
*/
|
|
export const useTheme = () => {
|
|
// Estado reactivo compartido entre todos los componentes
|
|
const theme = useState<ThemeColors>('app-theme', () => ({ ...defaultTheme }))
|
|
|
|
/**
|
|
* Aplica las variables CSS del tema al DOM
|
|
* @param themeColors - Objeto con los colores del tema (opcional, usa el estado actual si no se proporciona)
|
|
*/
|
|
const applyTheme = (themeColors?: ThemeColors) => {
|
|
if (import.meta.client) {
|
|
const colors = themeColors || theme.value
|
|
const root = document.documentElement
|
|
|
|
root.style.setProperty('--brand-bg', colors.bg)
|
|
root.style.setProperty('--brand-surface', colors.surface)
|
|
root.style.setProperty('--brand-border', colors.border)
|
|
root.style.setProperty('--brand-primary', colors.primary)
|
|
root.style.setProperty('--brand-primary-strong', colors.primaryStrong)
|
|
root.style.setProperty('--brand-accent', colors.accent)
|
|
root.style.setProperty('--brand-text', colors.text)
|
|
root.style.setProperty('--brand-text-muted', colors.textMuted)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Carga el tema guardado desde localStorage
|
|
* Si no existe tema guardado, aplica el tema por defecto
|
|
*/
|
|
const loadTheme = () => {
|
|
if (import.meta.client) {
|
|
try {
|
|
const savedTheme = localStorage.getItem(STORAGE_KEY)
|
|
if (savedTheme) {
|
|
const parsed = JSON.parse(savedTheme) as ThemeColors
|
|
theme.value = parsed
|
|
applyTheme(parsed)
|
|
} else {
|
|
// No hay tema guardado, aplicar el por defecto
|
|
applyTheme(defaultTheme)
|
|
}
|
|
} catch (error) {
|
|
console.error('Error al cargar el tema desde localStorage:', error)
|
|
// En caso de error, aplicar tema por defecto
|
|
applyTheme(defaultTheme)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Guarda el tema actual en localStorage
|
|
* @returns true si se guardó correctamente, false si hubo un error
|
|
*/
|
|
const saveTheme = (): boolean => {
|
|
if (import.meta.client) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(theme.value))
|
|
return true
|
|
} catch (error) {
|
|
console.error('Error al guardar el tema en localStorage:', error)
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Resetea el tema a los valores por defecto y limpia localStorage
|
|
*/
|
|
const resetTheme = () => {
|
|
theme.value = { ...defaultTheme }
|
|
applyTheme(defaultTheme)
|
|
|
|
if (import.meta.client) {
|
|
try {
|
|
localStorage.removeItem(STORAGE_KEY)
|
|
} catch (error) {
|
|
console.error('Error al limpiar el tema de localStorage:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Establece un nuevo tema completo
|
|
* @param newTheme - Objeto con los colores del nuevo tema
|
|
* @param save - Si true, guarda automáticamente en localStorage (default: false)
|
|
*/
|
|
const setTheme = (newTheme: ThemeColors, save = false) => {
|
|
theme.value = { ...newTheme }
|
|
applyTheme(newTheme)
|
|
|
|
if (save) {
|
|
saveTheme()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exporta el tema actual como JSON string
|
|
* @returns JSON string del tema actual
|
|
*/
|
|
const exportTheme = (): string => {
|
|
return JSON.stringify(theme.value, null, 2)
|
|
}
|
|
|
|
/**
|
|
* Importa un tema desde JSON string
|
|
* @param jsonString - JSON string con la configuración del tema
|
|
* @param save - Si true, guarda automáticamente en localStorage (default: false)
|
|
* @returns true si se importó correctamente, false si hubo un error
|
|
*/
|
|
const importTheme = (jsonString: string, save = false): boolean => {
|
|
try {
|
|
const parsed = JSON.parse(jsonString) as ThemeColors
|
|
|
|
// Validar que tenga todas las propiedades requeridas
|
|
const requiredKeys: (keyof ThemeColors)[] = [
|
|
'bg', 'surface', 'border', 'primary',
|
|
'primaryStrong', 'accent', 'text', 'textMuted'
|
|
]
|
|
|
|
for (const key of requiredKeys) {
|
|
if (!parsed[key] || typeof parsed[key] !== 'string') {
|
|
throw new Error(`Propiedad "${key}" faltante o inválida`)
|
|
}
|
|
}
|
|
|
|
setTheme(parsed, save)
|
|
return true
|
|
} catch (error) {
|
|
console.error('Error al importar el tema:', error)
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listener para sincronizar cambios de tema entre pestañas
|
|
* Se ejecuta automáticamente cuando se detecta un cambio en localStorage
|
|
*/
|
|
const syncThemeAcrossTabs = (event: StorageEvent) => {
|
|
if (event.key === STORAGE_KEY && event.newValue) {
|
|
try {
|
|
const newTheme = JSON.parse(event.newValue) as ThemeColors
|
|
theme.value = newTheme
|
|
applyTheme(newTheme)
|
|
} catch (error) {
|
|
console.error('Error al sincronizar tema entre pestañas:', error)
|
|
}
|
|
} else if (event.key === STORAGE_KEY && event.newValue === null) {
|
|
// El tema fue eliminado en otra pestaña, resetear
|
|
resetTheme()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inicializa los listeners de sincronización entre pestañas
|
|
* Solo debe llamarse una vez al montar la aplicación
|
|
*/
|
|
const initStorageListener = () => {
|
|
if (import.meta.client) {
|
|
window.addEventListener('storage', syncThemeAcrossTabs)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Limpia los listeners de sincronización
|
|
* Debe llamarse al desmontar si es necesario
|
|
*/
|
|
const cleanupStorageListener = () => {
|
|
if (import.meta.client) {
|
|
window.removeEventListener('storage', syncThemeAcrossTabs)
|
|
}
|
|
}
|
|
|
|
return {
|
|
theme,
|
|
defaultTheme,
|
|
applyTheme,
|
|
loadTheme,
|
|
saveTheme,
|
|
resetTheme,
|
|
setTheme,
|
|
exportTheme,
|
|
importTheme,
|
|
initStorageListener,
|
|
cleanupStorageListener
|
|
}
|
|
}
|