netgescon-day0/docs/90-UI-interfaccia-unica/05-CARDS.md

22 KiB
Executable File
Raw Permalink Blame History

🃏 CARDS - Documentazione Completa NetGescon

Sistema Cards dell'interfaccia universale
📅 Creato: 21/07/2025
🔄 Ultimo aggiornamento: 21/07/2025
🎯 Priorità: ALTO


📋 OVERVIEW CARDS SYSTEM

Il Sistema Cards NetGescon fornisce COMPONENTI MODULARI per:

  • 📊 Dashboard Stats Cards
  • 📋 Info Cards
  • 🎯 Action Cards
  • 📈 Chart Cards
  • 📄 Content Cards
  • ⚠️ Alert Cards

🎨 Design Philosophy

  • Consistenza Visiva - Stile uniforme in tutta l'app
  • Interattività - Hover effects e animazioni smooth
  • Responsiveness - Adattamento perfetto a ogni device
  • Accessibilità - WCAG 2.1 AA compliant

🎨 DESIGN SPECIFICATIONS

🌈 Color Palette

/* Base Card Styles */
.netgescon-card {
    background: white;
    border: 1px solid var(--netgescon-gray-200);
    border-radius: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    transition: all 0.3s ease;
}

.netgescon-card:hover {
    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
    transform: translateY(-2px);
}

/* Gradient Cards */
.card-gradient-blue {
    background: linear-gradient(135deg, #ebf4ff 0%, #dbeafe 100%);
    border-color: var(--netgescon-primary-light);
}

.card-gradient-green {
    background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
    border-color: var(--netgescon-success);
}

.card-gradient-yellow {
    background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
    border-color: var(--netgescon-warning);
}

.card-gradient-red {
    background: linear-gradient(135deg, #fef2f2 0%, #fecaca 100%);
    border-color: var(--netgescon-danger);
}

📏 Base Dimensions

/* Standard Card */
.netgescon-card {
    padding: 24px;
    min-height: 120px;
}

/* Compact Card */
.netgescon-card-compact {
    padding: 16px;
    min-height: 80px;
}

/* Large Card */
.netgescon-card-large {
    padding: 32px;
    min-height: 200px;
}

📊 DASHBOARD STATS CARDS

🔢 Stats Card Template

<!-- resources/views/components/cards/stats-card.blade.php -->
@props([
    'title' => '',
    'value' => '',
    'icon' => 'fas fa-chart-line',
    'color' => 'blue',
    'trend' => null,
    'trendDirection' => 'up',
    'subtitle' => '',
    'url' => null
])

@php
$cardClass = "card-gradient-{$color}";
$iconColorClass = match($color) {
    'blue' => 'text-blue-600',
    'green' => 'text-green-600', 
    'yellow' => 'text-yellow-600',
    'red' => 'text-red-600',
    'purple' => 'text-purple-600',
    default => 'text-gray-600'
};
$bgIconClass = match($color) {
    'blue' => 'bg-blue-500',
    'green' => 'bg-green-500',
    'yellow' => 'bg-yellow-500', 
    'red' => 'bg-red-500',
    'purple' => 'bg-purple-500',
    default => 'bg-gray-500'
};
@endphp

<div class="netgescon-card {{ $cardClass }} {{ $url ? 'cursor-pointer' : '' }}"
     @if($url) onclick="window.location='{{ $url }}'" @endif>
    
    <div class="flex items-center justify-between">
        <!-- Content Section -->
        <div class="flex-1">
            <!-- Title -->
            <p class="{{ $iconColorClass }} text-sm font-medium uppercase tracking-wide mb-1">
                {{ $title }}
            </p>
            
            <!-- Value -->
            <p class="text-3xl font-bold text-gray-900 mb-1">
                {{ $value }}
            </p>
            
            <!-- Trend & Subtitle -->
            <div class="flex items-center space-x-2">
                @if($trend)
                    <span class="flex items-center text-sm {{ $trendDirection === 'up' ? 'text-green-600' : 'text-red-600' }}">
                        <i class="fas fa-arrow-{{ $trendDirection }} mr-1"></i>
                        {{ $trend }}
                    </span>
                @endif
                
                @if($subtitle)
                    <span class="text-sm text-gray-600">{{ $subtitle }}</span>
                @endif
            </div>
        </div>
        
        <!-- Icon Section -->
        <div class="{{ $bgIconClass }} rounded-full p-3 shadow-lg">
            <i class="{{ $icon }} text-white text-xl"></i>
        </div>
    </div>
</div>

💻 Usage Examples

<!-- In Dashboard View -->
<div class="stats-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
    <x-cards.stats-card 
        title="Stabili Gestiti"
        value="{{ $stats['stabili'] }}"
        icon="fas fa-building"
        color="blue"
        trend="+3"
        trendDirection="up"
        subtitle="questo mese"
        url="{{ route('admin.stabili.index') }}"
    />
    
    <x-cards.stats-card 
        title="Condomini Attivi"
        value="{{ $stats['condomini'] }}"
        icon="fas fa-home"
        color="green"
        trend="+8"
        trendDirection="up"
        subtitle="questo mese"
        url="{{ route('admin.condomini.index') }}"
    />
    
    <x-cards.stats-card 
        title="Unità Immobiliari"
        value="{{ number_format($stats['unita']) }}"
        icon="fas fa-door-open"
        color="purple"
        trend="0"
        subtitle="nessuna variazione"
        url="{{ route('admin.unita.index') }}"
    />
    
    <x-cards.stats-card 
        title="Fatturato Mensile"
        value="€{{ number_format($stats['fatturato'], 0, ',', '.') }}"
        icon="fas fa-euro-sign"
        color="yellow"
        trend="+12%"
        trendDirection="up"
        subtitle="vs mese scorso"
        url="{{ route('admin.reports.financial') }}"
    />
</div>

📋 INFO CARDS

Info Card Template

<!-- resources/views/components/cards/info-card.blade.php -->
@props([
    'title' => '',
    'content' => '',
    'icon' => 'fas fa-info-circle',
    'type' => 'info', // info, success, warning, danger
    'dismissible' => false
])

@php
$typeClasses = [
    'info' => 'bg-blue-50 border-blue-200 text-blue-800',
    'success' => 'bg-green-50 border-green-200 text-green-800',
    'warning' => 'bg-yellow-50 border-yellow-200 text-yellow-800',
    'danger' => 'bg-red-50 border-red-200 text-red-800'
];
$iconClasses = [
    'info' => 'text-blue-600',
    'success' => 'text-green-600',
    'warning' => 'text-yellow-600',
    'danger' => 'text-red-600'
];
@endphp

<div class="netgescon-card {{ $typeClasses[$type] }} border">
    <div class="flex items-start">
        <!-- Icon -->
        <div class="flex-shrink-0">
            <i class="{{ $icon }} {{ $iconClasses[$type] }} text-xl"></i>
        </div>
        
        <!-- Content -->
        <div class="ml-3 flex-1">
            @if($title)
                <h4 class="font-medium mb-1">{{ $title }}</h4>
            @endif
            
            <div class="text-sm">
                {!! $content !!}
            </div>
        </div>
        
        <!-- Dismiss Button -->
        @if($dismissible)
            <button class="ml-4 text-current hover:opacity-75 transition-opacity"
                    onclick="this.parentElement.parentElement.remove()">
                <i class="fas fa-times"></i>
            </button>
        @endif
    </div>
</div>

🎯 ACTION CARDS

Action Card Template

<!-- resources/views/components/cards/action-card.blade.php -->
@props([
    'title' => '',
    'description' => '',
    'icon' => 'fas fa-plus',
    'buttonText' => 'Azione',
    'buttonUrl' => '#',
    'buttonColor' => 'primary',
    'image' => null
])

<div class="netgescon-card text-center">
    <!-- Image or Icon -->
    @if($image)
        <img src="{{ $image }}" alt="{{ $title }}" class="w-16 h-16 mx-auto mb-4 rounded-full">
    @else
        <div class="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
            <i class="{{ $icon }} text-gray-600 text-2xl"></i>
        </div>
    @endif
    
    <!-- Title -->
    <h3 class="text-lg font-semibold text-gray-900 mb-2">{{ $title }}</h3>
    
    <!-- Description -->
    @if($description)
        <p class="text-gray-600 text-sm mb-6">{{ $description }}</p>
    @endif
    
    <!-- Action Button -->
    <a href="{{ $buttonUrl }}" 
       class="btn btn-{{ $buttonColor }} inline-flex items-center">
        <i class="{{ $icon }} mr-2"></i>
        {{ $buttonText }}
    </a>
</div>

📈 CHART CARDS

📊 Chart Card Template

<!-- resources/views/components/cards/chart-card.blade.php -->
@props([
    'title' => '',
    'chartId' => '',
    'chartType' => 'line',
    'height' => '300px',
    'data' => [],
    'options' => []
])

<div class="netgescon-card">
    <!-- Card Header -->
    <div class="flex items-center justify-between mb-6">
        <h3 class="text-lg font-semibold text-gray-900">{{ $title }}</h3>
        
        <!-- Chart Controls -->
        <div class="flex items-center space-x-2">
            <select class="chart-period text-sm border border-gray-300 rounded px-2 py-1"
                    onchange="updateChart('{{ $chartId }}', this.value)">
                <option value="7d">7 giorni</option>
                <option value="30d" selected>30 giorni</option>
                <option value="3m">3 mesi</option>
                <option value="1y">1 anno</option>
            </select>
            
            <button class="chart-refresh text-gray-500 hover:text-gray-700 p-1"
                    onclick="refreshChart('{{ $chartId }}')">
                <i class="fas fa-sync-alt"></i>
            </button>
        </div>
    </div>
    
    <!-- Chart Container -->
    <div class="chart-container" style="height: {{ $height }}">
        <canvas id="{{ $chartId }}"></canvas>
    </div>
    
    <!-- Chart Script -->
    @push('scripts')
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const ctx = document.getElementById('{{ $chartId }}').getContext('2d');
            
            window.charts = window.charts || {};
            window.charts['{{ $chartId }}'] = new Chart(ctx, {
                type: '{{ $chartType }}',
                data: @json($data),
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    ...@json($options)
                }
            });
        });
    </script>
    @endpush
</div>

📄 CONTENT CARDS

📰 Content Card Template

<!-- resources/views/components/cards/content-card.blade.php -->
@props([
    'title' => '',
    'content' => '',
    'image' => null,
    'footer' => null,
    'tags' => [],
    'url' => null
])

<div class="netgescon-card {{ $url ? 'cursor-pointer hover:shadow-lg' : '' }}"
     @if($url) onclick="window.location='{{ $url }}'" @endif>
    
    <!-- Image -->
    @if($image)
        <div class="card-image mb-4 -mx-6 -mt-6">
            <img src="{{ $image }}" alt="{{ $title }}" 
                 class="w-full h-48 object-cover rounded-t-lg">
        </div>
    @endif
    
    <!-- Content -->
    <div class="card-content">
        <!-- Title -->
        @if($title)
            <h3 class="text-lg font-semibold text-gray-900 mb-3">{{ $title }}</h3>
        @endif
        
        <!-- Content -->
        <div class="text-gray-600 text-sm leading-relaxed">
            {!! $content !!}
        </div>
        
        <!-- Tags -->
        @if(count($tags) > 0)
            <div class="card-tags flex flex-wrap gap-2 mt-4">
                @foreach($tags as $tag)
                    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium 
                                 bg-gray-100 text-gray-800">
                        {{ $tag }}
                    </span>
                @endforeach
            </div>
        @endif
    </div>
    
    <!-- Footer -->
    @if($footer)
        <div class="card-footer border-t border-gray-200 mt-4 pt-4 text-sm text-gray-500">
            {!! $footer !!}
        </div>
    @endif
</div>

⚠️ ALERT CARDS

🚨 Alert Card Template

<!-- resources/views/components/cards/alert-card.blade.php -->
@props([
    'type' => 'info', // info, success, warning, danger
    'title' => '',
    'message' => '',
    'actions' => [],
    'dismissible' => true
])

@php
$alertClasses = [
    'info' => 'border-l-blue-500 bg-blue-50',
    'success' => 'border-l-green-500 bg-green-50',
    'warning' => 'border-l-yellow-500 bg-yellow-50',
    'danger' => 'border-l-red-500 bg-red-50'
];
$iconClasses = [
    'info' => 'fas fa-info-circle text-blue-500',
    'success' => 'fas fa-check-circle text-green-500',
    'warning' => 'fas fa-exclamation-triangle text-yellow-500',
    'danger' => 'fas fa-exclamation-circle text-red-500'
];
@endphp

<div class="netgescon-card {{ $alertClasses[$type] }} border-l-4">
    <div class="flex items-start">
        <!-- Icon -->
        <div class="flex-shrink-0">
            <i class="{{ $iconClasses[$type] }} text-xl"></i>
        </div>
        
        <!-- Content -->
        <div class="ml-3 flex-1">
            @if($title)
                <h4 class="font-medium text-gray-900 mb-1">{{ $title }}</h4>
            @endif
            
            <div class="text-sm text-gray-700">
                {!! $message !!}
            </div>
            
            <!-- Actions -->
            @if(count($actions) > 0)
                <div class="mt-4 flex space-x-3">
                    @foreach($actions as $action)
                        <a href="{{ $action['url'] }}" 
                           class="text-sm font-medium {{ $action['class'] ?? 'text-blue-600 hover:text-blue-500' }}">
                            {{ $action['text'] }}
                        </a>
                    @endforeach
                </div>
            @endif
        </div>
        
        <!-- Dismiss -->
        @if($dismissible)
            <div class="ml-4">
                <button class="text-gray-400 hover:text-gray-600 transition-colors"
                        onclick="this.closest('.netgescon-card').remove()">
                    <i class="fas fa-times"></i>
                </button>
            </div>
        @endif
    </div>
</div>

JAVASCRIPT ENHANCEMENTS

📁 File: /public/js/components/cards.js

/**
 * Cards NetGescon - JavaScript Enhancements
 */

// Initialize cards functionality
function initializeCards() {
    initializeStatsCards();
    initializeChartCards();
    initializeContentCards();
    initializeAnimations();
}

// Stats cards animations
function initializeStatsCards() {
    const statsCards = document.querySelectorAll('.stats-grid .netgescon-card');
    
    // Animate numbers on load
    statsCards.forEach((card, index) => {
        const valueElement = card.querySelector('.text-3xl');
        if (valueElement) {
            const targetValue = valueElement.textContent;
            const numericValue = parseInt(targetValue.replace(/[^\d]/g, ''));
            
            if (!isNaN(numericValue)) {
                animateNumber(valueElement, 0, numericValue, 1000, index * 100);
            }
        }
    });
    
    // Add click analytics
    statsCards.forEach(card => {
        card.addEventListener('click', function() {
            const title = this.querySelector('.text-sm').textContent;
            trackCardClick('stats', title);
        });
    });
}

// Animate number counting
function animateNumber(element, start, end, duration, delay = 0) {
    setTimeout(() => {
        const range = end - start;
        const startTime = performance.now();
        const originalText = element.textContent;
        const prefix = originalText.replace(/[\d,]/g, '');
        
        function updateNumber(currentTime) {
            const elapsed = currentTime - startTime;
            const progress = Math.min(elapsed / duration, 1);
            
            const current = Math.floor(start + (range * easeOutQuart(progress)));
            element.textContent = prefix + current.toLocaleString();
            
            if (progress < 1) {
                requestAnimationFrame(updateNumber);
            }
        }
        
        requestAnimationFrame(updateNumber);
    }, delay);
}

// Easing function
function easeOutQuart(t) {
    return 1 - (--t) * t * t * t;
}

// Chart cards functionality
function initializeChartCards() {
    // Auto-refresh charts
    setInterval(() => {
        const autoRefreshCharts = document.querySelectorAll('[data-auto-refresh="true"]');
        autoRefreshCharts.forEach(chart => {
            const chartId = chart.querySelector('canvas').id;
            refreshChart(chartId);
        });
    }, 300000); // 5 minutes
}

// Refresh chart data
async function refreshChart(chartId) {
    try {
        const response = await fetch(`/api/charts/${chartId}/data`);
        const data = await response.json();
        
        if (window.charts && window.charts[chartId]) {
            window.charts[chartId].data = data;
            window.charts[chartId].update();
        }
    } catch (error) {
        console.warn('Failed to refresh chart:', chartId, error);
    }
}

// Update chart period
async function updateChart(chartId, period) {
    try {
        const response = await fetch(`/api/charts/${chartId}/data?period=${period}`);
        const data = await response.json();
        
        if (window.charts && window.charts[chartId]) {
            window.charts[chartId].data = data;
            window.charts[chartId].update();
        }
    } catch (error) {
        console.warn('Failed to update chart:', chartId, error);
    }
}

// Content cards enhancements
function initializeContentCards() {
    const contentCards = document.querySelectorAll('.netgescon-card[data-url]');
    
    contentCards.forEach(card => {
        card.addEventListener('click', function() {
            const url = this.dataset.url;
            const title = this.querySelector('h3')?.textContent || 'Unknown';
            
            trackCardClick('content', title);
            window.location.href = url;
        });
    });
}

// Animation enhancements
function initializeAnimations() {
    // Intersection Observer for scroll animations
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                entry.target.classList.add('animate-in');
            }
        });
    }, { threshold: 0.1 });
    
    // Observe all cards
    document.querySelectorAll('.netgescon-card').forEach(card => {
        observer.observe(card);
    });
}

// Analytics tracking
function trackCardClick(type, title) {
    // Send analytics data
    if (window.gtag) {
        gtag('event', 'card_click', {
            card_type: type,
            card_title: title,
            page_location: window.location.href
        });
    }
    
    // Send to internal analytics
    fetch('/api/analytics/card-click', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
        },
        body: JSON.stringify({
            type: type,
            title: title,
            url: window.location.href,
            timestamp: new Date().toISOString()
        })
    }).catch(() => {}); // Silent fail
}

// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', initializeCards);

📱 RESPONSIVE GRID SYSTEM

📐 Grid Layouts

/* Stats Cards Grid */
.stats-grid {
    display: grid;
    gap: 1.5rem;
    grid-template-columns: 1fr;
}

@media (min-width: 640px) {
    .stats-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (min-width: 1024px) {
    .stats-grid {
        grid-template-columns: repeat(4, 1fr);
    }
}

/* Content Cards Grid */
.content-grid {
    display: grid;
    gap: 2rem;
    grid-template-columns: 1fr;
}

@media (min-width: 768px) {
    .content-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (min-width: 1024px) {
    .content-grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

/* Mixed Layout */
.cards-mixed {
    display: grid;
    gap: 1.5rem;
    grid-template-columns: 1fr;
}

@media (min-width: 768px) {
    .cards-mixed {
        grid-template-columns: 2fr 1fr;
    }
}

🧪 TESTING CHECKLIST

Visual Tests

  • Cards consistent spacing
  • Hover effects smooth
  • Icons properly aligned
  • Colors match brand palette
  • Responsive grid working

Functional Tests

  • Click handlers working
  • Chart updates functional
  • Animations performant
  • Dismiss buttons working
  • Analytics tracking

Accessibility Tests

  • Keyboard navigation
  • Screen reader compatibility
  • Color contrast sufficient
  • Focus indicators visible
  • Alternative text present

🚀 PERFORMANCE OPTIMIZATION

Best Practices

/* CSS Optimizations */
.netgescon-card {
    /* Use transform instead of changing box-shadow directly */
    will-change: transform, box-shadow;
}

.netgescon-card:hover {
    /* Hardware acceleration */
    transform: translateY(-2px) translateZ(0);
}

/* Reduce repaints */
.card-animation {
    backface-visibility: hidden;
    perspective: 1000px;
}
// JavaScript Optimizations
function throttle(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

// Throttled scroll handler
window.addEventListener('scroll', throttle(handleScroll, 16));

📚 FILES CORRELATI

🧩 Components

  • /resources/views/components/cards/ (directory)
  • /resources/views/components/cards/stats-card.blade.php
  • /resources/views/components/cards/info-card.blade.php
  • /resources/views/components/cards/action-card.blade.php
  • /resources/views/components/cards/chart-card.blade.php
  • /resources/views/components/cards/content-card.blade.php
  • /resources/views/components/cards/alert-card.blade.php

🎨 Styles

  • /public/css/components/cards.css
  • /public/css/netgescon-admin.css

JavaScript

  • /public/js/components/cards.js
  • /public/js/charts/chart-manager.js

🎯 NEXT STEPS

🔮 Planned Improvements

  • Advanced Charts - More chart types and interactions
  • Card Templates - Pre-built card collections
  • Drag & Drop - Rearrangeable dashboard cards
  • Real-time Data - WebSocket live updates
  • Custom Themes - User-customizable card appearances

📝 Documento mantenuto da: GitHub Copilot AI Assistant
🔗 Componente: Cards System NetGescon Interface v2.1.0