netgescon-master/app/Http/Controllers/Admin/RipartizioneSpesaController.php
Pikappa2 15e0be69ee 🎯 FASE 2 COMPLETATA: Controllers e UI per Sistema Spese e Rateizzazioni
 CONTROLLERS IMPLEMENTATI:
- VoceSpesaController: CRUD completo con filtri avanzati, duplicazione, AJAX
- RipartizioneSpesaController: Calcolo automatico/manuale, workflow stati
- PianoRateizzazioneController: Gestione piani rate con calcolo interessi
- RataController: Pagamenti, posticipazioni, report, export CSV

 INTERFACCE UI RESPONSIVE:
- Voci di Spesa: Elenco filtrato, form creazione Bootstrap 5
- Design System: Layout moderno con Font Awesome, responsive
- Filtri Real-time: Aggiornamento automatico con AJAX
- Validazioni: Client-side e server-side

 SISTEMA AUTORIZZAZIONI:
- Policies complete per tutti i modelli
- Controlli ownership per amministratori
- Role-based access control
- Data integrity e security

 FUNZIONALITÀ AVANZATE:
- Calcoli automatici millesimi e rate
- Gestione stati workflow (bozza→confermata→completata)
- Codici alfanumerici univoci (SP, RP, PR)
- Transazioni atomiche con rollback
- Export CSV e reporting

🚀 SISTEMA CORE OPERATIVO: Pronto per gestione completa spese condominiali
📱 UI PRODUCTION-READY: Interfacce moderne e intuitive
🔐 SICUREZZA COMPLETA: Autorizzazioni e validazioni robuste
📊 BUSINESS LOGIC: Calcoli automatici e workflow operativi

Next: Implementazione viste rimanenti e sistema plugin
2025-07-08 18:56:15 +02:00

398 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\RipartizioneSpese;
use App\Models\DettaglioRipartizioneSpese;
use App\Models\VoceSpesa;
use App\Models\Stabile;
use App\Models\TabellaMillesimale;
use App\Models\UnitaImmobiliare;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class RipartizioneSpesaController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = RipartizioneSpese::with(['voceSpesa.stabile', 'tabellaMillesimale'])
->whereHas('voceSpesa.stabile', function($q) {
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
});
// Filtro per stabile
if ($request->filled('stabile_id')) {
$query->whereHas('voceSpesa', function($q) use ($request) {
$q->where('stabile_id', $request->stabile_id);
});
}
// Filtro per voce di spesa
if ($request->filled('voce_spesa_id')) {
$query->where('voce_spesa_id', $request->voce_spesa_id);
}
// Filtro per stato
if ($request->filled('stato')) {
$query->where('stato', $request->stato);
}
// Filtro per data
if ($request->filled('data_da')) {
$query->where('data_ripartizione', '>=', $request->data_da);
}
if ($request->filled('data_a')) {
$query->where('data_ripartizione', '<=', $request->data_a);
}
$ripartizioni = $query->orderBy('data_ripartizione', 'desc')->paginate(15);
// Dati per i filtri
$stabili = Stabile::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)
->orderBy('denominazione')
->get();
$vociSpesa = VoceSpesa::whereHas('stabile', function($q) {
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
})
->orderBy('denominazione')
->get();
return view('admin.ripartizioni-spesa.index', compact('ripartizioni', 'stabili', 'vociSpesa'));
}
/**
* Show the form for creating a new resource.
*/
public function create(Request $request)
{
$vociSpesa = VoceSpesa::with(['stabile', 'tabellaMillesimaleDefault'])
->whereHas('stabile', function($q) {
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
})
->where('stato', 'attiva')
->orderBy('denominazione')
->get();
$voceSpesaSelezionata = null;
if ($request->filled('voce_spesa_id')) {
$voceSpesaSelezionata = $vociSpesa->firstWhere('id', $request->voce_spesa_id);
}
return view('admin.ripartizioni-spesa.create', compact('vociSpesa', 'voceSpesaSelezionata'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'voce_spesa_id' => 'required|exists:voci_spesa,id',
'descrizione' => 'required|string|max:255',
'importo_totale' => 'required|numeric|min:0',
'data_ripartizione' => 'required|date',
'tabella_millesimale_id' => 'required|exists:tabelle_millesimali,id',
'periodo_riferimento' => 'nullable|string|max:100',
'note' => 'nullable|string',
'dettagli' => 'nullable|array',
'dettagli.*.unita_immobiliare_id' => 'required|exists:unita_immobiliari,id',
'dettagli.*.importo' => 'required|numeric|min:0',
'dettagli.*.esclusa' => 'nullable|boolean',
'dettagli.*.note' => 'nullable|string|max:255',
]);
// Verifica che la voce di spesa appartenga all'amministratore
$voceSpesa = VoceSpesa::whereHas('stabile', function($q) {
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
})
->findOrFail($request->voce_spesa_id);
// Verifica che la tabella millesimale appartenga allo stabile
$tabellaMillesimale = TabellaMillesimale::where('id', $request->tabella_millesimale_id)
->where('stabile_id', $voceSpesa->stabile_id)
->firstOrFail();
DB::beginTransaction();
try {
// Crea la ripartizione principale
$ripartizione = RipartizioneSpese::create([
'codice_ripartizione' => $this->generateCodiceRipartizione(),
'voce_spesa_id' => $request->voce_spesa_id,
'descrizione' => $request->descrizione,
'importo_totale' => $request->importo_totale,
'data_ripartizione' => $request->data_ripartizione,
'tabella_millesimale_id' => $request->tabella_millesimale_id,
'periodo_riferimento' => $request->periodo_riferimento,
'note' => $request->note,
'stato' => 'bozza',
'created_by' => Auth::id(),
]);
// Se non sono stati forniti dettagli, calcola automaticamente
if (empty($request->dettagli)) {
$this->calcolaRipartizioneAutomatica($ripartizione);
} else {
// Crea i dettagli forniti
foreach ($request->dettagli as $dettaglio) {
DettaglioRipartizioneSpese::create([
'ripartizione_spese_id' => $ripartizione->id,
'unita_immobiliare_id' => $dettaglio['unita_immobiliare_id'],
'importo' => $dettaglio['importo'],
'esclusa' => $dettaglio['esclusa'] ?? false,
'note' => $dettaglio['note'] ?? null,
]);
}
}
DB::commit();
return redirect()->route('admin.ripartizioni-spesa.show', $ripartizione)
->with('success', 'Ripartizione spesa creata con successo.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()
->withInput()
->with('error', 'Errore durante la creazione della ripartizione: ' . $e->getMessage());
}
}
/**
* Display the specified resource.
*/
public function show(RipartizioneSpese $ripartizioneSpesa)
{
// Verifica autorizzazione
$this->authorize('view', $ripartizioneSpesa);
$ripartizioneSpesa->load([
'voceSpesa.stabile',
'tabellaMillesimale',
'dettagli.unitaImmobiliare.anagraficaCondominiale.soggetto',
'createdBy',
'updatedBy'
]);
return view('admin.ripartizioni-spesa.show', compact('ripartizioneSpesa'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(RipartizioneSpese $ripartizioneSpesa)
{
// Verifica autorizzazione
$this->authorize('update', $ripartizioneSpesa);
// Solo le ripartizioni in bozza possono essere modificate
if ($ripartizioneSpesa->stato !== 'bozza') {
return redirect()->route('admin.ripartizioni-spesa.show', $ripartizioneSpesa)
->with('error', 'Impossibile modificare una ripartizione già confermata.');
}
$ripartizioneSpesa->load([
'voceSpesa.stabile',
'tabellaMillesimale',
'dettagli.unitaImmobiliare'
]);
$tabelleMillesimali = TabellaMillesimale::where('stabile_id', $ripartizioneSpesa->voceSpesa->stabile_id)
->where('stato', 'attiva')
->orderBy('denominazione')
->get();
return view('admin.ripartizioni-spesa.edit', compact('ripartizioneSpesa', 'tabelleMillesimali'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, RipartizioneSpese $ripartizioneSpesa)
{
// Verifica autorizzazione
$this->authorize('update', $ripartizioneSpesa);
// Solo le ripartizioni in bozza possono essere modificate
if ($ripartizioneSpesa->stato !== 'bozza') {
return redirect()->route('admin.ripartizioni-spesa.show', $ripartizioneSpesa)
->with('error', 'Impossibile modificare una ripartizione già confermata.');
}
$request->validate([
'descrizione' => 'required|string|max:255',
'importo_totale' => 'required|numeric|min:0',
'data_ripartizione' => 'required|date',
'tabella_millesimale_id' => 'required|exists:tabelle_millesimali,id',
'periodo_riferimento' => 'nullable|string|max:100',
'note' => 'nullable|string',
'dettagli' => 'nullable|array',
'dettagli.*.unita_immobiliare_id' => 'required|exists:unita_immobiliari,id',
'dettagli.*.importo' => 'required|numeric|min:0',
'dettagli.*.esclusa' => 'nullable|boolean',
'dettagli.*.note' => 'nullable|string|max:255',
]);
// Verifica che la tabella millesimale appartenga allo stabile
$tabellaMillesimale = TabellaMillesimale::where('id', $request->tabella_millesimale_id)
->where('stabile_id', $ripartizioneSpesa->voceSpesa->stabile_id)
->firstOrFail();
DB::beginTransaction();
try {
// Aggiorna la ripartizione principale
$ripartizioneSpesa->update([
'descrizione' => $request->descrizione,
'importo_totale' => $request->importo_totale,
'data_ripartizione' => $request->data_ripartizione,
'tabella_millesimale_id' => $request->tabella_millesimale_id,
'periodo_riferimento' => $request->periodo_riferimento,
'note' => $request->note,
'updated_by' => Auth::id(),
]);
// Elimina i dettagli esistenti
$ripartizioneSpesa->dettagli()->delete();
// Se non sono stati forniti dettagli, calcola automaticamente
if (empty($request->dettagli)) {
$this->calcolaRipartizioneAutomatica($ripartizioneSpesa);
} else {
// Crea i nuovi dettagli
foreach ($request->dettagli as $dettaglio) {
DettaglioRipartizioneSpese::create([
'ripartizione_spese_id' => $ripartizioneSpesa->id,
'unita_immobiliare_id' => $dettaglio['unita_immobiliare_id'],
'importo' => $dettaglio['importo'],
'esclusa' => $dettaglio['esclusa'] ?? false,
'note' => $dettaglio['note'] ?? null,
]);
}
}
DB::commit();
return redirect()->route('admin.ripartizioni-spesa.show', $ripartizioneSpesa)
->with('success', 'Ripartizione spesa aggiornata con successo.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()
->withInput()
->with('error', 'Errore durante l\'aggiornamento della ripartizione: ' . $e->getMessage());
}
}
/**
* Remove the specified resource from storage.
*/
public function destroy(RipartizioneSpese $ripartizioneSpesa)
{
// Verifica autorizzazione
$this->authorize('delete', $ripartizioneSpesa);
// Solo le ripartizioni in bozza possono essere eliminate
if ($ripartizioneSpesa->stato !== 'bozza') {
return redirect()->route('admin.ripartizioni-spesa.index')
->with('error', 'Impossibile eliminare una ripartizione già confermata.');
}
DB::beginTransaction();
try {
// Elimina i dettagli
$ripartizioneSpesa->dettagli()->delete();
// Elimina la ripartizione
$ripartizioneSpesa->delete();
DB::commit();
return redirect()->route('admin.ripartizioni-spesa.index')
->with('success', 'Ripartizione spesa eliminata con successo.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->route('admin.ripartizioni-spesa.index')
->with('error', 'Errore durante l\'eliminazione della ripartizione: ' . $e->getMessage());
}
}
/**
* Conferma una ripartizione spesa
*/
public function conferma(RipartizioneSpese $ripartizioneSpesa)
{
// Verifica autorizzazione
$this->authorize('update', $ripartizioneSpesa);
if ($ripartizioneSpesa->stato !== 'bozza') {
return redirect()->route('admin.ripartizioni-spesa.show', $ripartizioneSpesa)
->with('error', 'La ripartizione è già stata confermata.');
}
// Verifica che ci siano dettagli
if (!$ripartizioneSpesa->dettagli()->exists()) {
return redirect()->route('admin.ripartizioni-spesa.show', $ripartizioneSpesa)
->with('error', 'Impossibile confermare una ripartizione senza dettagli.');
}
// Verifica che la somma dei dettagli corrisponda all'importo totale
$sommaDettagli = $ripartizioneSpesa->dettagli()->sum('importo');
if (abs($sommaDettagli - $ripartizioneSpesa->importo_totale) > 0.01) {
return redirect()->route('admin.ripartizioni-spesa.show', $ripartizioneSpesa)
->with('error', 'La somma dei dettagli non corrisponde all\'importo totale.');
}
$ripartizioneSpesa->update([
'stato' => 'confermata',
'data_conferma' => now(),
'confermata_by' => Auth::id(),
]);
return redirect()->route('admin.ripartizioni-spesa.show', $ripartizioneSpesa)
->with('success', 'Ripartizione spesa confermata con successo.');
}
/**
* Calcola automaticamente la ripartizione basata sui millesimi
*/
private function calcolaRipartizioneAutomatica(RipartizioneSpese $ripartizione)
{
$tabella = $ripartizione->tabellaMillesimale;
$unita = UnitaImmobiliare::where('stabile_id', $ripartizione->voceSpesa->stabile_id)->get();
foreach ($unita as $unita_immobiliare) {
$millesimi = $tabella->getMillesimiForUnita($unita_immobiliare->id);
$importo = ($ripartizione->importo_totale * $millesimi) / 1000;
DettaglioRipartizioneSpese::create([
'ripartizione_spese_id' => $ripartizione->id,
'unita_immobiliare_id' => $unita_immobiliare->id,
'importo' => round($importo, 2),
'esclusa' => false,
]);
}
}
/**
* Genera un codice ripartizione univoco
*/
private function generateCodiceRipartizione(): string
{
do {
$codice = 'RP' . strtoupper(Str::random(6));
} while (RipartizioneSpese::where('codice_ripartizione', $codice)->exists());
return $codice;
}
}