Feat: Agregar botones de Copiar Texto y Copiar JSON
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
This commit is contained in:
2025-10-30 16:33:54 -06:00
parent 507fb9ba1c
commit 63c7043664
10 changed files with 1143 additions and 134 deletions

View File

@@ -1,68 +1,91 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { ThemeColors } from '~/composables/useTheme'
definePageMeta({
layout: 'dashboard',
title: 'Configuración'
})
// Colores por defecto (actuales del tema café)
const defaultTheme = {
bg: '#14100b',
surface: '#1f180f',
border: '#3a2a16',
primary: '#e0c080',
primaryStrong: '#c08040',
accent: '#ffe0a0',
text: '#fef9f0',
textMuted: '#d8c7a6'
}
// Usar el composable de temas
const {
theme,
defaultTheme,
applyTheme,
saveTheme: saveThemeComposable,
resetTheme: resetThemeComposable,
exportTheme,
importTheme
} = useTheme()
// Estado del tema (inicializar con valores actuales del CSS)
const theme = ref({ ...defaultTheme })
// Cargar tema guardado en localStorage
onMounted(() => {
const savedTheme = localStorage.getItem('custom-theme')
if (savedTheme) {
try {
theme.value = JSON.parse(savedTheme)
applyTheme(theme.value)
} catch (e) {
console.error('Error al cargar tema guardado', e)
}
// 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 tema al documento
const applyTheme = (themeColors: typeof defaultTheme) => {
const root = document.documentElement
root.style.setProperty('--brand-bg', themeColors.bg)
root.style.setProperty('--brand-surface', themeColors.surface)
root.style.setProperty('--brand-border', themeColors.border)
root.style.setProperty('--brand-primary', themeColors.primary)
root.style.setProperty('--brand-primary-strong', themeColors.primaryStrong)
root.style.setProperty('--brand-accent', themeColors.accent)
root.style.setProperty('--brand-text', themeColors.text)
root.style.setProperty('--brand-text-muted', themeColors.textMuted)
}
// Guardar tema
const saveTheme = () => {
localStorage.setItem('custom-theme', JSON.stringify(theme.value))
// Aplicar preset de tema
const applyPreset = (presetName: keyof typeof presetThemes) => {
const preset = presetThemes[presetName]
theme.value = { ...preset }
applyTheme(theme.value)
useToast().add({
title: 'Tema guardado',
description: 'Los cambios se aplicaron correctamente',
color: 'green'
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 resetTheme = () => {
theme.value = { ...defaultTheme }
localStorage.removeItem('custom-theme')
applyTheme(theme.value)
const handleResetTheme = () => {
resetThemeComposable()
useToast().add({
title: 'Tema reseteado',
description: 'Se restauraron los colores por defecto',
@@ -70,8 +93,50 @@ const resetTheme = () => {
})
}
// Vista previa en tiempo real (opcional)
const livePreview = ref(false)
// 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)
@@ -110,6 +175,58 @@ watch(() => theme.value, (newTheme) => {
</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">
@@ -268,26 +385,83 @@ watch(() => theme.value, (newTheme) => {
</div>
<!-- Acciones -->
<div class="flex justify-between items-center pt-4 border-t border-[var(--brand-border)]">
<UButton
color="neutral"
variant="ghost"
icon="i-lucide-rotate-ccw"
@click="resetTheme"
>
Restaurar por defecto
</UButton>
<UButton
color="primary"
icon="i-lucide-save"
@click="saveTheme"
>
Guardar cambios
</UButton>
<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>