- Clientes ordenados por monto total de ingresos
-
+
+
+
Top 10 Clientes
+
+ Clientes ordenados por monto total de ingresos
+
+
+
+
+ Copiar Texto
+
+
+ Copiar JSON
+
+
@@ -491,11 +539,35 @@
-
-
Serie Temporal Acumulada
-
- Evolución de ingresos en el tiempo ({{ data.serieTemporal.length }} puntos de datos)
-
+
+
+
Serie Temporal Acumulada
+
+ Evolución de ingresos en el tiempo ({{ data.serieTemporal.length }} puntos de datos)
+
+
+
+
+ Copiar Texto
+
+
+ Copiar JSON
+
+
@@ -773,6 +845,97 @@ async function loadOpcionesFiltros() {
}
}
+// Funciones de copia
+async function copiarListaIngresosTexto() {
+ if (!data.value?.listaIngresos || data.value.listaIngresos.length === 0) return
+
+ const texto = `📊 LISTA DE INGRESOS - ${data.value.listaIngresos.length} registros
+Rango: ${rangoLegible.value}
+Generado: ${lastUpdated.value}
+
+${data.value.listaIngresos.map((ing, idx) => `
+${idx + 1}. ID: ${ing.id}
+ 📅 Fecha: ${ing.created_at ? new Date(ing.created_at).toLocaleDateString('es-ES') : '-'}
+ 👤 Cliente: ${ing.cliente_nombre || '-'}
+ ☕ Tipo: ${ing.tipo || '-'}
+ ⚖️ Peso: ${ing.peso_neto ? ing.peso_neto.toFixed(2) : '-'} lb
+ 💰 Precio: L ${ing.precio ? ing.precio.toFixed(2) : '-'}
+ 💵 Total: L ${ing.total_a_pagar ? ing.total_a_pagar.toFixed(2) : '-'}
+ 📍 Estado: ${ing.estado || '-'}
+`).join('\n')}`
+
+ await navigator.clipboard.writeText(texto)
+ alert('✅ Lista de ingresos copiada al portapapeles')
+}
+
+async function copiarListaIngresosJSON() {
+ if (!data.value?.listaIngresos || data.value.listaIngresos.length === 0) return
+
+ const json = JSON.stringify(data.value.listaIngresos, null, 2)
+ await navigator.clipboard.writeText(json)
+ alert('✅ JSON copiado al portapapeles')
+}
+
+async function copiarTop10ClientesTexto() {
+ if (!data.value?.listaClientes || data.value.listaClientes.length === 0) return
+
+ const top10 = data.value.listaClientes.slice(0, 10)
+ const texto = `🏆 TOP 10 CLIENTES
+Rango: ${rangoLegible.value}
+Generado: ${lastUpdated.value}
+
+${top10.map((cliente, idx) => `
+${idx + 1}. ${cliente.cliente_nombre || 'Sin nombre'}
+ 📄 Cédula: ${cliente.cliente_cedula || 'Sin cédula'}
+ 📍 Ubicación: ${cliente.cliente_ubicacion || 'Sin ubicación'}
+ 💰 Total Pagado: L ${cliente.total_pagado ? cliente.total_pagado.toFixed(2) : '0.00'}
+ 📦 Ingresos: ${cliente.num_ingresos || 0}
+ ⚖️ Quintales: ${cliente.total_qq_seco ? cliente.total_qq_seco.toFixed(2) : '0.00'} qq
+`).join('\n')}`
+
+ await navigator.clipboard.writeText(texto)
+ alert('✅ Top 10 clientes copiado al portapapeles')
+}
+
+async function copiarTop10ClientesJSON() {
+ if (!data.value?.listaClientes || data.value.listaClientes.length === 0) return
+
+ const top10 = data.value.listaClientes.slice(0, 10)
+ const json = JSON.stringify(top10, null, 2)
+ await navigator.clipboard.writeText(json)
+ alert('✅ JSON copiado al portapapeles')
+}
+
+async function copiarSerieTemporalTexto() {
+ if (!data.value?.serieTemporal || data.value.serieTemporal.length === 0) return
+
+ const texto = `📈 SERIE TEMPORAL ACUMULADA - ${data.value.serieTemporal.length} puntos
+Rango: ${rangoLegible.value}
+Generado: ${lastUpdated.value}
+
+${data.value.serieTemporal.map((punto, idx) => `
+${idx + 1}. 📅 ${punto.fecha_grupo ? new Date(punto.fecha_grupo).toLocaleDateString('es-ES') : '-'}
+ ☕ Tipo: ${punto.tipo || '-'}
+ 📍 Estado: ${punto.estado || '-'}
+ 📦 Ingresos: ${punto.num_ingresos_periodo || 0}
+ ⚖️ Peso Seco: ${punto.peso_seco_periodo ? punto.peso_seco_periodo.toFixed(2) : '0.00'} qq
+ 💰 Inversión: L ${punto.inversion_periodo ? punto.inversion_periodo.toFixed(2) : '0.00'}
+ 📊 Acumulado: ${punto.peso_seco_acumulado ? punto.peso_seco_acumulado.toFixed(2) : '0.00'} qq
+ 💵 Total Acum: L ${punto.inversion_acumulada ? punto.inversion_acumulada.toFixed(2) : '0.00'}
+`).join('\n')}`
+
+ await navigator.clipboard.writeText(texto)
+ alert('✅ Serie temporal copiada al portapapeles')
+}
+
+async function copiarSerieTemporalJSON() {
+ if (!data.value?.serieTemporal || data.value.serieTemporal.length === 0) return
+
+ const json = JSON.stringify(data.value.serieTemporal, null, 2)
+ await navigator.clipboard.writeText(json)
+ alert('✅ JSON copiado al portapapeles')
+}
+
// Inicializar preset por defecto sin cargar datos
onMounted(async () => {
// Default preset: cosecha 25-26
diff --git a/nuxt4-app/app/pages/settings.vue b/nuxt4-app/app/pages/settings.vue
index d5d5a8e..4e7d4fd 100644
--- a/nuxt4-app/app/pages/settings.vue
+++ b/nuxt4-app/app/pages/settings.vue
@@ -1,68 +1,91 @@
+```
+
+## Checklist Pre-Commit
+
+Antes de hacer commit de tu código, verifica:
+
+- [ ] **No hay colores hardcoded** (`#ffffff`, `#000000`, etc.)
+- [ ] **No hay clases gray-scale genéricas** (`bg-gray-900`, `text-gray-500`, etc.)
+- [ ] **Todos los fondos usan** `--brand-bg` o `--brand-surface`
+- [ ] **Todos los bordes usan** `--brand-border`
+- [ ] **Todo el texto usa** `--brand-text` o `--brand-text-muted`
+- [ ] **Los elementos interactivos usan** `--brand-primary` o `--brand-accent`
+- [ ] **Los hover states usan transparencias** (ej: `hover:bg-[var(--brand-primary)]/10`)
+- [ ] **Se usan clases utilitarias** cuando sea posible (`.brand-card`, `.brand-shell`, etc.)
+
+## Casos Especiales
+
+### Colores Semánticos (Success, Error, Warning)
+
+Para colores que NO son parte del tema (éxito, error, advertencia), puedes usar colores de Tailwind:
+
+```vue
+
+
Éxito
+
Error
+
Advertencia
+
Info
+
+
+
+ Operación exitosa
+
+```
+
+### Iconos
+
+Los iconos deben usar colores del tema:
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+### Imágenes y Avatares con Bordes
+
+```vue
+
+
+
+```
+
+### Hover y Focus States
+
+Siempre usa transparencias para hover states para que funcionen con cualquier tema:
+
+```vue
+
+
+
+
+
+```
+
+## Ejemplos de Migración
+
+### Antes (Incorrecto)
+
+```vue
+
+
+
Título
+
+ Descripción
+
+
+
+
+```
+
+### Después (Correcto)
+
+```vue
+
+
+
Título
+
+ Descripción
+
+
+ Acción
+
+
+
+```
+
+## Errores Comunes y Soluciones
+
+### Error 1: Mezclar Variables con Hardcoded Colors
+
+**❌ No hagas esto:**
+```vue
+
+ Contenido
+
+```
+
+**✅ Haz esto:**
+```vue
+
+ Contenido
+
+```
+
+### Error 2: No Considerar el Contraste
+
+Asegúrate de que el texto tenga suficiente contraste sobre su fondo:
+
+**❌ Mal contraste:**
+```vue
+
+ Texto importante
+
+```
+
+**✅ Buen contraste:**
+```vue
+
+ Texto importante
+
+```
+
+### Error 3: Duplicar Estilos en Lugar de Usar Clases
+
+**❌ Duplicación:**
+```vue
+
+ Card 1
+
+
+ Card 2
+
+```
+
+**✅ Reutilización:**
+```vue
+
Card 1
+
Card 2
+```
+
+## Testing del Tema
+
+Para probar que tu componente respeta el tema:
+
+1. Ve a `/settings`
+2. Cambia el tema a "Azul Corporativo" o "Verde Natural"
+3. Navega a tu componente
+4. Verifica que todos los colores cambien correctamente
+5. Verifica que no haya elementos con colores hardcoded que no cambiaron
+
+## Recursos Adicionales
+
+- **Documentación del usuario:** `THEME_CUSTOMIZATION.md`
+- **Composable de temas:** `app/composables/useTheme.ts`
+- **Variables CSS:** `app/assets/css/main.css`
+- **Configuración de Nuxt UI:** `app.config.ts`
+
+## Preguntas Frecuentes
+
+### ¿Puedo usar colores de Tailwind como `bg-blue-500`?
+
+Solo para estados semánticos (éxito, error, advertencia) que NO forman parte del tema. Para todo lo demás, usa las variables de tema.
+
+### ¿Cómo personalizo un componente de Nuxt UI?
+
+Usa la prop `:ui` con las variables de tema:
+
+```vue
+
+ Contenido
+
+```
+
+### ¿Qué hago si necesito un color que no está en el tema?
+
+Primero pregunta: ¿realmente necesito un color nuevo, o puedo usar uno existente? Si es absolutamente necesario, propón agregar una nueva variable al sistema de temas en lugar de hardcodear el color.
+
+### ¿Cómo manejo dark mode?
+
+No necesitas manejarlo manualmente. Las variables CSS ya están optimizadas para dark mode. Solo usa las variables y funcionará automáticamente.
+
+---
+
+**Última actualización:** 2025-10-30
+**Mantenedor:** Claude Code para Núcleo Río Frío
+**Versión:** 1.0.0