22 KiB
Executable File
22 KiB
Executable File
🃏 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