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
535 lines
19 KiB
Vue
535 lines
19 KiB
Vue
<script setup lang="ts">
|
|
import { ref, watch } from 'vue'
|
|
import type { ThemeColors } from '~/composables/useTheme'
|
|
|
|
definePageMeta({
|
|
layout: 'dashboard',
|
|
title: 'Configuración'
|
|
})
|
|
|
|
// Usar el composable de temas
|
|
const {
|
|
theme,
|
|
defaultTheme,
|
|
applyTheme,
|
|
saveTheme: saveThemeComposable,
|
|
resetTheme: resetThemeComposable,
|
|
exportTheme,
|
|
importTheme
|
|
} = useTheme()
|
|
|
|
// Temas predefinidos
|
|
const presetThemes: Record<string, ThemeColors> = {
|
|
cafe: { ...defaultTheme },
|
|
azul: {
|
|
bg: '#0a0e1a',
|
|
surface: '#151a28',
|
|
border: '#2d3748',
|
|
primary: '#60a5fa',
|
|
primaryStrong: '#3b82f6',
|
|
accent: '#93c5fd',
|
|
text: '#f0f4f8',
|
|
textMuted: '#cbd5e1'
|
|
},
|
|
verde: {
|
|
bg: '#0f1a14',
|
|
surface: '#1a2820',
|
|
border: '#2d4033',
|
|
primary: '#86efac',
|
|
primaryStrong: '#4ade80',
|
|
accent: '#bbf7d0',
|
|
text: '#f0fdf4',
|
|
textMuted: '#d1fae5'
|
|
},
|
|
carbon: {
|
|
bg: '#0f0f0f',
|
|
surface: '#1a1a1a',
|
|
border: '#333333',
|
|
primary: '#a3a3a3',
|
|
primaryStrong: '#737373',
|
|
accent: '#d4d4d4',
|
|
text: '#fafafa',
|
|
textMuted: '#d4d4d4'
|
|
}
|
|
}
|
|
|
|
// Aplicar preset de tema
|
|
const applyPreset = (presetName: keyof typeof presetThemes) => {
|
|
const preset = presetThemes[presetName]
|
|
theme.value = { ...preset }
|
|
applyTheme(theme.value)
|
|
useToast().add({
|
|
title: 'Tema aplicado',
|
|
description: `Se aplicó el tema ${presetName}`,
|
|
color: 'blue'
|
|
})
|
|
}
|
|
|
|
// Guardar tema con feedback
|
|
const handleSaveTheme = () => {
|
|
const success = saveThemeComposable()
|
|
if (success) {
|
|
useToast().add({
|
|
title: 'Tema guardado',
|
|
description: 'Los cambios se aplicaron correctamente',
|
|
color: 'green'
|
|
})
|
|
} else {
|
|
useToast().add({
|
|
title: 'Error al guardar',
|
|
description: 'No se pudo guardar el tema',
|
|
color: 'red'
|
|
})
|
|
}
|
|
}
|
|
|
|
// Resetear al tema por defecto
|
|
const handleResetTheme = () => {
|
|
resetThemeComposable()
|
|
useToast().add({
|
|
title: 'Tema reseteado',
|
|
description: 'Se restauraron los colores por defecto',
|
|
color: 'blue'
|
|
})
|
|
}
|
|
|
|
// Exportar tema al portapapeles
|
|
const handleExportTheme = async () => {
|
|
try {
|
|
const themeJson = exportTheme()
|
|
await navigator.clipboard.writeText(themeJson)
|
|
useToast().add({
|
|
title: 'Tema exportado',
|
|
description: 'El JSON del tema se copió al portapapeles',
|
|
color: 'green'
|
|
})
|
|
} catch (error) {
|
|
useToast().add({
|
|
title: 'Error al exportar',
|
|
description: 'No se pudo copiar al portapapeles',
|
|
color: 'red'
|
|
})
|
|
}
|
|
}
|
|
|
|
// Importar tema desde JSON
|
|
const importJsonInput = ref('')
|
|
const showImportModal = ref(false)
|
|
|
|
const handleImportTheme = () => {
|
|
const success = importTheme(importJsonInput.value, false)
|
|
if (success) {
|
|
useToast().add({
|
|
title: 'Tema importado',
|
|
description: 'El tema se aplicó correctamente',
|
|
color: 'green'
|
|
})
|
|
showImportModal.value = false
|
|
importJsonInput.value = ''
|
|
} else {
|
|
useToast().add({
|
|
title: 'Error al importar',
|
|
description: 'El JSON proporcionado no es válido',
|
|
color: 'red'
|
|
})
|
|
}
|
|
}
|
|
|
|
// Vista previa en tiempo real (activada por defecto)
|
|
const livePreview = ref(true)
|
|
watch(() => theme.value, (newTheme) => {
|
|
if (livePreview.value) {
|
|
applyTheme(newTheme)
|
|
}
|
|
}, { deep: true })
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardLayout>
|
|
<UDashboardPanel grow>
|
|
<UDashboardNavbar
|
|
title="Configuración"
|
|
description="Personaliza tu experiencia y preferencias del sistema"
|
|
/>
|
|
|
|
<UDashboardPanelContent>
|
|
<div class="max-w-4xl mx-auto space-y-8">
|
|
<!-- Configuración de Tema -->
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-lg bg-[var(--brand-primary)]/20 flex items-center justify-center">
|
|
<UIcon name="i-lucide-palette" class="size-5 text-[var(--brand-primary)]" />
|
|
</div>
|
|
<div>
|
|
<h3 class="font-semibold text-[var(--brand-text)]">
|
|
Tema y Apariencia
|
|
</h3>
|
|
<p class="text-sm text-[var(--brand-text-muted)]">
|
|
Personaliza los colores de la interfaz
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<UToggle v-model="livePreview" label="Vista previa en vivo" />
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Temas Predefinidos -->
|
|
<div class="mb-6 pb-6 border-b border-[var(--brand-border)]">
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-3">
|
|
Temas Predefinidos
|
|
</label>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<button
|
|
@click="applyPreset('cafe')"
|
|
class="p-4 rounded-lg border-2 border-[var(--brand-border)] hover:border-[var(--brand-primary)] transition-colors group"
|
|
>
|
|
<div class="flex gap-2 mb-2">
|
|
<div class="w-6 h-6 rounded" style="background: #e0c080" />
|
|
<div class="w-6 h-6 rounded" style="background: #1f180f" />
|
|
</div>
|
|
<p class="text-xs text-[var(--brand-text)] font-medium group-hover:text-[var(--brand-primary)]">Café</p>
|
|
</button>
|
|
|
|
<button
|
|
@click="applyPreset('azul')"
|
|
class="p-4 rounded-lg border-2 border-[var(--brand-border)] hover:border-[var(--brand-primary)] transition-colors group"
|
|
>
|
|
<div class="flex gap-2 mb-2">
|
|
<div class="w-6 h-6 rounded" style="background: #60a5fa" />
|
|
<div class="w-6 h-6 rounded" style="background: #151a28" />
|
|
</div>
|
|
<p class="text-xs text-[var(--brand-text)] font-medium group-hover:text-[var(--brand-primary)]">Azul</p>
|
|
</button>
|
|
|
|
<button
|
|
@click="applyPreset('verde')"
|
|
class="p-4 rounded-lg border-2 border-[var(--brand-border)] hover:border-[var(--brand-primary)] transition-colors group"
|
|
>
|
|
<div class="flex gap-2 mb-2">
|
|
<div class="w-6 h-6 rounded" style="background: #86efac" />
|
|
<div class="w-6 h-6 rounded" style="background: #1a2820" />
|
|
</div>
|
|
<p class="text-xs text-[var(--brand-text)] font-medium group-hover:text-[var(--brand-primary)]">Verde</p>
|
|
</button>
|
|
|
|
<button
|
|
@click="applyPreset('carbon')"
|
|
class="p-4 rounded-lg border-2 border-[var(--brand-border)] hover:border-[var(--brand-primary)] transition-colors group"
|
|
>
|
|
<div class="flex gap-2 mb-2">
|
|
<div class="w-6 h-6 rounded" style="background: #a3a3a3" />
|
|
<div class="w-6 h-6 rounded" style="background: #1a1a1a" />
|
|
</div>
|
|
<p class="text-xs text-[var(--brand-text)] font-medium group-hover:text-[var(--brand-primary)]">Carbón</p>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-6">
|
|
<!-- Fondo Principal -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
|
Fondo Principal
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
v-model="theme.bg"
|
|
type="color"
|
|
class="h-10 w-20 rounded-lg border border-[var(--brand-border)] cursor-pointer"
|
|
/>
|
|
<UInput
|
|
v-model="theme.bg"
|
|
placeholder="#14100b"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
|
Fondo Secundario (Surface)
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
v-model="theme.surface"
|
|
type="color"
|
|
class="h-10 w-20 rounded-lg border border-[var(--brand-border)] cursor-pointer"
|
|
/>
|
|
<UInput
|
|
v-model="theme.surface"
|
|
placeholder="#1f180f"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bordes y Primario -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
|
Color de Bordes
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
v-model="theme.border"
|
|
type="color"
|
|
class="h-10 w-20 rounded-lg border border-[var(--brand-border)] cursor-pointer"
|
|
/>
|
|
<UInput
|
|
v-model="theme.border"
|
|
placeholder="#3a2a16"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
|
Color Primario
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
v-model="theme.primary"
|
|
type="color"
|
|
class="h-10 w-20 rounded-lg border border-[var(--brand-border)] cursor-pointer"
|
|
/>
|
|
<UInput
|
|
v-model="theme.primary"
|
|
placeholder="#e0c080"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Primario Fuerte y Acento -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
|
Color Primario Fuerte
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
v-model="theme.primaryStrong"
|
|
type="color"
|
|
class="h-10 w-20 rounded-lg border border-[var(--brand-border)] cursor-pointer"
|
|
/>
|
|
<UInput
|
|
v-model="theme.primaryStrong"
|
|
placeholder="#c08040"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
|
Color de Acento
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
v-model="theme.accent"
|
|
type="color"
|
|
class="h-10 w-20 rounded-lg border border-[var(--brand-border)] cursor-pointer"
|
|
/>
|
|
<UInput
|
|
v-model="theme.accent"
|
|
placeholder="#ffe0a0"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Texto y Texto Muted -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
|
Color de Texto
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
v-model="theme.text"
|
|
type="color"
|
|
class="h-10 w-20 rounded-lg border border-[var(--brand-border)] cursor-pointer"
|
|
/>
|
|
<UInput
|
|
v-model="theme.text"
|
|
placeholder="#fef9f0"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
|
Color de Texto Secundario
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
v-model="theme.textMuted"
|
|
type="color"
|
|
class="h-10 w-20 rounded-lg border border-[var(--brand-border)] cursor-pointer"
|
|
/>
|
|
<UInput
|
|
v-model="theme.textMuted"
|
|
placeholder="#d8c7a6"
|
|
class="flex-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Acciones -->
|
|
<div class="space-y-3 pt-4 border-t border-[var(--brand-border)]">
|
|
<!-- Botones principales -->
|
|
<div class="flex justify-between items-center">
|
|
<UButton
|
|
color="neutral"
|
|
variant="ghost"
|
|
icon="i-lucide-rotate-ccw"
|
|
@click="handleResetTheme"
|
|
>
|
|
Restaurar por defecto
|
|
</UButton>
|
|
<UButton
|
|
color="primary"
|
|
icon="i-lucide-save"
|
|
@click="handleSaveTheme"
|
|
>
|
|
Guardar cambios
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- Botones de import/export -->
|
|
<div class="flex gap-2">
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-download"
|
|
@click="handleExportTheme"
|
|
class="flex-1"
|
|
>
|
|
Exportar tema
|
|
</UButton>
|
|
<UButton
|
|
color="neutral"
|
|
variant="outline"
|
|
icon="i-lucide-upload"
|
|
@click="showImportModal = true"
|
|
class="flex-1"
|
|
>
|
|
Importar tema
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Modal de Importar Tema -->
|
|
<UModal v-model="showImportModal" title="Importar Tema">
|
|
<div class="p-4 space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-[var(--brand-text)] mb-2">
|
|
Pega el JSON del tema
|
|
</label>
|
|
<UTextarea
|
|
v-model="importJsonInput"
|
|
:rows="10"
|
|
placeholder='{\n "bg": "#14100b",\n "surface": "#1f180f",\n ...\n}'
|
|
class="font-mono text-xs"
|
|
/>
|
|
</div>
|
|
<div class="flex justify-end gap-2">
|
|
<UButton
|
|
color="neutral"
|
|
variant="ghost"
|
|
@click="showImportModal = false"
|
|
>
|
|
Cancelar
|
|
</UButton>
|
|
<UButton
|
|
color="primary"
|
|
@click="handleImportTheme"
|
|
>
|
|
Importar
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</UModal>
|
|
|
|
<!-- Vista previa de colores -->
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex items-center gap-3">
|
|
<UIcon name="i-lucide-eye" class="size-5 text-[var(--brand-primary)]" />
|
|
<h3 class="font-semibold text-[var(--brand-text)]">
|
|
Vista Previa
|
|
</h3>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div class="space-y-2">
|
|
<div class="h-20 rounded-lg border-2 border-[var(--brand-border)]" :style="{ backgroundColor: theme.bg }" />
|
|
<p class="text-xs text-center text-[var(--brand-text-muted)]">Fondo Principal</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<div class="h-20 rounded-lg border-2 border-[var(--brand-border)]" :style="{ backgroundColor: theme.surface }" />
|
|
<p class="text-xs text-center text-[var(--brand-text-muted)]">Surface</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<div class="h-20 rounded-lg border-2 border-[var(--brand-border)]" :style="{ backgroundColor: theme.primary }" />
|
|
<p class="text-xs text-center text-[var(--brand-text-muted)]">Primario</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<div class="h-20 rounded-lg border-2 border-[var(--brand-border)]" :style="{ backgroundColor: theme.accent }" />
|
|
<p class="text-xs text-center text-[var(--brand-text-muted)]">Acento</p>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Otras configuraciones (placeholder) -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-lg bg-amber-50 dark:bg-amber-950/30 flex items-center justify-center">
|
|
<UIcon name="i-lucide-bell" class="size-5 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<h3 class="font-semibold text-[var(--brand-text)]">
|
|
Notificaciones
|
|
</h3>
|
|
</div>
|
|
</template>
|
|
<p class="text-sm text-[var(--brand-text-muted)]">
|
|
Gestión de alertas y comunicaciones (Próximamente)
|
|
</p>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-lg bg-green-50 dark:bg-green-950/30 flex items-center justify-center">
|
|
<UIcon name="i-lucide-shield" class="size-5 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
<h3 class="font-semibold text-[var(--brand-text)]">
|
|
Privacidad
|
|
</h3>
|
|
</div>
|
|
</template>
|
|
<p class="text-sm text-[var(--brand-text-muted)]">
|
|
Control de datos y seguridad (Próximamente)
|
|
</p>
|
|
</UCard>
|
|
</div>
|
|
</div>
|
|
</UDashboardPanelContent>
|
|
</UDashboardPanel>
|
|
</UDashboardLayout>
|
|
</template>
|