All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 58s
- Crear página principal /informe-comercios con todos los estados (loading, error, initial, main) - Implementar componente TotalesMonetariosComercio con distribución de pagos y gráficas - Implementar componente TotalesPesoComercio con totales por tipo de café - Implementar componente TablaComerciosResumen con paginación y exportación - Agregar navegación en sidebar para Informe de Comercios con icono receipt - Integrar filtros avanzados (fechas, clientes, ubicaciones, tipos) - Incluir sistema de alertas para cambios pendientes y comercios anulados - Agregar funciones de copiar a texto/JSON en todos los componentes
225 lines
9.8 KiB
Vue
225 lines
9.8 KiB
Vue
<template>
|
|
<UCard class="brand-card border border-transparent">
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-xl font-bold brand-section-title">Totales Monetarios</h2>
|
|
<div class="flex items-center gap-2">
|
|
<UButton
|
|
size="xs"
|
|
color="gray"
|
|
variant="soft"
|
|
icon="i-lucide-copy"
|
|
@click="copiarTexto"
|
|
>
|
|
Copiar Texto
|
|
</UButton>
|
|
<UButton
|
|
size="xs"
|
|
color="gray"
|
|
variant="soft"
|
|
icon="i-lucide-braces"
|
|
@click="copiarJSON"
|
|
>
|
|
Copiar JSON
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="space-y-6">
|
|
<!-- Inversión Total y Métricas Principales -->
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-[var(--brand-primary)] mb-3">Inversión Total</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
|
<MetricBox label="Total Invertido" :value="formatCurrency(data.total_invertido)" color="green" />
|
|
<MetricBox label="Total QQ Seco" :value="`${formatNumber(data.total_qq_seco)} QQ`" />
|
|
<MetricBox label="Precio Promedio/QQ" :value="formatCurrency(data.precio_promedio_por_qq)" color="blue" />
|
|
<MetricBox label="Número de Comercios" :value="data.num_comercios?.toString() || '0'" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Distribución de Pagos -->
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-[var(--brand-primary)] mb-3">Distribución de Pagos</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
|
<div class="rounded-lg border border-[var(--brand-border)] bg-[var(--brand-surface)] px-4 py-3">
|
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">Efectivo</div>
|
|
<div class="text-lg font-bold text-green-400">
|
|
{{ formatCurrency(data.total_efectivo) }}
|
|
</div>
|
|
<div class="text-xs text-[var(--brand-text-muted)] mt-1">
|
|
{{ calcularPorcentaje(data.total_efectivo, data.total_invertido) }}%
|
|
</div>
|
|
</div>
|
|
<div class="rounded-lg border border-[var(--brand-border)] bg-[var(--brand-surface)] px-4 py-3">
|
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">Depósito</div>
|
|
<div class="text-lg font-bold text-blue-400">
|
|
{{ formatCurrency(data.total_deposito) }}
|
|
</div>
|
|
<div class="text-xs text-[var(--brand-text-muted)] mt-1">
|
|
{{ calcularPorcentaje(data.total_deposito, data.total_invertido) }}%
|
|
</div>
|
|
</div>
|
|
<div class="rounded-lg border border-[var(--brand-border)] bg-[var(--brand-surface)] px-4 py-3">
|
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">Cheque</div>
|
|
<div class="text-lg font-bold text-purple-400">
|
|
{{ formatCurrency(data.total_cheque) }}
|
|
</div>
|
|
<div class="text-xs text-[var(--brand-text-muted)] mt-1">
|
|
{{ calcularPorcentaje(data.total_cheque, data.total_invertido) }}%
|
|
</div>
|
|
</div>
|
|
<div class="rounded-lg border border-[var(--brand-border)] bg-[var(--brand-surface)] px-4 py-3">
|
|
<div class="text-xs text-[var(--brand-text-muted)] uppercase tracking-wide mb-1">Total Verificación</div>
|
|
<div class="text-lg font-bold" :class="totalMatch ? 'text-green-500' : 'text-red-500'">
|
|
{{ formatCurrency(totalDistribucion) }}
|
|
</div>
|
|
<div class="text-xs mt-1" :class="totalMatch ? 'text-green-500' : 'text-red-500'">
|
|
{{ totalMatch ? '✓ Coincide' : '✗ No coincide' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gráfica de Distribución -->
|
|
<div v-if="showChart" class="mt-6">
|
|
<h4 class="text-sm font-semibold text-[var(--brand-text)] mb-3">Visualización de Distribución</h4>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-xs w-20 text-[var(--brand-text-muted)]">Efectivo</span>
|
|
<div class="flex-1 h-8 bg-[var(--brand-surface)] rounded-lg overflow-hidden border border-[var(--brand-border)]">
|
|
<div
|
|
class="h-full bg-green-500/60 flex items-center justify-end px-2 transition-all duration-500"
|
|
:style="{ width: `${calcularPorcentaje(data.total_efectivo, data.total_invertido)}%` }"
|
|
>
|
|
<span class="text-xs font-semibold text-white">{{ calcularPorcentaje(data.total_efectivo, data.total_invertido) }}%</span>
|
|
</div>
|
|
</div>
|
|
<span class="text-xs w-32 text-right text-[var(--brand-text)]">{{ formatCurrency(data.total_efectivo) }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-xs w-20 text-[var(--brand-text-muted)]">Depósito</span>
|
|
<div class="flex-1 h-8 bg-[var(--brand-surface)] rounded-lg overflow-hidden border border-[var(--brand-border)]">
|
|
<div
|
|
class="h-full bg-blue-500/60 flex items-center justify-end px-2 transition-all duration-500"
|
|
:style="{ width: `${calcularPorcentaje(data.total_deposito, data.total_invertido)}%` }"
|
|
>
|
|
<span class="text-xs font-semibold text-white">{{ calcularPorcentaje(data.total_deposito, data.total_invertido) }}%</span>
|
|
</div>
|
|
</div>
|
|
<span class="text-xs w-32 text-right text-[var(--brand-text)]">{{ formatCurrency(data.total_deposito) }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-xs w-20 text-[var(--brand-text-muted)]">Cheque</span>
|
|
<div class="flex-1 h-8 bg-[var(--brand-surface)] rounded-lg overflow-hidden border border-[var(--brand-border)]">
|
|
<div
|
|
class="h-full bg-purple-500/60 flex items-center justify-end px-2 transition-all duration-500"
|
|
:style="{ width: `${calcularPorcentaje(data.total_cheque, data.total_invertido)}%` }"
|
|
>
|
|
<span class="text-xs font-semibold text-white">{{ calcularPorcentaje(data.total_cheque, data.total_invertido) }}%</span>
|
|
</div>
|
|
</div>
|
|
<span class="text-xs w-32 text-right text-[var(--brand-text)]">{{ formatCurrency(data.total_cheque) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const props = defineProps<{
|
|
data: {
|
|
total_invertido: number
|
|
total_qq_seco: number
|
|
precio_promedio_por_qq: number
|
|
total_efectivo: number
|
|
total_deposito: number
|
|
total_cheque: number
|
|
num_comercios: number
|
|
}
|
|
contadores?: {
|
|
total_comercios?: number
|
|
comercios_filtrados?: number
|
|
total_clientes?: number
|
|
clientes_con_comercios_filtrados?: number
|
|
}
|
|
rangoLegible: string
|
|
lastUpdated: string
|
|
}>()
|
|
|
|
const showChart = ref(true)
|
|
|
|
// Computed
|
|
const totalDistribucion = computed(() => {
|
|
return (props.data.total_efectivo || 0) +
|
|
(props.data.total_deposito || 0) +
|
|
(props.data.total_cheque || 0)
|
|
})
|
|
|
|
const totalMatch = computed(() => {
|
|
const diff = Math.abs(totalDistribucion.value - (props.data.total_invertido || 0))
|
|
return diff < 0.01 // Tolerancia de 1 centavo por redondeo
|
|
})
|
|
|
|
const formatCurrency = (value: number) => {
|
|
if (!value) return 'L 0.00'
|
|
return new Intl.NumberFormat('es-HN', {
|
|
style: 'currency',
|
|
currency: 'HNL',
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}).format(value).replace('HNL', 'L')
|
|
}
|
|
|
|
const formatNumber = (value: number) => {
|
|
if (!value) return '0.00'
|
|
return new Intl.NumberFormat('es-HN', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}).format(value)
|
|
}
|
|
|
|
const calcularPorcentaje = (parte: number, total: number): string => {
|
|
if (!total || total === 0) return '0.0'
|
|
const porcentaje = (parte / total) * 100
|
|
return porcentaje.toFixed(1)
|
|
}
|
|
|
|
async function copiarTexto() {
|
|
const footer = `
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
📊 RESUMEN
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
📅 Rango: ${props.rangoLegible}
|
|
📦 Comercios: ${props.contadores?.comercios_filtrados || 0} de ${props.contadores?.total_comercios || 0} registros
|
|
👥 Clientes: ${props.contadores?.clientes_con_comercios_filtrados || 0} de ${props.contadores?.total_clientes || 0} clientes
|
|
🕐 Generado: ${props.lastUpdated}`
|
|
|
|
const texto = `💰 TOTALES MONETARIOS DE COMERCIOS
|
|
|
|
💵 INVERSIÓN TOTAL:
|
|
Total Invertido: ${formatCurrency(props.data.total_invertido)}
|
|
Total QQ Seco: ${formatNumber(props.data.total_qq_seco)} QQ
|
|
Precio Promedio/QQ: ${formatCurrency(props.data.precio_promedio_por_qq)}
|
|
Número de Comercios: ${props.data.num_comercios}
|
|
|
|
💳 DISTRIBUCIÓN DE PAGOS:
|
|
Efectivo: ${formatCurrency(props.data.total_efectivo)} (${calcularPorcentaje(props.data.total_efectivo, props.data.total_invertido)}%)
|
|
Depósito: ${formatCurrency(props.data.total_deposito)} (${calcularPorcentaje(props.data.total_deposito, props.data.total_invertido)}%)
|
|
Cheque: ${formatCurrency(props.data.total_cheque)} (${calcularPorcentaje(props.data.total_cheque, props.data.total_invertido)}%)
|
|
|
|
Total Distribución: ${formatCurrency(totalDistribucion.value)}
|
|
Verificación: ${totalMatch.value ? '✓ Coincide con total invertido' : '✗ No coincide con total invertido'}${footer}`
|
|
|
|
await navigator.clipboard.writeText(texto)
|
|
alert('✅ Totales Monetarios copiados al portapapeles')
|
|
}
|
|
|
|
async function copiarJSON() {
|
|
const json = JSON.stringify(props.data, null, 2)
|
|
await navigator.clipboard.writeText(json)
|
|
alert('✅ JSON copiado al portapapeles')
|
|
}
|
|
</script>
|