📋 Commit iniziale con: - ✅ Documentazione unificata in docs/ - ✅ Codice Laravel in netgescon-laravel/ - ✅ Script automazione in scripts/ - ✅ Configurazione sync rsync - ✅ Struttura organizzata e pulita 🔄 Versione: 2025.07.19-1644 🎯 Sistema pronto per Git distribuito
1134 lines
38 KiB
Markdown
1134 lines
38 KiB
Markdown
# 5. INTERFACCIA UNIVERSALE - GUIDA COMPLETA
|
|
|
|
## 📋 **INDICE CAPITOLO**
|
|
- [5.1 Architettura Layout](#51-architettura-layout)
|
|
- [5.2 Sistema Navigazione AJAX](#52-sistema-navigazione-ajax)
|
|
- [5.3 Helper Menu Personalizzati](#53-helper-menu-personalizzati)
|
|
- [5.4 Gestione Sidebar Dinamica](#54-gestione-sidebar-dinamica)
|
|
- [5.5 Dashboard Cards Interattive](#55-dashboard-cards-interattive)
|
|
- [5.6 Breadcrumb e Messaggi](#56-breadcrumb-e-messaggi)
|
|
- [5.7 Esempi Pratici](#57-esempi-pratici)
|
|
- [5.8 Best Practices](#58-best-practices)
|
|
|
|
---
|
|
|
|
## 5.1 Architettura Layout
|
|
|
|
### Layout Base Universale
|
|
**File**: `resources/views/layouts/universal.blade.php`
|
|
|
|
```blade
|
|
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{{ $pageTitle ?? 'NetGescon' }}</title>
|
|
|
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|
@stack('styles')
|
|
|
|
<!-- CSS per interfaccia universale -->
|
|
<style>
|
|
.sidebar {
|
|
background: #343a40;
|
|
min-height: 100vh;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 100;
|
|
padding-top: 70px;
|
|
}
|
|
|
|
.sidebar .nav-link {
|
|
color: #adb5bd;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 0.375rem;
|
|
margin: 0.125rem 0.5rem;
|
|
}
|
|
|
|
.sidebar .nav-link:hover {
|
|
background: #495057;
|
|
color: #fff;
|
|
}
|
|
|
|
.sidebar .nav-link.active {
|
|
background: #007bff;
|
|
color: #fff;
|
|
}
|
|
|
|
.main-content {
|
|
margin-left: 250px;
|
|
padding-top: 70px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.sidebar {
|
|
transform: translateX(-100%);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.sidebar.show {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.main-content {
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- Header Universale -->
|
|
@include('layouts.partials.header')
|
|
|
|
<!-- Sidebar Dinamica per Ruolo -->
|
|
@if($showSidebar ?? true)
|
|
<nav class="sidebar">
|
|
@include('layouts.partials.sidebar')
|
|
</nav>
|
|
@endif
|
|
|
|
<!-- Contenuto Principale -->
|
|
<main class="main-content">
|
|
<div class="container-fluid px-4">
|
|
<!-- Breadcrumb -->
|
|
@if($showBreadcrumb ?? true)
|
|
@include('layouts.partials.breadcrumb')
|
|
@endif
|
|
|
|
<!-- Alert Messaggi -->
|
|
@include('layouts.partials.alerts')
|
|
|
|
<!-- Contenuto Pagina -->
|
|
{{ $slot }}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
@stack('scripts')
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Header Universale
|
|
**File**: `resources/views/layouts/partials/header.blade.php`
|
|
|
|
```blade
|
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
|
<div class="container-fluid">
|
|
<!-- Logo e Nome -->
|
|
<a class="navbar-brand" href="{{ route('dashboard') }}">
|
|
<i class="fas fa-building me-2"></i>
|
|
NetGescon
|
|
</a>
|
|
|
|
<!-- Bottone Mobile Menu -->
|
|
<button class="navbar-toggler d-lg-none" type="button" id="sidebarToggle">
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
|
|
<!-- Menu Utente -->
|
|
<div class="navbar-nav ms-auto">
|
|
<div class="nav-item dropdown">
|
|
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-user me-1"></i>
|
|
{{ Auth::user()->name }}
|
|
<span class="badge bg-primary ms-1">
|
|
{{ Auth::user()->getRoleNames()->first() }}
|
|
</span>
|
|
</a>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><a class="dropdown-item" href="{{ route('profile.edit') }}">
|
|
<i class="fas fa-user-edit me-2"></i>Profilo
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li>
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<button type="submit" class="dropdown-item">
|
|
<i class="fas fa-sign-out-alt me-2"></i>Logout
|
|
</button>
|
|
</form>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
```
|
|
|
|
---
|
|
|
|
## 5.2 Sistema Navigazione AJAX
|
|
|
|
### JavaScript Navigazione Dashboard
|
|
**File**: `resources/js/dashboard-navigation.js`
|
|
|
|
```javascript
|
|
class DashboardNavigation {
|
|
constructor() {
|
|
this.currentSection = null;
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Gestione click cards dashboard
|
|
$(document).on('click', '.clickable-card', (e) => {
|
|
const section = $(e.currentTarget).data('section');
|
|
this.loadSection(section);
|
|
});
|
|
|
|
// Gestione navigazione sidebar
|
|
$(document).on('click', '.dashboard-nav-link', (e) => {
|
|
e.preventDefault();
|
|
const section = $(e.currentTarget).data('section');
|
|
const action = $(e.currentTarget).data('action');
|
|
this.loadSection(section, action);
|
|
});
|
|
|
|
// Gestione breadcrumb
|
|
$(document).on('click', '.breadcrumb-back', (e) => {
|
|
e.preventDefault();
|
|
this.showDashboard();
|
|
});
|
|
|
|
// Gestione pulsanti azione
|
|
$(document).on('click', '.section-action-btn', (e) => {
|
|
e.preventDefault();
|
|
const action = $(e.currentTarget).data('action');
|
|
this.handleSectionAction(action);
|
|
});
|
|
|
|
// Mobile sidebar toggle
|
|
$('#sidebarToggle').on('click', () => {
|
|
$('.sidebar').toggleClass('show');
|
|
});
|
|
}
|
|
|
|
loadSection(section, action = null) {
|
|
// Mostra loader
|
|
this.showLoader();
|
|
|
|
// Nascondi dashboard principale
|
|
$('#main-dashboard').addClass('d-none');
|
|
$('#section-content').removeClass('d-none');
|
|
|
|
// Aggiorna sidebar attiva
|
|
this.updateSidebarActive(section);
|
|
|
|
// Aggiorna breadcrumb
|
|
this.updateBreadcrumb(section);
|
|
|
|
// Carica contenuto specifico
|
|
this.loadSectionContent(section, action);
|
|
}
|
|
|
|
loadSectionContent(section, action) {
|
|
const sectionHandlers = {
|
|
'stabili': () => this.loadStabiliContent(action),
|
|
'condomini': () => this.loadCondominiContent(action),
|
|
'unita': () => this.loadUnitaContent(action),
|
|
'soggetti': () => this.loadSoggettiContent(action),
|
|
'contabilita': () => this.loadContabilitaContent(action),
|
|
'documenti': () => this.loadDocumentiContent(action),
|
|
'tickets': () => this.loadTicketsContent(action)
|
|
};
|
|
|
|
if (sectionHandlers[section]) {
|
|
sectionHandlers[section]();
|
|
} else {
|
|
this.showError(`Sezione '${section}' non trovata`);
|
|
}
|
|
}
|
|
|
|
showDashboard() {
|
|
$('#section-content').addClass('d-none');
|
|
$('#main-dashboard').removeClass('d-none');
|
|
this.updateSidebarActive('dashboard');
|
|
this.updateBreadcrumb('dashboard');
|
|
}
|
|
|
|
updateSidebarActive(section) {
|
|
$('.sidebar .nav-link').removeClass('active');
|
|
$(`.sidebar .nav-link[data-section="${section}"]`).addClass('active');
|
|
}
|
|
|
|
updateBreadcrumb(section) {
|
|
const breadcrumbMap = {
|
|
'dashboard': 'Dashboard',
|
|
'stabili': 'Gestione Stabili',
|
|
'condomini': 'Anagrafica Condomini',
|
|
'unita': 'Unità Immobiliari',
|
|
'soggetti': 'Anagrafica Soggetti',
|
|
'contabilita': 'Contabilità',
|
|
'documenti': 'Gestione Documenti',
|
|
'tickets': 'Comunicazioni'
|
|
};
|
|
|
|
const breadcrumbHtml = `
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item">
|
|
<a href="#" class="breadcrumb-back">Dashboard</a>
|
|
</li>
|
|
<li class="breadcrumb-item active">${breadcrumbMap[section] || section}</li>
|
|
</ol>
|
|
</nav>
|
|
`;
|
|
|
|
$('#breadcrumb-container').html(breadcrumbHtml);
|
|
}
|
|
|
|
showLoader() {
|
|
const loader = `
|
|
<div class="d-flex justify-content-center py-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Caricamento...</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
$('#section-content').html(loader);
|
|
}
|
|
|
|
showError(message) {
|
|
const error = `
|
|
<div class="alert alert-danger" role="alert">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
${message}
|
|
</div>
|
|
`;
|
|
$('#section-content').html(error);
|
|
}
|
|
}
|
|
|
|
// Inizializza quando DOM è pronto
|
|
$(document).ready(() => {
|
|
new DashboardNavigation();
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 5.3 Helper Menu Personalizzati
|
|
|
|
### MenuHelper Class Completa
|
|
**File**: `app/Helpers/MenuHelper.php`
|
|
|
|
```php
|
|
<?php
|
|
|
|
namespace App\Helpers;
|
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Route;
|
|
|
|
class MenuHelper
|
|
{
|
|
/**
|
|
* Mapping completo permessi per menu
|
|
*/
|
|
private static $menuPermissions = [
|
|
'stabili' => ['admin', 'amministratore'],
|
|
'condomini' => ['admin', 'amministratore'],
|
|
'unita' => ['admin', 'amministratore'],
|
|
'soggetti' => ['admin', 'amministratore'],
|
|
'contabilita' => ['admin', 'amministratore'],
|
|
'documenti' => ['admin', 'amministratore', 'condomino'],
|
|
'tickets' => ['admin', 'amministratore', 'condomino'],
|
|
'comunicazioni' => ['admin', 'amministratore'],
|
|
'backup' => ['admin', 'amministratore'],
|
|
'utenti' => ['super-admin'],
|
|
'comuni' => ['super-admin'],
|
|
'configurazione' => ['super-admin'],
|
|
'log' => ['super-admin', 'admin']
|
|
];
|
|
|
|
/**
|
|
* Verifica se l'utente può accedere a un menu specifico
|
|
*/
|
|
public static function canUserAccessMenu(string $menuKey): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if (!$user) {
|
|
return false;
|
|
}
|
|
|
|
// SuperAdmin può accedere a tutto
|
|
if ($user->hasRole('super-admin')) {
|
|
return true;
|
|
}
|
|
|
|
if (!isset(self::$menuPermissions[$menuKey])) {
|
|
return false;
|
|
}
|
|
|
|
return $user->hasAnyRole(self::$menuPermissions[$menuKey]);
|
|
}
|
|
|
|
/**
|
|
* Costruisce menu sidebar completo per ruolo utente
|
|
*/
|
|
public static function buildSidebarMenu(): array
|
|
{
|
|
$user = Auth::user();
|
|
$userRoles = $user->getRoleNames()->toArray();
|
|
$menu = [];
|
|
|
|
// Menu base sempre presente
|
|
$menu[] = [
|
|
'label' => 'Dashboard',
|
|
'icon' => 'fas fa-tachometer-alt',
|
|
'route' => 'dashboard',
|
|
'active' => request()->routeIs('dashboard'),
|
|
'type' => 'route'
|
|
];
|
|
|
|
// Menu per SuperAdmin
|
|
if ($user->hasRole('super-admin')) {
|
|
$menu[] = [
|
|
'label' => 'Gestione Utenti',
|
|
'icon' => 'fas fa-users',
|
|
'route' => 'superadmin.users.index',
|
|
'active' => request()->routeIs('superadmin.users.*'),
|
|
'type' => 'route'
|
|
];
|
|
|
|
$menu[] = [
|
|
'label' => 'Comuni Italiani',
|
|
'icon' => 'fas fa-map',
|
|
'route' => 'superadmin.comuni.index',
|
|
'active' => request()->routeIs('superadmin.comuni.*'),
|
|
'type' => 'route'
|
|
];
|
|
|
|
$menu[] = [
|
|
'label' => 'Configurazione',
|
|
'icon' => 'fas fa-cog',
|
|
'route' => 'superadmin.config.index',
|
|
'active' => request()->routeIs('superadmin.config.*'),
|
|
'type' => 'route'
|
|
];
|
|
|
|
$menu[] = [
|
|
'label' => 'Log Sistema',
|
|
'icon' => 'fas fa-file-alt',
|
|
'route' => 'superadmin.logs.index',
|
|
'active' => request()->routeIs('superadmin.logs.*'),
|
|
'type' => 'route'
|
|
];
|
|
}
|
|
|
|
// Menu per Admin/Amministratore
|
|
if ($user->hasAnyRole(['admin', 'amministratore'])) {
|
|
// Sezione Anagrafica
|
|
$menu[] = [
|
|
'label' => 'ANAGRAFICA',
|
|
'type' => 'header'
|
|
];
|
|
|
|
if (self::canUserAccessMenu('stabili')) {
|
|
$menu[] = [
|
|
'label' => 'Gestione Stabili',
|
|
'icon' => 'fas fa-building',
|
|
'section' => 'stabili',
|
|
'type' => 'ajax',
|
|
'badge' => $user->stabiliAmministrati()->count()
|
|
];
|
|
}
|
|
|
|
if (self::canUserAccessMenu('condomini')) {
|
|
$menu[] = [
|
|
'label' => 'Anagrafica Condomini',
|
|
'icon' => 'fas fa-home',
|
|
'section' => 'condomini',
|
|
'type' => 'ajax'
|
|
];
|
|
}
|
|
|
|
if (self::canUserAccessMenu('soggetti')) {
|
|
$menu[] = [
|
|
'label' => 'Anagrafica Soggetti',
|
|
'icon' => 'fas fa-users',
|
|
'section' => 'soggetti',
|
|
'type' => 'ajax'
|
|
];
|
|
}
|
|
|
|
if (self::canUserAccessMenu('unita')) {
|
|
$menu[] = [
|
|
'label' => 'Unità Immobiliari',
|
|
'icon' => 'fas fa-door-open',
|
|
'section' => 'unita',
|
|
'type' => 'ajax'
|
|
];
|
|
}
|
|
|
|
// Sezione Gestionale
|
|
$menu[] = [
|
|
'label' => 'GESTIONALE',
|
|
'type' => 'header'
|
|
];
|
|
|
|
if (self::canUserAccessMenu('contabilita')) {
|
|
$menu[] = [
|
|
'label' => 'Contabilità',
|
|
'icon' => 'fas fa-calculator',
|
|
'section' => 'contabilita',
|
|
'type' => 'ajax'
|
|
];
|
|
}
|
|
|
|
if (self::canUserAccessMenu('documenti')) {
|
|
$menu[] = [
|
|
'label' => 'Gestione Documenti',
|
|
'icon' => 'fas fa-file-alt',
|
|
'section' => 'documenti',
|
|
'type' => 'ajax'
|
|
];
|
|
}
|
|
|
|
if (self::canUserAccessMenu('tickets')) {
|
|
$menu[] = [
|
|
'label' => 'Comunicazioni',
|
|
'icon' => 'fas fa-envelope',
|
|
'section' => 'tickets',
|
|
'type' => 'ajax',
|
|
'badge' => self::getTicketsApertiCount()
|
|
];
|
|
}
|
|
}
|
|
|
|
// Menu per Condomino
|
|
if ($user->hasRole('condomino')) {
|
|
$menu[] = [
|
|
'label' => 'Le Mie Proprietà',
|
|
'icon' => 'fas fa-home',
|
|
'route' => 'condomino.unita.index',
|
|
'active' => request()->routeIs('condomino.unita.*'),
|
|
'type' => 'route'
|
|
];
|
|
|
|
$menu[] = [
|
|
'label' => 'I Miei Documenti',
|
|
'icon' => 'fas fa-file',
|
|
'route' => 'condomino.documenti.index',
|
|
'active' => request()->routeIs('condomino.documenti.*'),
|
|
'type' => 'route'
|
|
];
|
|
|
|
$menu[] = [
|
|
'label' => 'Comunicazioni',
|
|
'icon' => 'fas fa-envelope',
|
|
'route' => 'condomino.tickets.index',
|
|
'active' => request()->routeIs('condomino.tickets.*'),
|
|
'type' => 'route'
|
|
];
|
|
}
|
|
|
|
return $menu;
|
|
}
|
|
|
|
/**
|
|
* Genera HTML per menu sidebar
|
|
*/
|
|
public static function renderSidebarMenu(): string
|
|
{
|
|
$menu = self::buildSidebarMenu();
|
|
$html = '<ul class="nav flex-column">';
|
|
|
|
foreach ($menu as $item) {
|
|
$html .= self::renderMenuItem($item);
|
|
}
|
|
|
|
$html .= '</ul>';
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Renderizza singolo elemento menu
|
|
*/
|
|
private static function renderMenuItem(array $item): string
|
|
{
|
|
if ($item['type'] === 'header') {
|
|
return '<li class="nav-item mt-3 mb-2">
|
|
<h6 class="sidebar-heading text-muted">' . $item['label'] . '</h6>
|
|
</li>';
|
|
}
|
|
|
|
$activeClass = ($item['active'] ?? false) ? 'active' : '';
|
|
$badge = isset($item['badge']) ? '<span class="badge bg-primary ms-auto">' . $item['badge'] . '</span>' : '';
|
|
|
|
if ($item['type'] === 'route') {
|
|
$href = route($item['route']);
|
|
$attributes = '';
|
|
} else {
|
|
$href = '#' . $item['section'];
|
|
$attributes = 'data-section="' . $item['section'] . '"';
|
|
}
|
|
|
|
return '<li class="nav-item">
|
|
<a class="nav-link ' . $activeClass . ' ' . ($item['type'] === 'ajax' ? 'dashboard-nav-link' : '') . '"
|
|
href="' . $href . '" ' . $attributes . '>
|
|
<i class="' . $item['icon'] . ' me-2"></i>
|
|
' . $item['label'] . '
|
|
' . $badge . '
|
|
</a>
|
|
</li>';
|
|
}
|
|
|
|
/**
|
|
* Conta tickets aperti per badge
|
|
*/
|
|
private static function getTicketsApertiCount(): int
|
|
{
|
|
$user = Auth::user();
|
|
|
|
if ($user->hasRole('condomino')) {
|
|
// Conta tickets del condomino
|
|
return \App\Models\Ticket::where('user_id', $user->id)
|
|
->where('stato', 'aperto')
|
|
->count();
|
|
}
|
|
|
|
if ($user->hasAnyRole(['admin', 'amministratore'])) {
|
|
// Conta tickets degli stabili amministrati
|
|
$stabiliIds = $user->stabiliAmministrati()->pluck('id');
|
|
return \App\Models\Ticket::whereIn('stabile_id', $stabiliIds)
|
|
->where('stato', 'aperto')
|
|
->count();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5.4 Gestione Sidebar Dinamica
|
|
|
|
### Sidebar Blade Template
|
|
**File**: `resources/views/layouts/partials/sidebar.blade.php`
|
|
|
|
```blade
|
|
<div class="position-sticky pt-3">
|
|
{!! App\Helpers\MenuHelper::renderSidebarMenu() !!}
|
|
|
|
<!-- Informazioni Utente in fondo -->
|
|
<div class="mt-auto pt-4 border-top">
|
|
<div class="small text-muted px-3">
|
|
<div class="d-flex align-items-center mb-2">
|
|
<i class="fas fa-user me-2"></i>
|
|
<span>{{ Auth::user()->name }}</span>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<i class="fas fa-shield-alt me-2"></i>
|
|
<span class="badge bg-secondary">{{ Auth::user()->getRoleNames()->first() }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CSS aggiuntivo per sidebar -->
|
|
<style>
|
|
.sidebar-heading {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
padding: 0 1rem;
|
|
}
|
|
|
|
.nav-link {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.nav-link:hover {
|
|
transform: translateX(5px);
|
|
}
|
|
|
|
.nav-link.active::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 3px;
|
|
background: #007bff;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.65rem;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
---
|
|
|
|
## 5.5 Dashboard Cards Interattive
|
|
|
|
### Dashboard Cards Template
|
|
**File**: `resources/views/admin/dashboard-cards.blade.php`
|
|
|
|
```blade
|
|
<div class="row g-4 mb-4">
|
|
<!-- Card Stabili -->
|
|
@if(App\Helpers\MenuHelper::canUserAccessMenu('stabili'))
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="card clickable-card h-100" data-section="stabili">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h5 class="card-title">
|
|
<i class="fas fa-building text-primary me-2"></i>
|
|
Gestione Stabili
|
|
</h5>
|
|
<p class="card-text text-muted">
|
|
Amministra anagrafica e dati degli stabili
|
|
</p>
|
|
</div>
|
|
<div class="text-end">
|
|
<h3 class="text-primary mb-0">{{ $stats['stabili_count'] ?? 0 }}</h3>
|
|
<small class="text-muted">Stabili</small>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<div class="d-flex justify-content-between">
|
|
<small class="text-muted">Attivi</small>
|
|
<small class="text-success">{{ $stats['stabili_attivi'] ?? 0 }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer bg-transparent">
|
|
<small class="text-muted">
|
|
<i class="fas fa-mouse-pointer me-1"></i>
|
|
Clicca per gestire
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Card Condomini -->
|
|
@if(App\Helpers\MenuHelper::canUserAccessMenu('condomini'))
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="card clickable-card h-100" data-section="condomini">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h5 class="card-title">
|
|
<i class="fas fa-home text-success me-2"></i>
|
|
Anagrafica Condomini
|
|
</h5>
|
|
<p class="card-text text-muted">
|
|
Gestisce proprietari e inquilini
|
|
</p>
|
|
</div>
|
|
<div class="text-end">
|
|
<h3 class="text-success mb-0">{{ $stats['condomini_count'] ?? 0 }}</h3>
|
|
<small class="text-muted">Condomini</small>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<div class="d-flex justify-content-between">
|
|
<small class="text-muted">Proprietari</small>
|
|
<small class="text-info">{{ $stats['proprietari_count'] ?? 0 }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer bg-transparent">
|
|
<small class="text-muted">
|
|
<i class="fas fa-mouse-pointer me-1"></i>
|
|
Clicca per gestire
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Card Contabilità -->
|
|
@if(App\Helpers\MenuHelper::canUserAccessMenu('contabilita'))
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="card clickable-card h-100" data-section="contabilita">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h5 class="card-title">
|
|
<i class="fas fa-calculator text-warning me-2"></i>
|
|
Contabilità
|
|
</h5>
|
|
<p class="card-text text-muted">
|
|
Movimenti e bilanci condominiali
|
|
</p>
|
|
</div>
|
|
<div class="text-end">
|
|
<h3 class="text-warning mb-0">{{ $stats['movimenti_count'] ?? 0 }}</h3>
|
|
<small class="text-muted">Movimenti</small>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<div class="d-flex justify-content-between">
|
|
<small class="text-muted">Saldo</small>
|
|
<small class="text-{{ ($stats['saldo_totale'] ?? 0) >= 0 ? 'success' : 'danger' }}">
|
|
€{{ number_format($stats['saldo_totale'] ?? 0, 2) }}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer bg-transparent">
|
|
<small class="text-muted">
|
|
<i class="fas fa-mouse-pointer me-1"></i>
|
|
Clicca per gestire
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Card Documenti -->
|
|
@if(App\Helpers\MenuHelper::canUserAccessMenu('documenti'))
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="card clickable-card h-100" data-section="documenti">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h5 class="card-title">
|
|
<i class="fas fa-file-alt text-info me-2"></i>
|
|
Gestione Documenti
|
|
</h5>
|
|
<p class="card-text text-muted">
|
|
Archivia e organizza documenti
|
|
</p>
|
|
</div>
|
|
<div class="text-end">
|
|
<h3 class="text-info mb-0">{{ $stats['documenti_count'] ?? 0 }}</h3>
|
|
<small class="text-muted">Documenti</small>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<div class="d-flex justify-content-between">
|
|
<small class="text-muted">Questo mese</small>
|
|
<small class="text-primary">{{ $stats['documenti_mese'] ?? 0 }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer bg-transparent">
|
|
<small class="text-muted">
|
|
<i class="fas fa-mouse-pointer me-1"></i>
|
|
Clicca per gestire
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Card Comunicazioni -->
|
|
@if(App\Helpers\MenuHelper::canUserAccessMenu('tickets'))
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="card clickable-card h-100" data-section="tickets">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h5 class="card-title">
|
|
<i class="fas fa-envelope text-danger me-2"></i>
|
|
Comunicazioni
|
|
</h5>
|
|
<p class="card-text text-muted">
|
|
Ticket e richieste condomini
|
|
</p>
|
|
</div>
|
|
<div class="text-end">
|
|
<h3 class="text-danger mb-0">{{ $stats['tickets_aperti'] ?? 0 }}</h3>
|
|
<small class="text-muted">Aperti</small>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<div class="d-flex justify-content-between">
|
|
<small class="text-muted">Totali</small>
|
|
<small class="text-secondary">{{ $stats['tickets_totali'] ?? 0 }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer bg-transparent">
|
|
<small class="text-muted">
|
|
<i class="fas fa-mouse-pointer me-1"></i>
|
|
Clicca per gestire
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- CSS per cards interattive -->
|
|
<style>
|
|
.clickable-card {
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
.clickable-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.clickable-card:active {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.card-text {
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.card-footer {
|
|
border-top: 1px solid #f8f9fa;
|
|
padding: 0.5rem 1rem;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
---
|
|
|
|
## 5.6 Breadcrumb e Messaggi
|
|
|
|
### Breadcrumb Template
|
|
**File**: `resources/views/layouts/partials/breadcrumb.blade.php`
|
|
|
|
```blade
|
|
<div id="breadcrumb-container" class="mb-3">
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item">
|
|
<a href="{{ route('dashboard') }}">Dashboard</a>
|
|
</li>
|
|
@if(isset($breadcrumbs) && is_array($breadcrumbs))
|
|
@foreach($breadcrumbs as $breadcrumb)
|
|
@if($loop->last)
|
|
<li class="breadcrumb-item active" aria-current="page">
|
|
{{ $breadcrumb['label'] }}
|
|
</li>
|
|
@else
|
|
<li class="breadcrumb-item">
|
|
<a href="{{ $breadcrumb['url'] }}">{{ $breadcrumb['label'] }}</a>
|
|
</li>
|
|
@endif
|
|
@endforeach
|
|
@endif
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
```
|
|
|
|
### Alert Messaggi Template
|
|
**File**: `resources/views/layouts/partials/alerts.blade.php`
|
|
|
|
```blade
|
|
<!-- Messaggi di successo -->
|
|
@if(session('success'))
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
<i class="fas fa-check-circle me-2"></i>
|
|
{{ session('success') }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Messaggi di errore -->
|
|
@if(session('error'))
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
<i class="fas fa-exclamation-circle me-2"></i>
|
|
{{ session('error') }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Messaggi di warning -->
|
|
@if(session('warning'))
|
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
{{ session('warning') }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Messaggi di info -->
|
|
@if(session('info'))
|
|
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
{{ session('info') }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Errori di validazione -->
|
|
@if($errors->any())
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
<i class="fas fa-exclamation-circle me-2"></i>
|
|
<strong>Errori di validazione:</strong>
|
|
<ul class="mb-0 mt-2">
|
|
@foreach($errors->all() as $error)
|
|
<li>{{ $error }}</li>
|
|
@endforeach
|
|
</ul>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
@endif
|
|
```
|
|
|
|
---
|
|
|
|
## 5.7 Esempi Pratici
|
|
|
|
### Come Aggiungere Nuova Sezione Menu
|
|
|
|
**Passo 1**: Aggiungere permesso in `MenuHelper.php`
|
|
```php
|
|
private static $menuPermissions = [
|
|
// ...existing permissions...
|
|
'nuova_sezione' => ['admin', 'amministratore'],
|
|
];
|
|
```
|
|
|
|
**Passo 2**: Aggiungere voce menu in `buildSidebarMenu()`
|
|
```php
|
|
if (self::canUserAccessMenu('nuova_sezione')) {
|
|
$menu[] = [
|
|
'label' => 'Nuova Sezione',
|
|
'icon' => 'fas fa-star',
|
|
'section' => 'nuova_sezione',
|
|
'type' => 'ajax'
|
|
];
|
|
}
|
|
```
|
|
|
|
**Passo 3**: Aggiungere handler JavaScript in `dashboard-navigation.js`
|
|
```javascript
|
|
const sectionHandlers = {
|
|
// ...existing handlers...
|
|
'nuova_sezione': () => this.loadNuovaSezionePage(action),
|
|
};
|
|
|
|
loadNuovaSezionePage(action) {
|
|
let content = `
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2>Nuova Sezione</h2>
|
|
<button class="btn btn-primary section-action-btn" data-action="nuovo">
|
|
<i class="fas fa-plus me-2"></i>Nuovo
|
|
</button>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<p>Contenuto della nuova sezione</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
$('#section-content').html(content);
|
|
}
|
|
```
|
|
|
|
### Come Aggiungere Nuova Card Dashboard
|
|
|
|
**Passo 1**: Aggiungere in `dashboard-cards.blade.php`
|
|
```blade
|
|
@if(App\Helpers\MenuHelper::canUserAccessMenu('nuova_sezione'))
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="card clickable-card h-100" data-section="nuova_sezione">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h5 class="card-title">
|
|
<i class="fas fa-star text-purple me-2"></i>
|
|
Nuova Sezione
|
|
</h5>
|
|
<p class="card-text text-muted">
|
|
Descrizione della nuova sezione
|
|
</p>
|
|
</div>
|
|
<div class="text-end">
|
|
<h3 class="text-purple mb-0">{{ $stats['nuova_count'] ?? 0 }}</h3>
|
|
<small class="text-muted">Elementi</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer bg-transparent">
|
|
<small class="text-muted">
|
|
<i class="fas fa-mouse-pointer me-1"></i>
|
|
Clicca per gestire
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
```
|
|
|
|
---
|
|
|
|
## 5.8 Best Practices
|
|
|
|
### Convenzioni Naming
|
|
- **Sezioni**: snake_case (es: `gestione_stabili`)
|
|
- **Icone**: Font Awesome 6 (es: `fas fa-building`)
|
|
- **Colori**: Bootstrap classes (es: `text-primary`)
|
|
- **Route**: kebab-case (es: `admin.stabili.index`)
|
|
|
|
### Performance
|
|
- **Caricamento lazy**: Solo il contenuto necessario
|
|
- **Cache menu**: MenuHelper usa cache per permessi
|
|
- **Minify assets**: Vite ottimizza JS/CSS
|
|
- **AJAX progressive**: Carica solo sezioni attive
|
|
|
|
### Responsive Design
|
|
- **Mobile-first**: Bootstrap 5 grid system
|
|
- **Sidebar collapsible**: Hidden su mobile
|
|
- **Touch-friendly**: Buttons e cards ottimizzati
|
|
- **Breakpoints**: sm, md, lg, xl, xxl
|
|
|
|
### Sicurezza
|
|
- **CSRF protection**: Token in tutti i form
|
|
- **Route protection**: Middleware per ogni sezione
|
|
- **Permission checking**: Helper per ogni menu
|
|
- **Input validation**: Laravel Form Requests
|
|
|
|
### Debugging
|
|
- **Console logs**: JavaScript errors
|
|
- **Laravel debugbar**: Query monitoring
|
|
- **Error pages**: Custom 403/404/500
|
|
- **Log channels**: Separate logs per sezione
|
|
|
|
---
|
|
|
|
**📝 COMPLETATO: Capitolo 5 - Interfaccia Universale**
|
|
|
|
Questo capitolo fornisce una guida completa per:
|
|
- ✅ Implementare layout universale
|
|
- ✅ Gestire navigazione AJAX
|
|
- ✅ Creare helper menu personalizzati
|
|
- ✅ Configurare sidebar dinamica
|
|
- ✅ Implementare dashboard interattive
|
|
- ✅ Aggiungere nuove sezioni/menu
|
|
- ✅ Seguire best practices
|
|
|
|
**🔄 Prossimi capitoli da completare:**
|
|
- 6. Sistema Multi-Ruolo
|
|
- 7. API e Integrazioni
|
|
- 8. Frontend e UX
|
|
- 9. Gestione Stabili e Condomini
|
|
- 10. Sistema Contabile
|
|
- ... (tutti gli altri capitoli)
|