mejoras UI/UX potentes
This commit is contained in:
122
nuxt4-app/app/components/clientes/ClienteCard.vue
Normal file
122
nuxt4-app/app/components/clientes/ClienteCard.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<!-- nuxt4-app/app/components/clientes/ClienteCard.vue -->
|
||||
<template>
|
||||
<div class="relative overflow-hidden rounded-lg border border-gray-400/30 bg-gradient-to-br from-gray-100/10 to-gray-300/5 backdrop-blur-sm p-5 shadow-lg transition-all duration-300 hover:border-gray-300/50 hover:shadow-xl">
|
||||
<!-- Header with Avatar and Name -->
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="h-14 w-14 rounded-full bg-gradient-to-br from-gray-300/40 to-gray-500/20 flex items-center justify-center text-xl font-bold text-gray-200 border border-gray-400/30">
|
||||
{{ clienteInitials }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg font-bold text-gray-100 truncate mb-1">{{ cliente.name }}</h3>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<span v-if="cliente.cedula" class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-gray-500/20 text-gray-300 border border-gray-400/20">
|
||||
<UIcon name="i-lucide-credit-card" class="w-3 h-3" />
|
||||
{{ cliente.cedula }}
|
||||
</span>
|
||||
<span v-if="cliente.empleado" class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-blue-500/20 text-blue-300 border border-blue-400/30">
|
||||
<UIcon name="i-lucide-briefcase" class="w-3 h-3" />
|
||||
Empleado
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Remove Button -->
|
||||
<button
|
||||
@click="$emit('remove')"
|
||||
class="flex-shrink-0 p-1.5 rounded-full hover:bg-red-500/20 text-gray-400 hover:text-red-400 transition-colors"
|
||||
title="Quitar cliente"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Details Grid -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div v-if="cliente.ubicacion" class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-map-pin" class="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs text-gray-500 uppercase tracking-wide">Ubicación</div>
|
||||
<div class="text-sm text-gray-200 truncate">{{ cliente.ubicacion }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="cliente.telefono" class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-phone" class="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs text-gray-500 uppercase tracking-wide">Teléfono</div>
|
||||
<div class="text-sm text-gray-200 truncate">{{ cliente.telefono }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="cliente.grupo_estudio" class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-users" class="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs text-gray-500 uppercase tracking-wide">Grupo</div>
|
||||
<div class="text-sm text-gray-200 truncate">{{ cliente.grupo_estudio }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="cliente.idciat" class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-hash" class="w-4 h-4 text-gray-400 mt-0.5 flex-shrink-0" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs text-gray-500 uppercase tracking-wide">ID CIAT</div>
|
||||
<div class="text-sm text-gray-200">{{ cliente.idciat }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer with metadata -->
|
||||
<div class="mt-4 pt-3 border-t border-gray-400/20 flex items-center justify-between text-xs text-gray-500">
|
||||
<span>ID: {{ cliente.id }}</span>
|
||||
<span v-if="cliente.created_at">Desde {{ formatDate(cliente.created_at) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Decorative gradient overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-tr from-transparent via-transparent to-white/5 pointer-events-none rounded-lg"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ClienteRecord {
|
||||
id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
name: string
|
||||
cedula?: number
|
||||
ubicacion?: string
|
||||
grupo_estudio?: string
|
||||
empleado?: boolean
|
||||
avatar_url?: string
|
||||
telefono?: string
|
||||
idciat?: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cliente: ClienteRecord
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
defineEmits<{
|
||||
remove: []
|
||||
}>()
|
||||
|
||||
// Compute initials from name
|
||||
const clienteInitials = computed(() => {
|
||||
const names = props.cliente.name.trim().split(' ')
|
||||
if (names.length >= 2) {
|
||||
return (names[0][0] + names[1][0]).toUpperCase()
|
||||
}
|
||||
return names[0].substring(0, 2).toUpperCase()
|
||||
})
|
||||
|
||||
// Format date helper
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: 'short'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -34,6 +34,18 @@
|
||||
<MetadatosCard v-if="clientesMetadata" :metadata="clientesMetadata" />
|
||||
</div>
|
||||
|
||||
<!-- Clientes Seleccionados Cards -->
|
||||
<div v-if="clientesSeleccionados.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<TransitionGroup name="cliente-card">
|
||||
<ClientesClienteCard
|
||||
v-for="cliente in clientesSeleccionados"
|
||||
:key="cliente.id"
|
||||
:cliente="cliente"
|
||||
@remove="removeCliente(cliente.id)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<!-- 🔻 Card de Filtros -->
|
||||
<UCard class="brand-card border border-transparent">
|
||||
<template #header>
|
||||
@@ -273,6 +285,17 @@ function isClienteSelected(clienteId: number): boolean {
|
||||
return selectedClienteIds.value.includes(clienteId)
|
||||
}
|
||||
|
||||
// Get selected clientes for display cards
|
||||
const clientesSeleccionados = computed((): ClienteRecord[] => {
|
||||
if (selectedClienteIds.value.length === 0) return []
|
||||
return (clientes.value ?? []).filter(c => selectedClienteIds.value.includes(c.id))
|
||||
})
|
||||
|
||||
// Remove a cliente from selection
|
||||
function removeCliente(clienteId: number) {
|
||||
selectedClienteIds.value = selectedClienteIds.value.filter(id => id !== clienteId)
|
||||
}
|
||||
|
||||
// Filtrados que alimentan los métricos
|
||||
const ingresosFiltrados = computed(() => {
|
||||
return (ingresos.value ?? [])
|
||||
@@ -350,3 +373,24 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cliente-card-enter-active,
|
||||
.cliente-card-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cliente-card-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-10px);
|
||||
}
|
||||
|
||||
.cliente-card-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(10px);
|
||||
}
|
||||
|
||||
.cliente-card-move {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user