netgescon-day0/docs/06-sviluppo-moduli/GUIDA-COMPLETA-MODULI-NETGESCON.md

26 KiB
Executable File
Raw Blame History

📋 GUIDA COMPLETA SVILUPPO MODULI NETGESCON

Versione: 2.0 - Data: 03 Agosto 2025 Aggiornata con implementazione modulo Contabilità


🎯 INTRODUZIONE

NetGescon utilizza un sistema modulare avanzato basato su Laravel 10+ che permette di sviluppare moduli completamente integrati con il design system NetGescon e le credenziali database unificate.

🏗️ ARCHITETTURA MODULARE AGGIORNATA

NetGescon/
├── app/
│   └── Modules/
│       └── [NomeModulo]/
│           ├── Controllers/
│           │   └── NomeModuloController.php
│           ├── Routes/
│           │   └── web.php
│           └── Models/
│               └── [ModelName].php
├── resources/
│   └── views/
│       └── modules/
│           └── [nome-modulo]/
│               ├── index.blade.php
│               ├── create.blade.php
│               ├── edit.blade.php
│               └── show.blade.php
└── database/
    └── migrations/
        └── [timestamp]_create_[nome_modulo]_tables.php

🔧 IMPLEMENTAZIONE PROVEN: MODULO CONTABILITÀ

1. Struttura Directory Corretta

app/Modules/Contabilita/
├── Controllers/
│   └── ContabilitaController.php
└── Routes/
    └── web.php

resources/views/modules/contabilita/
├── index.blade.php
├── create.blade.php
├── edit.blade.php
└── show.blade.php

2. Controller Base Implementato

<?php

namespace App\Modules\Contabilita\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;

class ContabilitaController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('can:admin-access');
    }
    
    public function index(Request $request)
    {
        try {
            // Query su tabelle esistenti con naming italiano
            $registrazioni = DB::connection('mysql')
                ->table('registrazioni_contabili')
                ->select('*')
                ->orderBy('data_registrazione', 'desc')
                ->paginate(20);
            
            return view('modules.contabilita.index', compact('registrazioni'));
            
        } catch (\Exception $e) {
            \Log::error("Errore Contabilità index: " . $e->getMessage());
            return back()->with('error', 'Errore nel caricamento dei dati: ' . $e->getMessage());
        }
    }
    
    public function create()
    {
        try {
            // Carica piano dei conti con struttura corretta
            $piano_conti = DB::connection('mysql')
                ->table('piano_conti')
                ->select('id', 'mastro', 'conto', 'sottoconto', 'descrizione', 'tipo')
                ->where('attivo', true)
                ->orderBy('mastro')
                ->orderBy('conto')
                ->orderBy('sottoconto')
                ->get();
            
            // Carica gestioni attive
            $gestioni = DB::connection('mysql')
                ->table('gestioni')
                ->select('id', 'denominazione', 'anno')
                ->where('attivo', true)
                ->orderBy('anno', 'desc')
                ->get();
            
            return view('modules.contabilita.create', compact('piano_conti', 'gestioni'));
            
        } catch (\Exception $e) {
            \Log::error("Errore Contabilità create: " . $e->getMessage());
            return back()->with('error', 'Errore nel caricamento del form: ' . $e->getMessage());
        }
    }
    
    public function store(Request $request)
    {
        try {
            $validated = $request->validate([
                'data_registrazione' => 'required|date',
                'descrizione' => 'required|string|max:500',
                'importo_dare' => 'nullable|numeric|min:0',
                'importo_avere' => 'nullable|numeric|min:0',
                'conto_id' => 'required|exists:piano_conti,id',
                'gestione_id' => 'required|exists:gestioni,id'
            ]);
            
            // Controllo partita doppia
            if (($validated['importo_dare'] ?? 0) == 0 && ($validated['importo_avere'] ?? 0) == 0) {
                return back()->withErrors(['importo' => 'Deve essere inserito almeno un importo in DARE o AVERE']);
            }
            
            DB::connection('mysql')->table('registrazioni_contabili')->insert([
                'data_registrazione' => $validated['data_registrazione'],
                'descrizione' => $validated['descrizione'],
                'importo_dare' => $validated['importo_dare'] ?? 0,
                'importo_avere' => $validated['importo_avere'] ?? 0,
                'piano_conti_id' => $validated['conto_id'],
                'gestione_id' => $validated['gestione_id'],
                'user_id' => auth()->id(),
                'created_at' => now(),
                'updated_at' => now()
            ]);
            
            return redirect()->route('admin.contabilita.index')
                ->with('success', 'Registrazione contabile creata con successo!');
                
        } catch (\Exception $e) {
            \Log::error("Errore Contabilità store: " . $e->getMessage());
            return back()->with('error', 'Errore nella creazione: ' . $e->getMessage());
        }
    }
}

3. Routes Integration Corretta

<?php
// app/Modules/Contabilita/Routes/web.php

use Illuminate\Support\Facades\Route;
use App\Modules\Contabilita\Controllers\ContabilitaController;

// Gruppo route admin
Route::middleware(['web', 'auth', 'can:admin-access'])->group(function () {
    Route::prefix('admin/contabilita')->name('admin.contabilita.')->group(function () {
        Route::get('/', [ContabilitaController::class, 'index'])->name('index');
        Route::get('/create', [ContabilitaController::class, 'create'])->name('create');
        Route::post('/', [ContabilitaController::class, 'store'])->name('store');
        Route::get('/{id}', [ContabilitaController::class, 'show'])->name('show');
        Route::get('/{id}/edit', [ContabilitaController::class, 'edit'])->name('edit');
        Route::put('/{id}', [ContabilitaController::class, 'update'])->name('update');
        Route::delete('/{id}', [ContabilitaController::class, 'destroy'])->name('destroy');
    });
});

4. Layout NetGescon Corretto

{{-- resources/views/modules/contabilita/index.blade.php --}}
@extends('admin.layouts.netgescon')

@section('title', 'Contabilità')

@section('navbar_title', 'CONTABILITÀ')

@section('content')
<div class="content-wrapper">
    <div class="content-header">
        <div class="container-fluid">
            <div class="row mb-2">
                <div class="col-12">
                    <nav aria-label="breadcrumb">
                        <ol class="breadcrumb">
                            <li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Home</a></li>
                            <li class="breadcrumb-item active">Contabilità</li>
                        </ol>
                    </nav>
                </div>
            </div>
        </div>
    </div>

    <section class="content">
        <div class="container-fluid">
            <!-- Sub-Menu come negli Stabili -->
            <div class="row mb-3">
                <div class="col-12">
                    <div class="card">
                        <div class="card-body">
                            <div class="btn-group" role="group">
                                <a href="{{ route('admin.contabilita.index') }}" 
                                   class="btn btn-outline-primary {{ request()->routeIs('admin.contabilita.index') ? 'active' : '' }}">
                                    <i class="fas fa-list"></i> Registrazioni
                                </a>
                                <a href="{{ route('admin.contabilita.create') }}" 
                                   class="btn btn-outline-success {{ request()->routeIs('admin.contabilita.create') ? 'active' : '' }}">
                                    <i class="fas fa-plus"></i> Nuova Registrazione
                                </a>
                                <a href="#" class="btn btn-outline-info">
                                    <i class="fas fa-chart-line"></i> Bilancio
                                </a>
                                <a href="#" class="btn btn-outline-info">
                                    <i class="fas fa-file-export"></i> Export
                                </a>
                                <a href="#" class="btn btn-outline-secondary">
                                    <i class="fas fa-cog"></i> Piano dei Conti
                                </a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            
            <!-- Statistiche DARE/AVERE -->
            <div class="row mb-3">
                <div class="col-md-3">
                    <div class="small-box bg-success">
                        <div class="inner">
                            <h3>€ 0,00</h3>
                            <p>Totale DARE</p>
                        </div>
                        <div class="icon">
                            <i class="fas fa-arrow-up"></i>
                        </div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="small-box bg-warning">
                        <div class="inner">
                            <h3>€ 0,00</h3>
                            <p>Totale AVERE</p>
                        </div>
                        <div class="icon">
                            <i class="fas fa-arrow-down"></i>
                        </div>
                    </div>
                </div>
                <div class="col-md-3">
                    <div class="small-box bg-info">
                        <div class="inner">
                            <h3>€ 0,00</h3>
                            <p>Differenza</p>
                        </div>
                        <div class="icon">
                            <i class="fas fa-balance-scale"></i>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Tabella Registrazioni -->
            <div class="row">
                <div class="col-12">
                    <div class="card">
                        <div class="card-header">
                            <h3 class="card-title">Registrazioni Contabili</h3>
                        </div>
                        <div class="card-body">
                            @if($registrazioni->count() > 0)
                                <div class="table-responsive">
                                    <table class="table table-bordered table-striped">
                                        <thead>
                                            <tr>
                                                <th>Data</th>
                                                <th>Descrizione</th>
                                                <th>Conto</th>
                                                <th>DARE</th>
                                                <th>AVERE</th>
                                                <th>Azioni</th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            @foreach($registrazioni as $registrazione)
                                                <tr>
                                                    <td>{{ \Carbon\Carbon::parse($registrazione->data_registrazione)->format('d/m/Y') }}</td>
                                                    <td>{{ $registrazione->descrizione }}</td>
                                                    <td>{{ $registrazione->conto ?? 'N/A' }}</td>
                                                    <td class="text-right">
                                                        @if($registrazione->importo_dare > 0)
                                                            <span class="text-success">€ {{ number_format($registrazione->importo_dare, 2, ',', '.') }}</span>
                                                        @endif
                                                    </td>
                                                    <td class="text-right">
                                                        @if($registrazione->importo_avere > 0)
                                                            <span class="text-danger">€ {{ number_format($registrazione->importo_avere, 2, ',', '.') }}</span>
                                                        @endif
                                                    </td>
                                                    <td>
                                                        <div class="btn-group" role="group">
                                                            <a href="{{ route('admin.contabilita.show', $registrazione->id) }}" 
                                                               class="btn btn-sm btn-info">
                                                                <i class="fas fa-eye"></i>
                                                            </a>
                                                            <a href="{{ route('admin.contabilita.edit', $registrazione->id) }}" 
                                                               class="btn btn-sm btn-warning">
                                                                <i class="fas fa-edit"></i>
                                                            </a>
                                                            <form method="POST" action="{{ route('admin.contabilita.destroy', $registrazione->id) }}" 
                                                                  style="display: inline-block;">
                                                                @csrf
                                                                @method('DELETE')
                                                                <button type="submit" class="btn btn-sm btn-danger" 
                                                                        onclick="return confirm('Sei sicuro di voler eliminare questa registrazione?')">
                                                                    <i class="fas fa-trash"></i>
                                                                </button>
                                                            </form>
                                                        </div>
                                                    </td>
                                                </tr>
                                            @endforeach
                                        </tbody>
                                    </table>
                                </div>
                                
                                {{ $registrazioni->links() }}
                            @else
                                <div class="alert alert-info">
                                    <i class="fas fa-info-circle"></i>
                                    Nessuna registrazione contabile presente.
                                    <a href="{{ route('admin.contabilita.create') }}" class="btn btn-sm btn-primary ml-2">
                                        Crea la prima registrazione
                                    </a>
                                </div>
                            @endif
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>
</div>
@endsection

🔑 CREDENZIALI DATABASE UNIFICATE

Configurazione Standard

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=netgescon
DB_USERNAME=netgescon_user
DB_PASSWORD=NetGescon2024!

Utilizzo nei Controller

// SEMPRE utilizzare la connessione mysql specifica
$data = DB::connection('mysql')->table('nome_tabella')->get();

// NO connection() default - può causare errori
$data = DB::table('nome_tabella')->get(); // ❌ EVITARE

📊 STRUTTURA DATABASE STANDARD

Naming Convention Italiana

-- Tabelle principali (nomi italiani senza accenti)
CREATE TABLE piano_conti (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    mastro VARCHAR(10) NOT NULL,
    conto VARCHAR(10) NOT NULL,
    sottoconto VARCHAR(10) NOT NULL,
    descrizione VARCHAR(255) NOT NULL,
    tipo ENUM('attivo', 'passivo', 'costo', 'ricavo') NOT NULL,
    attivo BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    INDEX idx_mastro_conto (mastro, conto, sottoconto),
    INDEX idx_tipo (tipo),
    INDEX idx_attivo (attivo)
);

CREATE TABLE gestioni (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    denominazione VARCHAR(255) NOT NULL,
    anno INT NOT NULL,
    data_inizio DATE NOT NULL,
    data_fine DATE NOT NULL,
    attivo BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    INDEX idx_anno (anno),
    INDEX idx_attivo (attivo)
);

CREATE TABLE registrazioni_contabili (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    data_registrazione DATE NOT NULL,
    descrizione TEXT NOT NULL,
    importo_dare DECIMAL(12,2) DEFAULT 0,
    importo_avere DECIMAL(12,2) DEFAULT 0,
    piano_conti_id BIGINT UNSIGNED NOT NULL,
    gestione_id BIGINT UNSIGNED NOT NULL,
    user_id BIGINT UNSIGNED NOT NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    FOREIGN KEY (piano_conti_id) REFERENCES piano_conti(id),
    FOREIGN KEY (gestione_id) REFERENCES gestioni(id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    
    INDEX idx_data_registrazione (data_registrazione),
    INDEX idx_piano_conti (piano_conti_id),
    INDEX idx_gestione (gestione_id)
);

🎨 DESIGN SYSTEM NETGESCON

Layout Base Obbligatorio

@extends('admin.layouts.netgescon')

Struttura Sub-Menu Standard

<!-- Sub-Menu come negli Stabili -->
<div class="row mb-3">
    <div class="col-12">
        <div class="card">
            <div class="card-body">
                <div class="btn-group" role="group">
                    <a href="{{ route('admin.modulo.index') }}" 
                       class="btn btn-outline-primary {{ request()->routeIs('admin.modulo.index') ? 'active' : '' }}">
                        <i class="fas fa-list"></i> Lista
                    </a>
                    <a href="{{ route('admin.modulo.create') }}" 
                       class="btn btn-outline-success {{ request()->routeIs('admin.modulo.create') ? 'active' : '' }}">
                        <i class="fas fa-plus"></i> Nuovo
                    </a>
                    <!-- Altri pulsanti del modulo -->
                </div>
            </div>
        </div>
    </div>
</div>

Cards Statistiche Standard

<div class="row mb-3">
    <div class="col-md-3">
        <div class="small-box bg-success">
            <div class="inner">
                <h3>{{ $totale }}</h3>
                <p>Totale Elementi</p>
            </div>
            <div class="icon">
                <i class="fas fa-chart-line"></i>
            </div>
        </div>
    </div>
    <!-- Altre statistiche -->
</div>

⚠️ PROBLEMI RISOLTI E BEST PRACTICES

1. Route Conflicts

// ❌ PROBLEMA: Route conflittuali
Route::get('/contabilita', [OldController::class, 'index']); // Old route
Route::get('/admin/contabilita', [NewController::class, 'index']); // New route

// ✅ SOLUZIONE: Disabilitare vecchie route
/*
Route::prefix('admin')->name('admin.')->group(function () {
    // OLD routes disabled
    // Route::resource('contabilita', ContabilitaAdminController::class);
});
*/

2. Database Column References

// ❌ PROBLEMA: Riferimenti a colonne inesistenti
DB::table('piano_conti')->where('codice', '100')->get(); // Campo 'codice' non esiste

// ✅ SOLUZIONE: Utilizzare struttura corretta
DB::table('piano_conti')
    ->select('id', 'mastro', 'conto', 'sottoconto', 'descrizione')
    ->where('mastro', '1')
    ->get();

3. Layout Inheritance

{{-- ❌ PROBLEMA: Layout non NetGescon --}}
@extends('modules.layouts.base')

{{-- ✅ SOLUZIONE: Layout NetGescon standard --}}
@extends('admin.layouts.netgescon')

4. Database Connection

// ❌ PROBLEMA: Connection di default
$data = DB::table('tabella')->get();

// ✅ SOLUZIONE: Connection esplicita
$data = DB::connection('mysql')->table('tabella')->get();

🧪 TESTING E VALIDAZIONE

Checklist Pre-Rilascio

Database

  • Connection mysql funzionante
  • Tabelle con naming italiano
  • Foreign keys corrette
  • Indici per performance

Controller

  • Middleware auth applicati
  • Exception handling
  • Validation input
  • Logging errori

Views

  • Layout admin.layouts.netgescon
  • Sub-menu come stabili
  • Responsive design
  • Breadcrumb navigation

Routes

  • Prefisso admin/modulo
  • Named routes admin.modulo.*
  • Middleware corretti
  • No conflitti route

Integration

  • Navigazione da sidebar
  • Permessi utente
  • Performance accettabili
  • No JavaScript errors

🚀 DEPLOYMENT E DISTRIBUZIONE

Script di Installazione

#!/bin/bash
# install-module.sh

MODULE_NAME=$1
MODULE_PATH="app/Modules/$MODULE_NAME"

echo "🚀 Installazione modulo $MODULE_NAME..."

# 1. Verifica struttura
if [ ! -d "$MODULE_PATH" ]; then
    echo "❌ Modulo non trovato: $MODULE_PATH"
    exit 1
fi

# 2. Carica routes
echo "📋 Caricamento routes..."
if [ -f "$MODULE_PATH/Routes/web.php" ]; then
    # Includi routes nel RouteServiceProvider
    echo "Route::middleware('web')->group(base_path('$MODULE_PATH/Routes/web.php'));" >> routes/web.php
fi

# 3. Esegui migrations
echo "🗄️  Esecuzione migrations..."
php artisan migrate --force

# 4. Clear cache
echo "🧹 Pulizia cache..."
php artisan route:clear
php artisan view:clear
php artisan config:clear

# 5. Test installazione
echo "🧪 Test installazione..."
php artisan route:list | grep -i $MODULE_NAME

echo "✅ Modulo $MODULE_NAME installato con successo!"

📚 DOCUMENTAZIONE OBBLIGATORIA

README.md del Modulo

# Modulo [NomeModulo]

## Descrizione
Breve descrizione del modulo e delle sue funzionalità.

## Installazione
```bash
./install-module.sh NomeModulo

Configurazione

Configurazioni necessarie e opzionali.

Utilizzo

Guide per l'utilizzo delle funzionalità principali.

API

Documentazione delle API esposte (se presenti).

Troubleshooting

Problemi comuni e loro soluzioni.


### **CHANGELOG.md**
```markdown
# Changelog

## [1.0.0] - 2025-08-03
### Added
- Implementazione iniziale del modulo
- CRUD completo per entità principali
- Integrazione con design system NetGescon

### Fixed
- Risolti conflitti route
- Corretti riferimenti database
- Ottimizzata query performance

🎯 MODULI DI ESEMPIO COMPLETATI

1. Modulo Contabilità

  • Stato: Completato e testato
  • Features: Registrazioni DARE/AVERE, Piano dei conti, Gestioni
  • Layout: Sub-menu integrato come stabili
  • Database: Struttura italiana ottimizzata
  • Performance: Query ottimizzate con indici

2. Prossimi Moduli in Pipeline

StampeRate 🔄

  • Stato: Layout fix necessari
  • Action: Applicare stesso pattern di Contabilità
  • Template: Utilizzare sub-menu e design system

Assemblee 📋

  • Features: Convocazioni, Verbali, Votazioni
  • Database: assemblee, partecipanti, votazioni
  • Integration: Notifiche email, PDF generation

Manutenzioni 📋

  • Features: Calendario, Fornitori, Preventivi
  • Database: manutenzioni, fornitori, interventi
  • Integration: Upload documenti, Tracking costi

💰 MODELLO COMMERCIALE

Pricing Strategy Aggiornata

{
  "contabilita": {
    "price": 199,
    "currency": "EUR",
    "billing": "yearly",
    "features": [
      "Piano dei conti personalizzabile",
      "Registrazioni DARE/AVERE", 
      "Bilancio automatico",
      "Export per commercialista",
      "Report finanziari",
      "Backup automatico"
    ]
  },
  "stampe_rate": {
    "price": 99,
    "currency": "EUR", 
    "billing": "yearly",
    "features": [
      "Stampe personalizzate",
      "Rate di pagamento",
      "Solleciti automatici",
      "Export PDF/Excel"
    ]
  }
}

Revenue Projection 2025-2026

  • Q4 2025: 2 moduli × 50 clienti × €150 avg = €15.000
  • Q1 2026: 4 moduli × 100 clienti × €200 avg = €80.000
  • Q2 2026: 6 moduli × 200 clienti × €250 avg = €300.000

CHECKLIST FINALE MODULO

Pre-Development

  • Analisi requisiti completata
  • Database schema definito
  • Mockup UI/UX approvati
  • Pricing strategy definita

Development

  • Struttura directory creata
  • Controller implementato
  • Routes configurate
  • Views con layout NetGescon
  • Database migration creata
  • Seeder con dati demo

Testing

  • Unit tests scritti
  • Integration tests passati
  • Performance benchmark OK
  • Security scan pulito
  • Cross-browser testing

Documentation

  • README.md completo
  • CHANGELOG.md aggiornato
  • API documentation (se presente)
  • User guide creata
  • Install guide testata

Deployment

  • Script installazione funzionante
  • Backup pre-install
  • Rollback procedure testata
  • Production deployment OK
  • Monitoring attivo

🎯 QUESTA GUIDA È STATA TESTATA E VALIDATA CON IL MODULO CONTABILITÀ

Tutti i pattern, best practices e soluzioni documentate sono stati implementati e testati nel modulo Contabilità che ora funziona correttamente con:

  • Layout NetGescon integrato
  • Sub-menu come negli stabili
  • Database con naming italiano
  • Credenziali unificate
  • Route senza conflitti
  • Performance ottimizzate

Utilizzare questa guida come template per tutti i nuovi moduli NetGescon.