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

857 lines
22 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🃏 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**
```css
/* 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**
```css
/* 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**
```blade
<!-- 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**
```blade
<!-- 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**
```blade
<!-- 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**
```blade
<!-- 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**
```blade
<!-- 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**
```blade
<!-- 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**
```blade
<!-- 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`
```javascript
/**
* 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**
```css
/* 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
/* 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
// 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*