netgescon-master/app/Http/Controllers/Admin/PianoRateizzazioneController.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

453 lines
18 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\PianoRateizzazione;
use App\Models\Rata;
use App\Models\RipartizioneSpese;
use App\Models\DettaglioRipartizioneSpese;
use App\Models\UnitaImmobiliare;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Carbon\Carbon;
class PianoRateizzazioneController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = PianoRateizzazione::with(['ripartizioneSpese.voceSpesa.stabile', 'unitaImmobiliare'])
->whereHas('ripartizioneSpese.voceSpesa.stabile', function($q) {
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
});
// Filtro per stabile
if ($request->filled('stabile_id')) {
$query->whereHas('ripartizioneSpese.voceSpesa', function($q) use ($request) {
$q->where('stabile_id', $request->stabile_id);
});
}
// Filtro per unità immobiliare
if ($request->filled('unita_immobiliare_id')) {
$query->where('unita_immobiliare_id', $request->unita_immobiliare_id);
}
// Filtro per stato
if ($request->filled('stato')) {
$query->where('stato', $request->stato);
}
// Filtro per data
if ($request->filled('data_da')) {
$query->where('data_inizio', '>=', $request->data_da);
}
if ($request->filled('data_a')) {
$query->where('data_inizio', '<=', $request->data_a);
}
$pianiRateizzazione = $query->orderBy('data_inizio', 'desc')->paginate(15);
// Dati per i filtri
$stabili = \App\Models\Stabile::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)
->orderBy('denominazione')
->get();
return view('admin.piani-rateizzazione.index', compact('pianiRateizzazione', 'stabili'));
}
/**
* Show the form for creating a new resource.
*/
public function create(Request $request)
{
$ripartizioneSpesa = null;
$dettaglioRipartizione = null;
// Se arriva da una ripartizione specifica
if ($request->filled('ripartizione_spesa_id')) {
$ripartizioneSpesa = RipartizioneSpese::with(['voceSpesa.stabile', 'dettagli.unitaImmobiliare'])
->whereHas('voceSpesa.stabile', function($q) {
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
})
->findOrFail($request->ripartizione_spesa_id);
}
// Se arriva da un dettaglio specifico
if ($request->filled('dettaglio_ripartizione_id')) {
$dettaglioRipartizione = DettaglioRipartizioneSpese::with(['ripartizioneSpese.voceSpesa.stabile', 'unitaImmobiliare'])
->whereHas('ripartizioneSpese.voceSpesa.stabile', function($q) {
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
})
->findOrFail($request->dettaglio_ripartizione_id);
$ripartizioneSpesa = $dettaglioRipartizione->ripartizioneSpese;
}
// Ripartizioni disponibili
$ripartizioni = RipartizioneSpese::with(['voceSpesa.stabile'])
->whereHas('voceSpesa.stabile', function($q) {
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
})
->where('stato', 'confermata')
->orderBy('data_ripartizione', 'desc')
->get();
return view('admin.piani-rateizzazione.create', compact('ripartizioni', 'ripartizioneSpesa', 'dettaglioRipartizione'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'ripartizione_spese_id' => 'required|exists:ripartizioni_spese,id',
'unita_immobiliare_id' => 'required|exists:unita_immobiliari,id',
'denominazione' => 'required|string|max:255',
'importo_totale' => 'required|numeric|min:0',
'numero_rate' => 'required|integer|min:1|max:60',
'data_inizio' => 'required|date',
'frequenza' => 'required|in:mensile,bimestrale,trimestrale,semestrale',
'importo_prima_rata' => 'nullable|numeric|min:0',
'note' => 'nullable|string',
'applica_interessi' => 'nullable|boolean',
'tasso_interesse' => 'nullable|numeric|min:0|max:100',
]);
// Verifica che la ripartizione appartenga all'amministratore
$ripartizioneSpesa = RipartizioneSpese::whereHas('voceSpesa.stabile', function($q) {
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
})
->where('stato', 'confermata')
->findOrFail($request->ripartizione_spese_id);
// Verifica che l'unità immobiliare appartenga al stabile della ripartizione
$unitaImmobiliare = UnitaImmobiliare::where('id', $request->unita_immobiliare_id)
->where('stabile_id', $ripartizioneSpesa->voceSpesa->stabile_id)
->firstOrFail();
// Verifica che l'unità abbia un dettaglio nella ripartizione
$dettaglioRipartizione = DettaglioRipartizioneSpese::where('ripartizione_spese_id', $request->ripartizione_spese_id)
->where('unita_immobiliare_id', $request->unita_immobiliare_id)
->firstOrFail();
// Verifica che l'importo totale non superi l'importo del dettaglio
if ($request->importo_totale > $dettaglioRipartizione->importo) {
return redirect()->back()
->withInput()
->with('error', 'L\'importo totale del piano non può superare l\'importo della ripartizione (' . number_format($dettaglioRipartizione->importo, 2) . ' €).');
}
DB::beginTransaction();
try {
// Crea il piano di rateizzazione
$pianoRateizzazione = PianoRateizzazione::create([
'codice_piano' => $this->generateCodicePiano(),
'ripartizione_spese_id' => $request->ripartizione_spese_id,
'unita_immobiliare_id' => $request->unita_immobiliare_id,
'denominazione' => $request->denominazione,
'importo_totale' => $request->importo_totale,
'numero_rate' => $request->numero_rate,
'data_inizio' => $request->data_inizio,
'frequenza' => $request->frequenza,
'importo_prima_rata' => $request->importo_prima_rata,
'note' => $request->note,
'applica_interessi' => $request->boolean('applica_interessi'),
'tasso_interesse' => $request->tasso_interesse,
'stato' => 'bozza',
'created_by' => Auth::id(),
]);
// Calcola e crea le rate
$this->calcolaRate($pianoRateizzazione);
DB::commit();
return redirect()->route('admin.piani-rateizzazione.show', $pianoRateizzazione)
->with('success', 'Piano di rateizzazione creato con successo.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()
->withInput()
->with('error', 'Errore durante la creazione del piano: ' . $e->getMessage());
}
}
/**
* Display the specified resource.
*/
public function show(PianoRateizzazione $pianoRateizzazione)
{
// Verifica autorizzazione
$this->authorize('view', $pianoRateizzazione);
$pianoRateizzazione->load([
'ripartizioneSpese.voceSpesa.stabile',
'unitaImmobiliare.anagraficaCondominiale.soggetto',
'rate' => function($query) {
$query->orderBy('numero_rata');
},
'createdBy',
'updatedBy'
]);
return view('admin.piani-rateizzazione.show', compact('pianoRateizzazione'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(PianoRateizzazione $pianoRateizzazione)
{
// Verifica autorizzazione
$this->authorize('update', $pianoRateizzazione);
// Solo i piani in bozza possono essere modificati
if ($pianoRateizzazione->stato !== 'bozza') {
return redirect()->route('admin.piani-rateizzazione.show', $pianoRateizzazione)
->with('error', 'Impossibile modificare un piano già attivato.');
}
$pianoRateizzazione->load([
'ripartizioneSpese.voceSpesa.stabile',
'unitaImmobiliare',
'rate' => function($query) {
$query->orderBy('numero_rata');
}
]);
return view('admin.piani-rateizzazione.edit', compact('pianoRateizzazione'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, PianoRateizzazione $pianoRateizzazione)
{
// Verifica autorizzazione
$this->authorize('update', $pianoRateizzazione);
// Solo i piani in bozza possono essere modificati
if ($pianoRateizzazione->stato !== 'bozza') {
return redirect()->route('admin.piani-rateizzazione.show', $pianoRateizzazione)
->with('error', 'Impossibile modificare un piano già attivato.');
}
$request->validate([
'denominazione' => 'required|string|max:255',
'importo_totale' => 'required|numeric|min:0',
'numero_rate' => 'required|integer|min:1|max:60',
'data_inizio' => 'required|date',
'frequenza' => 'required|in:mensile,bimestrale,trimestrale,semestrale',
'importo_prima_rata' => 'nullable|numeric|min:0',
'note' => 'nullable|string',
'applica_interessi' => 'nullable|boolean',
'tasso_interesse' => 'nullable|numeric|min:0|max:100',
]);
// Verifica che l'importo totale non superi l'importo del dettaglio
$dettaglioRipartizione = DettaglioRipartizioneSpese::where('ripartizione_spese_id', $pianoRateizzazione->ripartizione_spese_id)
->where('unita_immobiliare_id', $pianoRateizzazione->unita_immobiliare_id)
->firstOrFail();
if ($request->importo_totale > $dettaglioRipartizione->importo) {
return redirect()->back()
->withInput()
->with('error', 'L\'importo totale del piano non può superare l\'importo della ripartizione (' . number_format($dettaglioRipartizione->importo, 2) . ' €).');
}
DB::beginTransaction();
try {
// Aggiorna il piano
$pianoRateizzazione->update([
'denominazione' => $request->denominazione,
'importo_totale' => $request->importo_totale,
'numero_rate' => $request->numero_rate,
'data_inizio' => $request->data_inizio,
'frequenza' => $request->frequenza,
'importo_prima_rata' => $request->importo_prima_rata,
'note' => $request->note,
'applica_interessi' => $request->boolean('applica_interessi'),
'tasso_interesse' => $request->tasso_interesse,
'updated_by' => Auth::id(),
]);
// Elimina le rate esistenti
$pianoRateizzazione->rate()->delete();
// Ricalcola le rate
$this->calcolaRate($pianoRateizzazione);
DB::commit();
return redirect()->route('admin.piani-rateizzazione.show', $pianoRateizzazione)
->with('success', 'Piano di rateizzazione aggiornato con successo.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->back()
->withInput()
->with('error', 'Errore durante l\'aggiornamento del piano: ' . $e->getMessage());
}
}
/**
* Remove the specified resource from storage.
*/
public function destroy(PianoRateizzazione $pianoRateizzazione)
{
// Verifica autorizzazione
$this->authorize('delete', $pianoRateizzazione);
// Solo i piani in bozza possono essere eliminati
if ($pianoRateizzazione->stato !== 'bozza') {
return redirect()->route('admin.piani-rateizzazione.index')
->with('error', 'Impossibile eliminare un piano già attivato.');
}
DB::beginTransaction();
try {
// Elimina le rate
$pianoRateizzazione->rate()->delete();
// Elimina il piano
$pianoRateizzazione->delete();
DB::commit();
return redirect()->route('admin.piani-rateizzazione.index')
->with('success', 'Piano di rateizzazione eliminato con successo.');
} catch (\Exception $e) {
DB::rollBack();
return redirect()->route('admin.piani-rateizzazione.index')
->with('error', 'Errore durante l\'eliminazione del piano: ' . $e->getMessage());
}
}
/**
* Attiva un piano di rateizzazione
*/
public function attiva(PianoRateizzazione $pianoRateizzazione)
{
// Verifica autorizzazione
$this->authorize('update', $pianoRateizzazione);
if ($pianoRateizzazione->stato !== 'bozza') {
return redirect()->route('admin.piani-rateizzazione.show', $pianoRateizzazione)
->with('error', 'Il piano è già stato attivato.');
}
// Verifica che ci siano rate
if (!$pianoRateizzazione->rate()->exists()) {
return redirect()->route('admin.piani-rateizzazione.show', $pianoRateizzazione)
->with('error', 'Impossibile attivare un piano senza rate.');
}
$pianoRateizzazione->update([
'stato' => 'attivo',
'data_attivazione' => now(),
'attivato_by' => Auth::id(),
]);
return redirect()->route('admin.piani-rateizzazione.show', $pianoRateizzazione)
->with('success', 'Piano di rateizzazione attivato con successo.');
}
/**
* Sospende un piano di rateizzazione
*/
public function sospendi(PianoRateizzazione $pianoRateizzazione)
{
// Verifica autorizzazione
$this->authorize('update', $pianoRateizzazione);
if ($pianoRateizzazione->stato !== 'attivo') {
return redirect()->route('admin.piani-rateizzazione.show', $pianoRateizzazione)
->with('error', 'Il piano non è attivo.');
}
$pianoRateizzazione->update([
'stato' => 'sospeso',
'data_sospensione' => now(),
'sospeso_by' => Auth::id(),
]);
return redirect()->route('admin.piani-rateizzazione.show', $pianoRateizzazione)
->with('success', 'Piano di rateizzazione sospeso con successo.');
}
/**
* Calcola le rate per un piano di rateizzazione
*/
private function calcolaRate(PianoRateizzazione $piano)
{
$dataScadenza = Carbon::parse($piano->data_inizio);
$importoRata = $piano->importo_totale / $piano->numero_rate;
$importoPrimaRata = $piano->importo_prima_rata ?? $importoRata;
// Calcola gli interessi se applicabili
$importoTotaleConInteressi = $piano->importo_totale;
if ($piano->applica_interessi && $piano->tasso_interesse > 0) {
$importoTotaleConInteressi = $piano->importo_totale * (1 + ($piano->tasso_interesse / 100));
$importoRata = $importoTotaleConInteressi / $piano->numero_rate;
}
for ($i = 1; $i <= $piano->numero_rate; $i++) {
$importoCorrente = ($i === 1) ? $importoPrimaRata : $importoRata;
// Aggiusta l'ultima rata per eventuali arrotondamenti
if ($i === $piano->numero_rate) {
$totaleRatePrecedenti = $importoPrimaRata + (($piano->numero_rate - 2) * $importoRata);
$importoCorrente = $importoTotaleConInteressi - $totaleRatePrecedenti;
}
Rata::create([
'piano_rateizzazione_id' => $piano->id,
'numero_rata' => $i,
'importo' => round($importoCorrente, 2),
'data_scadenza' => $dataScadenza->copy(),
'stato' => 'da_pagare',
]);
// Calcola la prossima data di scadenza
switch ($piano->frequenza) {
case 'mensile':
$dataScadenza->addMonth();
break;
case 'bimestrale':
$dataScadenza->addMonths(2);
break;
case 'trimestrale':
$dataScadenza->addMonths(3);
break;
case 'semestrale':
$dataScadenza->addMonths(6);
break;
}
}
}
/**
* Genera un codice piano univoco
*/
private function generateCodicePiano(): string
{
do {
$codice = 'PR' . strtoupper(Str::random(6));
} while (PianoRateizzazione::where('codice_piano', $codice)->exists());
return $codice;
}
}