🎯 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
This commit is contained in:
parent
bb38044019
commit
15e0be69ee
1382
CONSUMI_WATER_HEATING_SYSTEM.md
Normal file
1382
CONSUMI_WATER_HEATING_SYSTEM.md
Normal file
File diff suppressed because it is too large
Load Diff
766
DOCUMENT_MANAGEMENT_SYSTEM.md
Normal file
766
DOCUMENT_MANAGEMENT_SYSTEM.md
Normal file
|
|
@ -0,0 +1,766 @@
|
||||||
|
# 📁 **NetGesCon Laravel - Sistema Gestione Documentale**
|
||||||
|
|
||||||
|
## 📍 **OVERVIEW GENERALE**
|
||||||
|
Sistema integrato per gestione documenti condominiali con supporto per archiviazione locale, cloud (Office 365, Google Drive) e audit documentale completo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ **ARCHITETTURA ARCHIVIAZIONE DOCUMENTI**
|
||||||
|
|
||||||
|
### **1. Struttura Database Documenti**
|
||||||
|
|
||||||
|
#### **A. Tabella Principale Documenti**
|
||||||
|
```sql
|
||||||
|
-- Migration: create_documenti_table.php
|
||||||
|
CREATE TABLE documenti (
|
||||||
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
stabile_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
amministratore_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
categoria_documento ENUM('contratto', 'fattura', 'verbale', 'corrispondenza', 'tecnico', 'legale', 'assicurazione', 'altro') NOT NULL,
|
||||||
|
tipo_documento VARCHAR(100) NOT NULL,
|
||||||
|
titolo VARCHAR(255) NOT NULL,
|
||||||
|
descrizione TEXT,
|
||||||
|
nome_file VARCHAR(255) NOT NULL,
|
||||||
|
nome_file_originale VARCHAR(255) NOT NULL,
|
||||||
|
path_relativo VARCHAR(500) NOT NULL,
|
||||||
|
path_assoluto VARCHAR(1000) NOT NULL,
|
||||||
|
dimensione_file BIGINT UNSIGNED NOT NULL,
|
||||||
|
mime_type VARCHAR(100) NOT NULL,
|
||||||
|
hash_file VARCHAR(64) NOT NULL, -- SHA256 per integrità
|
||||||
|
stato_documento ENUM('bozza', 'attivo', 'archiviato', 'eliminato') DEFAULT 'attivo',
|
||||||
|
data_documento DATE,
|
||||||
|
data_scadenza DATE NULL,
|
||||||
|
numero_protocollo VARCHAR(50) NULL,
|
||||||
|
anno_protocollo YEAR NULL,
|
||||||
|
note_interne TEXT,
|
||||||
|
metadati_personalizzati JSON,
|
||||||
|
visibilita ENUM('privato', 'amministratore', 'condomini', 'pubblico') DEFAULT 'amministratore',
|
||||||
|
-- Audit fields
|
||||||
|
caricato_da BIGINT UNSIGNED NOT NULL,
|
||||||
|
modificato_da BIGINT UNSIGNED NULL,
|
||||||
|
verificato_da BIGINT UNSIGNED NULL,
|
||||||
|
verificato_at TIMESTAMP NULL,
|
||||||
|
-- Cloud sync
|
||||||
|
sincronizzato_cloud BOOLEAN DEFAULT FALSE,
|
||||||
|
cloud_provider ENUM('office365', 'google_drive', 'dropbox') NULL,
|
||||||
|
cloud_file_id VARCHAR(255) NULL,
|
||||||
|
cloud_ultimo_sync TIMESTAMP NULL,
|
||||||
|
-- Timestamps
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP NULL,
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
INDEX idx_stabile_categoria (stabile_id, categoria_documento),
|
||||||
|
INDEX idx_data_documento (data_documento),
|
||||||
|
INDEX idx_data_scadenza (data_scadenza),
|
||||||
|
INDEX idx_stato (stato_documento),
|
||||||
|
INDEX idx_hash (hash_file),
|
||||||
|
INDEX idx_protocollo (numero_protocollo, anno_protocollo),
|
||||||
|
|
||||||
|
-- Foreign Keys
|
||||||
|
FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (amministratore_id) REFERENCES amministratori(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (caricato_da) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (modificato_da) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (verificato_da) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **B. Tabella Collegamenti Documenti**
|
||||||
|
```sql
|
||||||
|
-- Per collegare documenti a voci di spesa, ripartizioni, etc.
|
||||||
|
CREATE TABLE collegamenti_documenti (
|
||||||
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
documento_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
entita_tipo VARCHAR(50) NOT NULL, -- 'voce_spesa', 'ripartizione_spese', 'rata', etc.
|
||||||
|
entita_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
tipo_collegamento ENUM('supporto', 'fattura', 'ricevuta', 'autorizzazione', 'altro') NOT NULL,
|
||||||
|
note VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_documento (documento_id),
|
||||||
|
INDEX idx_entita (entita_tipo, entita_id),
|
||||||
|
|
||||||
|
FOREIGN KEY (documento_id) REFERENCES documenti(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **C. Tabella Versioni Documenti**
|
||||||
|
```sql
|
||||||
|
CREATE TABLE versioni_documenti (
|
||||||
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
documento_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
numero_versione INT NOT NULL DEFAULT 1,
|
||||||
|
nome_file VARCHAR(255) NOT NULL,
|
||||||
|
path_relativo VARCHAR(500) NOT NULL,
|
||||||
|
dimensione_file BIGINT UNSIGNED NOT NULL,
|
||||||
|
hash_file VARCHAR(64) NOT NULL,
|
||||||
|
modifiche_descrizione TEXT,
|
||||||
|
creato_da BIGINT UNSIGNED NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_documento_versione (documento_id, numero_versione),
|
||||||
|
|
||||||
|
FOREIGN KEY (documento_id) REFERENCES documenti(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (creato_da) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂️ **STRUTTURA CARTELLE FISICHE**
|
||||||
|
|
||||||
|
### **1. Organizzazione Filesystem**
|
||||||
|
```
|
||||||
|
storage/app/
|
||||||
|
├── documenti/
|
||||||
|
│ ├── amministratore_{id}/
|
||||||
|
│ │ ├── stabile_{id}/
|
||||||
|
│ │ │ ├── {anno}/
|
||||||
|
│ │ │ │ ├── contratti/
|
||||||
|
│ │ │ │ ├── fatture/
|
||||||
|
│ │ │ │ │ ├── {mese}/
|
||||||
|
│ │ │ │ │ │ ├── fornitori/
|
||||||
|
│ │ │ │ │ │ └── utenze/
|
||||||
|
│ │ │ │ ├── verbali/
|
||||||
|
│ │ │ │ │ ├── assemblee/
|
||||||
|
│ │ │ │ │ └── consiglio/
|
||||||
|
│ │ │ │ ├── corrispondenza/
|
||||||
|
│ │ │ │ │ ├── ingresso/
|
||||||
|
│ │ │ │ │ └── uscita/
|
||||||
|
│ │ │ │ ├── tecnici/
|
||||||
|
│ │ │ │ │ ├── progetti/
|
||||||
|
│ │ │ │ │ ├── certificati/
|
||||||
|
│ │ │ │ │ └── collaudi/
|
||||||
|
│ │ │ │ ├── legali/
|
||||||
|
│ │ │ │ ├── assicurazioni/
|
||||||
|
│ │ │ │ └── altro/
|
||||||
|
│ │ │ └── versioni/
|
||||||
|
│ │ │ └── {documento_id}/
|
||||||
|
│ │ └── backup/
|
||||||
|
│ │ └── {data}/
|
||||||
|
│ └── templates/
|
||||||
|
│ ├── contratti/
|
||||||
|
│ ├── lettere/
|
||||||
|
│ └── verbali/
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Naming Convention**
|
||||||
|
```
|
||||||
|
Formato: {YYYY}{MM}{DD}_{categoria}_{progressivo}_{descrizione_breve}.{ext}
|
||||||
|
Esempio: 20250127_fattura_001_enel_energia_elettrica.pdf
|
||||||
|
20250127_verbale_001_assemblea_ordinaria.pdf
|
||||||
|
20250127_contratto_001_pulizie_ditta_abc.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 **MODELLO ELOQUENT E SERVIZI**
|
||||||
|
|
||||||
|
### **1. Modello Documento**
|
||||||
|
```php
|
||||||
|
// app/Models/Documento.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class Documento extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'documenti';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'stabile_id', 'amministratore_id', 'categoria_documento',
|
||||||
|
'tipo_documento', 'titolo', 'descrizione', 'nome_file',
|
||||||
|
'nome_file_originale', 'path_relativo', 'dimensione_file',
|
||||||
|
'mime_type', 'hash_file', 'stato_documento', 'data_documento',
|
||||||
|
'data_scadenza', 'numero_protocollo', 'anno_protocollo',
|
||||||
|
'note_interne', 'metadati_personalizzati', 'visibilita',
|
||||||
|
'caricato_da'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'data_documento' => 'date',
|
||||||
|
'data_scadenza' => 'date',
|
||||||
|
'metadati_personalizzati' => 'array',
|
||||||
|
'sincronizzato_cloud' => 'boolean',
|
||||||
|
'verificato_at' => 'datetime',
|
||||||
|
'cloud_ultimo_sync' => 'datetime'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Relazioni
|
||||||
|
public function stabile()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Stabile::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function amministratore()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Amministratore::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function caricatoDa()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'caricato_da');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function modificatoDa()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'modificato_da');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verificatoDa()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'verificato_da');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function versioni()
|
||||||
|
{
|
||||||
|
return $this->hasMany(VersioneDocumento::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collegamenti()
|
||||||
|
{
|
||||||
|
return $this->hasMany(CollegamentoDocumento::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scopes
|
||||||
|
public function scopePerStabile($query, $stabileId)
|
||||||
|
{
|
||||||
|
return $query->where('stabile_id', $stabileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopePerCategoria($query, $categoria)
|
||||||
|
{
|
||||||
|
return $query->where('categoria_documento', $categoria);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeInScadenza($query, $giorni = 30)
|
||||||
|
{
|
||||||
|
return $query->whereNotNull('data_scadenza')
|
||||||
|
->whereBetween('data_scadenza', [
|
||||||
|
now()->toDateString(),
|
||||||
|
now()->addDays($giorni)->toDateString()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeAttivi($query)
|
||||||
|
{
|
||||||
|
return $query->where('stato_documento', 'attivo');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metodi utilità
|
||||||
|
public function getPathCompletoAttribute()
|
||||||
|
{
|
||||||
|
return Storage::path($this->path_relativo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUrlDownloadAttribute()
|
||||||
|
{
|
||||||
|
return route('documenti.download', $this->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDimensioneFormattataAttribute()
|
||||||
|
{
|
||||||
|
return $this->formatBytes($this->dimensione_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatBytes($size)
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$i = 0;
|
||||||
|
while ($size >= 1024 && $i < count($units) - 1) {
|
||||||
|
$size /= 1024;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
return round($size, 2) . ' ' . $units[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verificaIntegrita()
|
||||||
|
{
|
||||||
|
if (!Storage::exists($this->path_relativo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashCorrente = hash_file('sha256', Storage::path($this->path_relativo));
|
||||||
|
return $hashCorrente === $this->hash_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function creaVersione($motivo = null)
|
||||||
|
{
|
||||||
|
return VersioneDocumento::create([
|
||||||
|
'documento_id' => $this->id,
|
||||||
|
'numero_versione' => $this->versioni()->max('numero_versione') + 1,
|
||||||
|
'nome_file' => $this->nome_file,
|
||||||
|
'path_relativo' => $this->path_relativo,
|
||||||
|
'dimensione_file' => $this->dimensione_file,
|
||||||
|
'hash_file' => $this->hash_file,
|
||||||
|
'modifiche_descrizione' => $motivo,
|
||||||
|
'creato_da' => auth()->id()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Categorie statiche
|
||||||
|
public static function getCategorie()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'contratto' => 'Contratti',
|
||||||
|
'fattura' => 'Fatture',
|
||||||
|
'verbale' => 'Verbali',
|
||||||
|
'corrispondenza' => 'Corrispondenza',
|
||||||
|
'tecnico' => 'Documenti Tecnici',
|
||||||
|
'legale' => 'Documenti Legali',
|
||||||
|
'assicurazione' => 'Assicurazioni',
|
||||||
|
'altro' => 'Altro'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getTipiVisibilita()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'privato' => 'Solo Amministratore',
|
||||||
|
'amministratore' => 'Staff Amministrazione',
|
||||||
|
'condomini' => 'Condomini',
|
||||||
|
'pubblico' => 'Pubblico'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Servizio Gestione Documenti**
|
||||||
|
```php
|
||||||
|
// app/Services/DocumentoService.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Documento;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class DocumentoService
|
||||||
|
{
|
||||||
|
public function caricaDocumento(UploadedFile $file, array $dati)
|
||||||
|
{
|
||||||
|
// Validazione file
|
||||||
|
$this->validaFile($file);
|
||||||
|
|
||||||
|
// Genera path di destinazione
|
||||||
|
$pathRelativo = $this->generaPath($dati);
|
||||||
|
|
||||||
|
// Calcola hash per integrità
|
||||||
|
$hashFile = hash_file('sha256', $file->getPathname());
|
||||||
|
|
||||||
|
// Verifica duplicati
|
||||||
|
if ($this->verificaDuplicato($hashFile, $dati['stabile_id'])) {
|
||||||
|
throw new \Exception('Il documento è già presente nell\'archivio');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Genera nome file univoco
|
||||||
|
$nomeFile = $this->generaNomeFile($file, $dati);
|
||||||
|
$pathCompleto = $pathRelativo . '/' . $nomeFile;
|
||||||
|
|
||||||
|
// Salva il file
|
||||||
|
$file->storeAs(dirname($pathCompleto), basename($pathCompleto));
|
||||||
|
|
||||||
|
// Crea record in database
|
||||||
|
return Documento::create([
|
||||||
|
'stabile_id' => $dati['stabile_id'],
|
||||||
|
'amministratore_id' => $dati['amministratore_id'],
|
||||||
|
'categoria_documento' => $dati['categoria_documento'],
|
||||||
|
'tipo_documento' => $dati['tipo_documento'],
|
||||||
|
'titolo' => $dati['titolo'],
|
||||||
|
'descrizione' => $dati['descrizione'] ?? null,
|
||||||
|
'nome_file' => $nomeFile,
|
||||||
|
'nome_file_originale' => $file->getClientOriginalName(),
|
||||||
|
'path_relativo' => $pathCompleto,
|
||||||
|
'path_assoluto' => Storage::path($pathCompleto),
|
||||||
|
'dimensione_file' => $file->getSize(),
|
||||||
|
'mime_type' => $file->getMimeType(),
|
||||||
|
'hash_file' => $hashFile,
|
||||||
|
'data_documento' => $dati['data_documento'] ?? now()->toDateString(),
|
||||||
|
'data_scadenza' => $dati['data_scadenza'] ?? null,
|
||||||
|
'numero_protocollo' => $dati['numero_protocollo'] ?? null,
|
||||||
|
'anno_protocollo' => $dati['anno_protocollo'] ?? now()->year,
|
||||||
|
'note_interne' => $dati['note_interne'] ?? null,
|
||||||
|
'metadati_personalizzati' => $dati['metadati'] ?? [],
|
||||||
|
'visibilita' => $dati['visibilita'] ?? 'amministratore',
|
||||||
|
'caricato_da' => auth()->id()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generaPath(array $dati)
|
||||||
|
{
|
||||||
|
$anno = $dati['anno'] ?? now()->year;
|
||||||
|
$mese = $dati['mese'] ?? now()->format('m');
|
||||||
|
|
||||||
|
return "documenti/amministratore_{$dati['amministratore_id']}/stabile_{$dati['stabile_id']}/{$anno}/{$dati['categoria_documento']}" .
|
||||||
|
($dati['categoria_documento'] === 'fattura' ? "/{$mese}" : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generaNomeFile(UploadedFile $file, array $dati)
|
||||||
|
{
|
||||||
|
$data = now()->format('Ymd');
|
||||||
|
$categoria = $dati['categoria_documento'];
|
||||||
|
$progressivo = $this->getProgressivo($dati['stabile_id'], $categoria);
|
||||||
|
$descrizione = Str::slug($dati['descrizione_breve'] ?? 'documento');
|
||||||
|
$estensione = $file->getClientOriginalExtension();
|
||||||
|
|
||||||
|
return "{$data}_{$categoria}_{$progressivo}_{$descrizione}.{$estensione}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getProgressivo($stabileId, $categoria)
|
||||||
|
{
|
||||||
|
$ultimo = Documento::where('stabile_id', $stabileId)
|
||||||
|
->where('categoria_documento', $categoria)
|
||||||
|
->whereDate('created_at', now()->toDateString())
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return str_pad($ultimo + 1, 3, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validaFile(UploadedFile $file)
|
||||||
|
{
|
||||||
|
$tipiConsentiti = [
|
||||||
|
'application/pdf',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!in_array($file->getMimeType(), $tipiConsentiti)) {
|
||||||
|
throw new \Exception('Tipo di file non consentito');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($file->getSize() > 50 * 1024 * 1024) { // 50MB
|
||||||
|
throw new \Exception('File troppo grande (max 50MB)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function verificaDuplicato($hash, $stabileId)
|
||||||
|
{
|
||||||
|
return Documento::where('hash_file', $hash)
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where('stato_documento', 'attivo')
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ☁️ **INTEGRAZIONE CLOUD STORAGE**
|
||||||
|
|
||||||
|
### **1. Servizio Office 365**
|
||||||
|
```php
|
||||||
|
// app/Services/Office365Service.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Microsoft\Graph\Graph;
|
||||||
|
use Microsoft\Graph\Model;
|
||||||
|
|
||||||
|
class Office365Service
|
||||||
|
{
|
||||||
|
private $graph;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->graph = new Graph();
|
||||||
|
$this->graph->setAccessToken($this->getAccessToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sincronizzaDocumento(Documento $documento)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Crea cartella se non esiste
|
||||||
|
$cartellaPadre = $this->creaCatellaStabile($documento->stabile);
|
||||||
|
|
||||||
|
// Upload del file
|
||||||
|
$fileContent = Storage::get($documento->path_relativo);
|
||||||
|
$uploadedFile = $this->graph->createRequest('PUT',
|
||||||
|
"/me/drive/items/{$cartellaPadre}/children/{$documento->nome_file}/content")
|
||||||
|
->attachBody($fileContent)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
// Aggiorna documento con info cloud
|
||||||
|
$documento->update([
|
||||||
|
'sincronizzato_cloud' => true,
|
||||||
|
'cloud_provider' => 'office365',
|
||||||
|
'cloud_file_id' => $uploadedFile->getId(),
|
||||||
|
'cloud_ultimo_sync' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Log::error('Errore sincronizzazione Office 365: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function creaCatellaStabile($stabile)
|
||||||
|
{
|
||||||
|
$nomeCartella = "Stabile_{$stabile->codice}_{$stabile->denominazione}";
|
||||||
|
|
||||||
|
// Verifica se esiste
|
||||||
|
$folders = $this->graph->createRequest('GET',
|
||||||
|
"/me/drive/root/children?filter=name eq '{$nomeCartella}'")
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
if ($folders->getBody()['value']) {
|
||||||
|
return $folders->getBody()['value'][0]['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea nuova cartella
|
||||||
|
$folderData = [
|
||||||
|
'name' => $nomeCartella,
|
||||||
|
'folder' => new \stdClass()
|
||||||
|
];
|
||||||
|
|
||||||
|
$newFolder = $this->graph->createRequest('POST', '/me/drive/root/children')
|
||||||
|
->attachBody($folderData)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
return $newFolder->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAccessToken()
|
||||||
|
{
|
||||||
|
// Implementa OAuth2 flow per Office 365
|
||||||
|
// Restituisce access token valido
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Servizio Google Drive**
|
||||||
|
```php
|
||||||
|
// app/Services/GoogleDriveService.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Google_Client;
|
||||||
|
use Google_Service_Drive;
|
||||||
|
use Google_Service_Drive_DriveFile;
|
||||||
|
|
||||||
|
class GoogleDriveService
|
||||||
|
{
|
||||||
|
private $client;
|
||||||
|
private $service;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->client = new Google_Client();
|
||||||
|
$this->client->setClientId(config('services.google.client_id'));
|
||||||
|
$this->client->setClientSecret(config('services.google.client_secret'));
|
||||||
|
$this->client->setRedirectUri(config('services.google.redirect_uri'));
|
||||||
|
$this->client->addScope(Google_Service_Drive::DRIVE);
|
||||||
|
|
||||||
|
$this->service = new Google_Service_Drive($this->client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sincronizzaDocumento(Documento $documento)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Crea cartella se non esiste
|
||||||
|
$cartellaPadre = $this->creaCatellaStabile($documento->stabile);
|
||||||
|
|
||||||
|
// Prepara file per upload
|
||||||
|
$fileMetadata = new Google_Service_Drive_DriveFile([
|
||||||
|
'name' => $documento->nome_file,
|
||||||
|
'parents' => [$cartellaPadre]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$content = Storage::get($documento->path_relativo);
|
||||||
|
|
||||||
|
$file = $this->service->files->create($fileMetadata, [
|
||||||
|
'data' => $content,
|
||||||
|
'mimeType' => $documento->mime_type,
|
||||||
|
'uploadType' => 'multipart'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Aggiorna documento
|
||||||
|
$documento->update([
|
||||||
|
'sincronizzato_cloud' => true,
|
||||||
|
'cloud_provider' => 'google_drive',
|
||||||
|
'cloud_file_id' => $file->id,
|
||||||
|
'cloud_ultimo_sync' => now()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Log::error('Errore sincronizzazione Google Drive: ' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function creaCatellaStabile($stabile)
|
||||||
|
{
|
||||||
|
$nomeCartella = "Stabile_{$stabile->codice}_{$stabile->denominazione}";
|
||||||
|
|
||||||
|
// Cerca cartella esistente
|
||||||
|
$response = $this->service->files->listFiles([
|
||||||
|
'q' => "name='{$nomeCartella}' and mimeType='application/vnd.google-apps.folder'",
|
||||||
|
'spaces' => 'drive'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (count($response->files) > 0) {
|
||||||
|
return $response->files[0]->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea nuova cartella
|
||||||
|
$fileMetadata = new Google_Service_Drive_DriveFile([
|
||||||
|
'name' => $nomeCartella,
|
||||||
|
'mimeType' => 'application/vnd.google-apps.folder'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$folder = $this->service->files->create($fileMetadata);
|
||||||
|
return $folder->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 **SISTEMA AUDIT DOCUMENTALE**
|
||||||
|
|
||||||
|
### **1. Modello Audit Log**
|
||||||
|
```php
|
||||||
|
// app/Models/AuditDocumento.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class AuditDocumento extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'audit_documenti';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'documento_id', 'utente_id', 'azione', 'dettagli',
|
||||||
|
'ip_address', 'user_agent', 'dati_precedenti', 'dati_nuovi'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'dettagli' => 'array',
|
||||||
|
'dati_precedenti' => 'array',
|
||||||
|
'dati_nuovi' => 'array'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function documento()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Documento::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function utente()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function logAzione($documento, $azione, $dettagli = [])
|
||||||
|
{
|
||||||
|
return self::create([
|
||||||
|
'documento_id' => $documento->id,
|
||||||
|
'utente_id' => auth()->id(),
|
||||||
|
'azione' => $azione,
|
||||||
|
'dettagli' => $dettagli,
|
||||||
|
'ip_address' => request()->ip(),
|
||||||
|
'user_agent' => request()->userAgent(),
|
||||||
|
'dati_precedenti' => $documento->getOriginal(),
|
||||||
|
'dati_nuovi' => $documento->getAttributes()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Observer per Audit Automatico**
|
||||||
|
```php
|
||||||
|
// app/Observers/DocumentoObserver.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\Documento;
|
||||||
|
use App\Models\AuditDocumento;
|
||||||
|
|
||||||
|
class DocumentoObserver
|
||||||
|
{
|
||||||
|
public function created(Documento $documento)
|
||||||
|
{
|
||||||
|
AuditDocumento::logAzione($documento, 'created', [
|
||||||
|
'messaggio' => 'Documento caricato'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updated(Documento $documento)
|
||||||
|
{
|
||||||
|
$modifiche = [];
|
||||||
|
foreach ($documento->getDirty() as $campo => $nuovoValore) {
|
||||||
|
$modifiche[$campo] = [
|
||||||
|
'da' => $documento->getOriginal($campo),
|
||||||
|
'a' => $nuovoValore
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditDocumento::logAzione($documento, 'updated', [
|
||||||
|
'messaggio' => 'Documento modificato',
|
||||||
|
'modifiche' => $modifiche
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleted(Documento $documento)
|
||||||
|
{
|
||||||
|
AuditDocumento::logAzione($documento, 'deleted', [
|
||||||
|
'messaggio' => 'Documento eliminato'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 **CHECKLIST IMPLEMENTAZIONE**
|
||||||
|
|
||||||
|
### **Database e Modelli**
|
||||||
|
- [ ] Migration tabelle documenti
|
||||||
|
- [ ] Migration collegamenti documenti
|
||||||
|
- [ ] Migration versioni documenti
|
||||||
|
- [ ] Migration audit documenti
|
||||||
|
- [ ] Modelli Eloquent completi
|
||||||
|
- [ ] Observer per audit automatico
|
||||||
|
|
||||||
|
### **Servizi Core**
|
||||||
|
- [ ] DocumentoService per gestione base
|
||||||
|
- [ ] Office365Service per integrazione cloud
|
||||||
|
- [ ] GoogleDriveService per integrazione cloud
|
||||||
|
- [ ] AuditService per tracciamento completo
|
||||||
|
|
||||||
|
### **Controller e API**
|
||||||
|
- [ ] DocumentoController per CRUD
|
||||||
|
- [ ] API per upload tramite drag&drop
|
||||||
|
- [ ] API per sincronizzazione cloud
|
||||||
|
- [ ] API per ricerca avanzata
|
||||||
|
|
||||||
|
### **Frontend**
|
||||||
|
- [ ] Interfaccia upload documenti
|
||||||
|
- [ ] Visualizzatore documenti integrato
|
||||||
|
- [ ] Sistema ricerca e filtri
|
||||||
|
- [ ] Dashboard audit e statistiche
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Aggiornato**: 2025-01-27
|
||||||
|
**Versione**: 1.0
|
||||||
|
**Prossimo step**: Implementazione migration e modelli base
|
||||||
493
LARAVEL_FORMS_DOCUMENTATION.md
Normal file
493
LARAVEL_FORMS_DOCUMENTATION.md
Normal file
|
|
@ -0,0 +1,493 @@
|
||||||
|
# 📋 **NetGesCon Laravel - Documentazione Maschere e Interfacce**
|
||||||
|
|
||||||
|
## 📍 **STATO DEL PROGETTO**
|
||||||
|
- **Fase**: Business Logic Core completata
|
||||||
|
- **Prossima fase**: Sviluppo interfacce utente responsive
|
||||||
|
- **Livello**: Pronto per sviluppo controller e viste
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **STRUTTURA VERIFICATA - COLLEGAMENTI FUNZIONALI**
|
||||||
|
|
||||||
|
### ✅ **Voci di Spesa → Tabelle Millesimali**
|
||||||
|
Le voci di spesa sono correttamente collegate al sistema millesimale:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// VoceSpesa.php - Relazione con tabella millesimale
|
||||||
|
public function tabellaMillesimaleDefault()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(TabellaMillesimale::class, 'tabella_millesimale_default_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collegamento alla ripartizione spese
|
||||||
|
public function ripartizioniSpese()
|
||||||
|
{
|
||||||
|
return $this->hasMany(RipartizioneSpese::class);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ **Workflow Completo di Ripartizione**
|
||||||
|
1. **Voce Spesa** → definisce la spesa e tabella millesimale di default
|
||||||
|
2. **Ripartizione Spese** → calcola la ripartizione per ogni unità
|
||||||
|
3. **Dettaglio Ripartizione** → importo specifico per ogni unità
|
||||||
|
4. **Piano Rateizzazione** → gestione pagamenti dilazionati
|
||||||
|
5. **Rate** → singole scadenze di pagamento
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 **STANDARDIZZAZIONE SVILUPPO UI**
|
||||||
|
|
||||||
|
### **1. Struttura Base per Maschere Laravel**
|
||||||
|
|
||||||
|
#### **A. Layout Base Responsive**
|
||||||
|
```php
|
||||||
|
// resources/views/layouts/app.blade.php
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>@yield('title', 'NetGesCon')</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap 5 + Custom CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="{{ asset('css/netgescon.css') }}" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{{ route('dashboard') }}">
|
||||||
|
<i class="fas fa-building"></i> NetGesCon
|
||||||
|
</a>
|
||||||
|
<!-- Navigation menu -->
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-2">
|
||||||
|
@include('partials.sidebar')
|
||||||
|
</div>
|
||||||
|
<div class="col-md-10">
|
||||||
|
@yield('content')
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="{{ asset('js/netgescon.js') }}"></script>
|
||||||
|
@yield('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **B. Componenti Standard per Form**
|
||||||
|
```php
|
||||||
|
// resources/views/components/form-input.blade.php
|
||||||
|
@props(['name', 'label', 'type' => 'text', 'required' => false, 'value' => ''])
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ $name }}" class="form-label">
|
||||||
|
{{ $label }}
|
||||||
|
@if($required)
|
||||||
|
<span class="text-danger">*</span>
|
||||||
|
@endif
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="{{ $type }}"
|
||||||
|
class="form-control @error($name) is-invalid @enderror"
|
||||||
|
id="{{ $name }}"
|
||||||
|
name="{{ $name }}"
|
||||||
|
value="{{ old($name, $value) }}"
|
||||||
|
@if($required) required @endif
|
||||||
|
>
|
||||||
|
@error($name)
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **C. Componente Select per Relazioni**
|
||||||
|
```php
|
||||||
|
// resources/views/components/form-select.blade.php
|
||||||
|
@props(['name', 'label', 'options' => [], 'selected' => '', 'required' => false])
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ $name }}" class="form-label">
|
||||||
|
{{ $label }}
|
||||||
|
@if($required)
|
||||||
|
<span class="text-danger">*</span>
|
||||||
|
@endif
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="form-select @error($name) is-invalid @enderror"
|
||||||
|
id="{{ $name }}"
|
||||||
|
name="{{ $name }}"
|
||||||
|
@if($required) required @endif
|
||||||
|
>
|
||||||
|
<option value="">-- Seleziona --</option>
|
||||||
|
@foreach($options as $key => $value)
|
||||||
|
<option value="{{ $key }}" @if(old($name, $selected) == $key) selected @endif>
|
||||||
|
{{ $value }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error($name)
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ **ARCHITETTURA CONTROLLER E VISTE**
|
||||||
|
|
||||||
|
### **1. Controller Standard Pattern**
|
||||||
|
|
||||||
|
#### **A. Controller Base per Gestione CRUD**
|
||||||
|
```php
|
||||||
|
// app/Http/Controllers/BaseController.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
abstract class BaseController extends Controller
|
||||||
|
{
|
||||||
|
protected $model;
|
||||||
|
protected $viewPath;
|
||||||
|
protected $routePrefix;
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = $this->model::query();
|
||||||
|
|
||||||
|
// Filtri standard
|
||||||
|
if ($request->has('search')) {
|
||||||
|
$query = $this->applySearch($query, $request->search);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('stabile_id')) {
|
||||||
|
$query->where('stabile_id', $request->stabile_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $query->paginate(20);
|
||||||
|
|
||||||
|
return view("{$this->viewPath}.index", compact('items'));
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function applySearch($query, $search);
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view("{$this->viewPath}.create", $this->getFormData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$this->validateRequest($request);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
$item = $this->model::create($request->validated());
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route("{$this->routePrefix}.show", $item)
|
||||||
|
->with('success', 'Elemento creato con successo');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return back()->with('error', 'Errore durante la creazione: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function validateRequest(Request $request);
|
||||||
|
abstract protected function getFormData();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **B. Controller Specifico per Voci di Spesa**
|
||||||
|
```php
|
||||||
|
// app/Http/Controllers/VoceSpesaController.php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\VoceSpesa;
|
||||||
|
use App\Models\Stabile;
|
||||||
|
use App\Models\TabellaMillesimale;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class VoceSpesaController extends BaseController
|
||||||
|
{
|
||||||
|
protected $model = VoceSpesa::class;
|
||||||
|
protected $viewPath = 'voci_spesa';
|
||||||
|
protected $routePrefix = 'voci-spesa';
|
||||||
|
|
||||||
|
protected function applySearch($query, $search)
|
||||||
|
{
|
||||||
|
return $query->where(function($q) use ($search) {
|
||||||
|
$q->where('codice', 'LIKE', "%{$search}%")
|
||||||
|
->orWhere('descrizione', 'LIKE', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateRequest(Request $request)
|
||||||
|
{
|
||||||
|
return $request->validate([
|
||||||
|
'stabile_id' => 'required|exists:stabili,id',
|
||||||
|
'descrizione' => 'required|string|max:255',
|
||||||
|
'tipo_gestione' => 'required|in:ordinaria,straordinaria,speciale',
|
||||||
|
'categoria' => 'required|string',
|
||||||
|
'tabella_millesimale_default_id' => 'nullable|exists:tabelle_millesimali,id',
|
||||||
|
'ritenuta_acconto_default' => 'nullable|numeric|between:0,100',
|
||||||
|
'attiva' => 'boolean',
|
||||||
|
'ordinamento' => 'nullable|integer'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getFormData()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'stabili' => Stabile::pluck('denominazione', 'id'),
|
||||||
|
'tabelleMillesimali' => TabellaMillesimale::pluck('denominazione', 'id'),
|
||||||
|
'categorie' => VoceSpesa::getCategorieStandard(),
|
||||||
|
'tipiGestione' => VoceSpesa::getTipiGestione()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **MASCHERE PRINCIPALI DA IMPLEMENTARE**
|
||||||
|
|
||||||
|
### **1. Gestione Voci di Spesa**
|
||||||
|
- **Lista**: Filtri per stabile, categoria, tipo gestione
|
||||||
|
- **Dettaglio**: Visualizzazione completa con storico ripartizioni
|
||||||
|
- **Creazione/Modifica**: Form con validazione e selezione tabella millesimale
|
||||||
|
|
||||||
|
### **2. Gestione Ripartizione Spese**
|
||||||
|
- **Wizard di Ripartizione**: Step-by-step per calcolo automatico
|
||||||
|
- **Anteprima Ripartizione**: Visualizzazione prima della conferma
|
||||||
|
- **Gestione Esenzioni**: Interfaccia per escludere unità specifiche
|
||||||
|
|
||||||
|
### **3. Gestione Rate**
|
||||||
|
- **Piano Rateizzazione**: Creazione piani di pagamento
|
||||||
|
- **Calendario Scadenze**: Vista calendario con rate in scadenza
|
||||||
|
- **Gestione Pagamenti**: Registrazione e storico pagamenti
|
||||||
|
|
||||||
|
### **4. Dashboard Amministratore**
|
||||||
|
- **Riepilogo Finanziario**: Grafici e statistiche
|
||||||
|
- **Scadenze Imminenti**: Alert per rate in scadenza
|
||||||
|
- **Stato Ripartizioni**: Monitoraggio ripartizioni aperte
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 **STRUMENTI E INTEGRAZIONI**
|
||||||
|
|
||||||
|
### **1. Validazione Client-Side**
|
||||||
|
```javascript
|
||||||
|
// public/js/netgescon.js
|
||||||
|
class NetgesconValidator {
|
||||||
|
static validateRipartizione(form) {
|
||||||
|
const importo = parseFloat(form.importo_totale.value);
|
||||||
|
const tabellaId = form.tabella_millesimale_id.value;
|
||||||
|
|
||||||
|
if (importo <= 0) {
|
||||||
|
this.showError('L\'importo deve essere maggiore di zero');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tabellaId) {
|
||||||
|
this.showError('Seleziona una tabella millesimale');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static showError(message) {
|
||||||
|
// Implementazione notifica errore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. API JSON per Interazioni Ajax**
|
||||||
|
```php
|
||||||
|
// app/Http/Controllers/Api/RipartizioneSpesaController.php
|
||||||
|
public function calcolaAnteprima(Request $request)
|
||||||
|
{
|
||||||
|
$voceSpesa = VoceSpesa::find($request->voce_spesa_id);
|
||||||
|
$importoTotale = $request->importo_totale;
|
||||||
|
|
||||||
|
$ripartizione = new RipartizioneSpese();
|
||||||
|
$anteprima = $ripartizione->calcolaAnteprimaRipartizione(
|
||||||
|
$voceSpesa,
|
||||||
|
$importoTotale,
|
||||||
|
$request->tabella_millesimale_id
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'anteprima' => $anteprima,
|
||||||
|
'totale_calcolato' => $anteprima->sum('importo_calcolato')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 **TEMI CSS PERSONALIZZATI**
|
||||||
|
|
||||||
|
### **1. Variabili CSS NetGesCon**
|
||||||
|
```css
|
||||||
|
/* public/css/netgescon.css */
|
||||||
|
:root {
|
||||||
|
--primary-color: #2c3e50;
|
||||||
|
--secondary-color: #3498db;
|
||||||
|
--success-color: #27ae60;
|
||||||
|
--warning-color: #f39c12;
|
||||||
|
--danger-color: #e74c3c;
|
||||||
|
--light-bg: #f8f9fa;
|
||||||
|
--dark-text: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-netgescon {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-netgescon {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-netgescon {
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-netgescon th {
|
||||||
|
background-color: var(--light-bg);
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 **PLUGIN SYSTEM ARCHITECTURE**
|
||||||
|
|
||||||
|
### **1. Struttura Base per Plugin**
|
||||||
|
```php
|
||||||
|
// app/Plugins/PluginInterface.php
|
||||||
|
interface PluginInterface
|
||||||
|
{
|
||||||
|
public function register(): void;
|
||||||
|
public function boot(): void;
|
||||||
|
public function getMenuItems(): array;
|
||||||
|
public function getRoutes(): array;
|
||||||
|
public function getViews(): array;
|
||||||
|
}
|
||||||
|
|
||||||
|
// app/Plugins/BasePlugin.php
|
||||||
|
abstract class BasePlugin implements PluginInterface
|
||||||
|
{
|
||||||
|
protected $name;
|
||||||
|
protected $version;
|
||||||
|
protected $description;
|
||||||
|
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
// Registrazione servizi base
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract public function boot(): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **2. Plugin Manager**
|
||||||
|
```php
|
||||||
|
// app/Services/PluginManager.php
|
||||||
|
class PluginManager
|
||||||
|
{
|
||||||
|
protected $plugins = [];
|
||||||
|
|
||||||
|
public function loadPlugin($pluginClass)
|
||||||
|
{
|
||||||
|
$plugin = new $pluginClass();
|
||||||
|
$plugin->register();
|
||||||
|
$plugin->boot();
|
||||||
|
|
||||||
|
$this->plugins[] = $plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMenuItems()
|
||||||
|
{
|
||||||
|
$items = [];
|
||||||
|
foreach ($this->plugins as $plugin) {
|
||||||
|
$items = array_merge($items, $plugin->getMenuItems());
|
||||||
|
}
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 **PROSSIMI STEP IMPLEMENTATIVI**
|
||||||
|
|
||||||
|
### **Fase 1: Implementazione Controller e Viste Base**
|
||||||
|
1. Creazione controller per VoceSpesa, RipartizioneSpese, Rate
|
||||||
|
2. Implementazione viste index, create, edit, show
|
||||||
|
3. Configurazione rotte e middleware
|
||||||
|
|
||||||
|
### **Fase 2: Interfacce Avanzate**
|
||||||
|
1. Wizard ripartizione spese con preview
|
||||||
|
2. Dashboard amministratore con grafici
|
||||||
|
3. Calendario scadenze interattivo
|
||||||
|
|
||||||
|
### **Fase 3: Integrazioni**
|
||||||
|
1. Sistema notifiche (email, SMS)
|
||||||
|
2. Export PDF/Excel
|
||||||
|
3. API REST per app mobile
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **CHECKLIST SVILUPPO**
|
||||||
|
|
||||||
|
### **Backend**
|
||||||
|
- [x] Modelli Eloquent completi
|
||||||
|
- [x] Relazioni verificate
|
||||||
|
- [x] Migration funzionali
|
||||||
|
- [ ] Controller CRUD
|
||||||
|
- [ ] Validazione form
|
||||||
|
- [ ] API endpoints
|
||||||
|
|
||||||
|
### **Frontend**
|
||||||
|
- [ ] Layout responsive
|
||||||
|
- [ ] Componenti riutilizzabili
|
||||||
|
- [ ] Validazione client-side
|
||||||
|
- [ ] Interfacce wizard
|
||||||
|
- [ ] Dashboard grafici
|
||||||
|
|
||||||
|
### **Integrazioni**
|
||||||
|
- [ ] Sistema notifiche
|
||||||
|
- [ ] Export documenti
|
||||||
|
- [ ] Plugin system
|
||||||
|
- [ ] Mobile API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Aggiornato**: 2025-01-27
|
||||||
|
**Versione**: 1.0
|
||||||
|
**Prossimo update**: Dopo implementazione primi controller
|
||||||
452
app/Http/Controllers/Admin/PianoRateizzazioneController.php
Normal file
452
app/Http/Controllers/Admin/PianoRateizzazioneController.php
Normal file
|
|
@ -0,0 +1,452 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
463
app/Http/Controllers/Admin/RataController.php
Normal file
463
app/Http/Controllers/Admin/RataController.php
Normal file
|
|
@ -0,0 +1,463 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Rata;
|
||||||
|
use App\Models\PianoRateizzazione;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class RataController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = Rata::with(['pianoRateizzazione.ripartizioneSpese.voceSpesa.stabile', 'pianoRateizzazione.unitaImmobiliare'])
|
||||||
|
->whereHas('pianoRateizzazione.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('pianoRateizzazione.ripartizioneSpese.voceSpesa', function($q) use ($request) {
|
||||||
|
$q->where('stabile_id', $request->stabile_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro per unità immobiliare
|
||||||
|
if ($request->filled('unita_immobiliare_id')) {
|
||||||
|
$query->whereHas('pianoRateizzazione', function($q) use ($request) {
|
||||||
|
$q->where('unita_immobiliare_id', $request->unita_immobiliare_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro per stato
|
||||||
|
if ($request->filled('stato')) {
|
||||||
|
$query->where('stato', $request->stato);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro per scadenza
|
||||||
|
if ($request->filled('scadenza_da')) {
|
||||||
|
$query->where('data_scadenza', '>=', $request->scadenza_da);
|
||||||
|
}
|
||||||
|
if ($request->filled('scadenza_a')) {
|
||||||
|
$query->where('data_scadenza', '<=', $request->scadenza_a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro per rate in scadenza
|
||||||
|
if ($request->filled('in_scadenza')) {
|
||||||
|
$giorni = (int) $request->in_scadenza;
|
||||||
|
$query->where('data_scadenza', '<=', Carbon::now()->addDays($giorni))
|
||||||
|
->where('stato', 'da_pagare');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro per rate scadute
|
||||||
|
if ($request->filled('scadute')) {
|
||||||
|
$query->where('data_scadenza', '<', Carbon::now())
|
||||||
|
->where('stato', 'da_pagare');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rate = $query->orderBy('data_scadenza')->paginate(20);
|
||||||
|
|
||||||
|
// Dati per i filtri
|
||||||
|
$stabili = \App\Models\Stabile::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// Statistiche
|
||||||
|
$statistiche = [
|
||||||
|
'totale_rate' => $query->count(),
|
||||||
|
'da_pagare' => $query->where('stato', 'da_pagare')->count(),
|
||||||
|
'pagate' => $query->where('stato', 'pagata')->count(),
|
||||||
|
'scadute' => $query->where('data_scadenza', '<', Carbon::now())->where('stato', 'da_pagare')->count(),
|
||||||
|
'importo_totale' => $query->sum('importo'),
|
||||||
|
'importo_pagato' => $query->where('stato', 'pagata')->sum('importo'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('admin.rate.index', compact('rate', 'stabili', 'statistiche'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(Rata $rata)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('view', $rata);
|
||||||
|
|
||||||
|
$rata->load([
|
||||||
|
'pianoRateizzazione.ripartizioneSpese.voceSpesa.stabile',
|
||||||
|
'pianoRateizzazione.unitaImmobiliare.anagraficaCondominiale.soggetto',
|
||||||
|
'createdBy',
|
||||||
|
'updatedBy'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('admin.rate.show', compact('rata'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(Rata $rata)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('update', $rata);
|
||||||
|
|
||||||
|
// Solo le rate da pagare possono essere modificate
|
||||||
|
if ($rata->stato !== 'da_pagare') {
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('error', 'Impossibile modificare una rata già pagata o annullata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rata->load([
|
||||||
|
'pianoRateizzazione.ripartizioneSpese.voceSpesa.stabile',
|
||||||
|
'pianoRateizzazione.unitaImmobiliare'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('admin.rate.edit', compact('rata'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Rata $rata)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('update', $rata);
|
||||||
|
|
||||||
|
// Solo le rate da pagare possono essere modificate
|
||||||
|
if ($rata->stato !== 'da_pagare') {
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('error', 'Impossibile modificare una rata già pagata o annullata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'importo' => 'required|numeric|min:0',
|
||||||
|
'data_scadenza' => 'required|date',
|
||||||
|
'note' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rata->update([
|
||||||
|
'importo' => $request->importo,
|
||||||
|
'data_scadenza' => $request->data_scadenza,
|
||||||
|
'note' => $request->note,
|
||||||
|
'updated_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('success', 'Rata aggiornata con successo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra il pagamento di una rata
|
||||||
|
*/
|
||||||
|
public function registraPagamento(Request $request, Rata $rata)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('update', $rata);
|
||||||
|
|
||||||
|
if ($rata->stato !== 'da_pagare') {
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('error', 'La rata è già stata pagata o annullata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'importo_pagato' => 'required|numeric|min:0',
|
||||||
|
'data_pagamento' => 'required|date',
|
||||||
|
'metodo_pagamento' => 'required|string|max:100',
|
||||||
|
'riferimento_pagamento' => 'nullable|string|max:255',
|
||||||
|
'note_pagamento' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rata->update([
|
||||||
|
'stato' => 'pagata',
|
||||||
|
'importo_pagato' => $request->importo_pagato,
|
||||||
|
'data_pagamento' => $request->data_pagamento,
|
||||||
|
'metodo_pagamento' => $request->metodo_pagamento,
|
||||||
|
'riferimento_pagamento' => $request->riferimento_pagamento,
|
||||||
|
'note_pagamento' => $request->note_pagamento,
|
||||||
|
'registrato_by' => Auth::id(),
|
||||||
|
'updated_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verifica se il piano è completato
|
||||||
|
$pianoRateizzazione = $rata->pianoRateizzazione;
|
||||||
|
$rateRimanenti = $pianoRateizzazione->rate()->where('stato', 'da_pagare')->count();
|
||||||
|
|
||||||
|
if ($rateRimanenti === 0) {
|
||||||
|
$pianoRateizzazione->update([
|
||||||
|
'stato' => 'completato',
|
||||||
|
'data_completamento' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('success', 'Pagamento registrato con successo.');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return redirect()->back()
|
||||||
|
->withInput()
|
||||||
|
->with('error', 'Errore durante la registrazione del pagamento: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annulla il pagamento di una rata
|
||||||
|
*/
|
||||||
|
public function annullaPagamento(Rata $rata)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('update', $rata);
|
||||||
|
|
||||||
|
if ($rata->stato !== 'pagata') {
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('error', 'La rata non è stata pagata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rata->update([
|
||||||
|
'stato' => 'da_pagare',
|
||||||
|
'importo_pagato' => null,
|
||||||
|
'data_pagamento' => null,
|
||||||
|
'metodo_pagamento' => null,
|
||||||
|
'riferimento_pagamento' => null,
|
||||||
|
'note_pagamento' => null,
|
||||||
|
'registrato_by' => null,
|
||||||
|
'updated_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Aggiorna lo stato del piano se necessario
|
||||||
|
$pianoRateizzazione = $rata->pianoRateizzazione;
|
||||||
|
if ($pianoRateizzazione->stato === 'completato') {
|
||||||
|
$pianoRateizzazione->update([
|
||||||
|
'stato' => 'attivo',
|
||||||
|
'data_completamento' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('success', 'Pagamento annullato con successo.');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('error', 'Errore durante l\'annullamento del pagamento: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posticipa una rata
|
||||||
|
*/
|
||||||
|
public function posticipa(Request $request, Rata $rata)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('update', $rata);
|
||||||
|
|
||||||
|
if ($rata->stato !== 'da_pagare') {
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('error', 'Impossibile posticipare una rata già pagata o annullata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'nuova_data_scadenza' => 'required|date|after:' . $rata->data_scadenza,
|
||||||
|
'motivo_posticipo' => 'required|string|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rata->update([
|
||||||
|
'data_scadenza' => $request->nuova_data_scadenza,
|
||||||
|
'motivo_posticipo' => $request->motivo_posticipo,
|
||||||
|
'posticipata_by' => Auth::id(),
|
||||||
|
'updated_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('success', 'Rata posticipata con successo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mostra il form per registrare un pagamento
|
||||||
|
*/
|
||||||
|
public function showPagamentoForm(Rata $rata)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('update', $rata);
|
||||||
|
|
||||||
|
if ($rata->stato !== 'da_pagare') {
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('error', 'La rata è già stata pagata o annullata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rata->load([
|
||||||
|
'pianoRateizzazione.ripartizioneSpese.voceSpesa.stabile',
|
||||||
|
'pianoRateizzazione.unitaImmobiliare.anagraficaCondominiale.soggetto'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('admin.rate.pagamento', compact('rata'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mostra il form per posticipare una rata
|
||||||
|
*/
|
||||||
|
public function showPosticipoForm(Rata $rata)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('update', $rata);
|
||||||
|
|
||||||
|
if ($rata->stato !== 'da_pagare') {
|
||||||
|
return redirect()->route('admin.rate.show', $rata)
|
||||||
|
->with('error', 'Impossibile posticipare una rata già pagata o annullata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rata->load([
|
||||||
|
'pianoRateizzazione.ripartizioneSpese.voceSpesa.stabile',
|
||||||
|
'pianoRateizzazione.unitaImmobiliare'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('admin.rate.posticipo', compact('rata'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genera un report delle rate
|
||||||
|
*/
|
||||||
|
public function report(Request $request)
|
||||||
|
{
|
||||||
|
$query = Rata::with([
|
||||||
|
'pianoRateizzazione.ripartizioneSpese.voceSpesa.stabile',
|
||||||
|
'pianoRateizzazione.unitaImmobiliare.anagraficaCondominiale.soggetto'
|
||||||
|
])->whereHas('pianoRateizzazione.ripartizioneSpese.voceSpesa.stabile', function($q) {
|
||||||
|
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Applica filtri
|
||||||
|
if ($request->filled('stabile_id')) {
|
||||||
|
$query->whereHas('pianoRateizzazione.ripartizioneSpese.voceSpesa', function($q) use ($request) {
|
||||||
|
$q->where('stabile_id', $request->stabile_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('stato')) {
|
||||||
|
$query->where('stato', $request->stato);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('data_da')) {
|
||||||
|
$query->where('data_scadenza', '>=', $request->data_da);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('data_a')) {
|
||||||
|
$query->where('data_scadenza', '<=', $request->data_a);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rate = $query->orderBy('data_scadenza')->get();
|
||||||
|
|
||||||
|
// Statistiche per il report
|
||||||
|
$statistiche = [
|
||||||
|
'totale_rate' => $rate->count(),
|
||||||
|
'da_pagare' => $rate->where('stato', 'da_pagare')->count(),
|
||||||
|
'pagate' => $rate->where('stato', 'pagata')->count(),
|
||||||
|
'scadute' => $rate->where('data_scadenza', '<', Carbon::now())->where('stato', 'da_pagare')->count(),
|
||||||
|
'importo_totale' => $rate->sum('importo'),
|
||||||
|
'importo_pagato' => $rate->where('stato', 'pagata')->sum('importo_pagato'),
|
||||||
|
'importo_da_pagare' => $rate->where('stato', 'da_pagare')->sum('importo'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('admin.rate.report', compact('rate', 'statistiche'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Esporta le rate in CSV
|
||||||
|
*/
|
||||||
|
public function exportCsv(Request $request)
|
||||||
|
{
|
||||||
|
$query = Rata::with([
|
||||||
|
'pianoRateizzazione.ripartizioneSpese.voceSpesa.stabile',
|
||||||
|
'pianoRateizzazione.unitaImmobiliare.anagraficaCondominiale.soggetto'
|
||||||
|
])->whereHas('pianoRateizzazione.ripartizioneSpese.voceSpesa.stabile', function($q) {
|
||||||
|
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Applica gli stessi filtri del report
|
||||||
|
if ($request->filled('stabile_id')) {
|
||||||
|
$query->whereHas('pianoRateizzazione.ripartizioneSpese.voceSpesa', function($q) use ($request) {
|
||||||
|
$q->where('stabile_id', $request->stabile_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('stato')) {
|
||||||
|
$query->where('stato', $request->stato);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('data_da')) {
|
||||||
|
$query->where('data_scadenza', '>=', $request->data_da);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('data_a')) {
|
||||||
|
$query->where('data_scadenza', '<=', $request->data_a);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rate = $query->orderBy('data_scadenza')->get();
|
||||||
|
|
||||||
|
$filename = 'rate_' . Carbon::now()->format('Y-m-d_H-i-s') . '.csv';
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type' => 'text/csv',
|
||||||
|
'Content-Disposition' => "attachment; filename=\"$filename\"",
|
||||||
|
];
|
||||||
|
|
||||||
|
$callback = function() use ($rate) {
|
||||||
|
$file = fopen('php://output', 'w');
|
||||||
|
|
||||||
|
// Intestazioni CSV
|
||||||
|
fputcsv($file, [
|
||||||
|
'Stabile',
|
||||||
|
'Unità Immobiliare',
|
||||||
|
'Condomino',
|
||||||
|
'Piano Rateizzazione',
|
||||||
|
'Numero Rata',
|
||||||
|
'Importo',
|
||||||
|
'Data Scadenza',
|
||||||
|
'Stato',
|
||||||
|
'Data Pagamento',
|
||||||
|
'Importo Pagato',
|
||||||
|
'Metodo Pagamento',
|
||||||
|
'Riferimento'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Dati
|
||||||
|
foreach ($rate as $rata) {
|
||||||
|
fputcsv($file, [
|
||||||
|
$rata->pianoRateizzazione->ripartizioneSpese->voceSpesa->stabile->denominazione,
|
||||||
|
$rata->pianoRateizzazione->unitaImmobiliare->denominazione,
|
||||||
|
$rata->pianoRateizzazione->unitaImmobiliare->anagraficaCondominiale?->soggetto?->denominazione,
|
||||||
|
$rata->pianoRateizzazione->denominazione,
|
||||||
|
$rata->numero_rata,
|
||||||
|
$rata->importo,
|
||||||
|
$rata->data_scadenza,
|
||||||
|
$rata->stato,
|
||||||
|
$rata->data_pagamento,
|
||||||
|
$rata->importo_pagato,
|
||||||
|
$rata->metodo_pagamento,
|
||||||
|
$rata->riferimento_pagamento
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose($file);
|
||||||
|
};
|
||||||
|
|
||||||
|
return response()->stream($callback, 200, $headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
397
app/Http/Controllers/Admin/RipartizioneSpesaController.php
Normal file
397
app/Http/Controllers/Admin/RipartizioneSpesaController.php
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
282
app/Http/Controllers/Admin/VoceSpesaController.php
Normal file
282
app/Http/Controllers/Admin/VoceSpesaController.php
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\VoceSpesa;
|
||||||
|
use App\Models\Stabile;
|
||||||
|
use App\Models\TabellaMillesimale;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class VoceSpesaController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = VoceSpesa::with(['stabile', 'tabellaMillesimaleDefault'])
|
||||||
|
->whereHas('stabile', function($q) {
|
||||||
|
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtro per stabile
|
||||||
|
if ($request->filled('stabile_id')) {
|
||||||
|
$query->where('stabile_id', $request->stabile_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro per categoria
|
||||||
|
if ($request->filled('categoria')) {
|
||||||
|
$query->where('categoria', $request->categoria);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro per stato
|
||||||
|
if ($request->filled('stato')) {
|
||||||
|
$query->where('stato', $request->stato);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ricerca per denominazione
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$query->where('denominazione', 'like', '%' . $request->search . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$vociSpesa = $query->orderBy('denominazione')->paginate(15);
|
||||||
|
|
||||||
|
// Dati per i filtri
|
||||||
|
$stabili = Stabile::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$categorie = VoceSpesa::distinct()->pluck('categoria')->filter()->sort();
|
||||||
|
|
||||||
|
return view('admin.voci-spesa.index', compact('vociSpesa', 'stabili', 'categorie'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$stabili = Stabile::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$tabelleMillesimali = TabellaMillesimale::whereHas('stabile', function($q) {
|
||||||
|
$q->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null);
|
||||||
|
})
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.voci-spesa.create', compact('stabili', 'tabelleMillesimali'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'stabile_id' => 'required|exists:stabili,id',
|
||||||
|
'denominazione' => 'required|string|max:255',
|
||||||
|
'categoria' => 'required|string|max:100',
|
||||||
|
'sottocategoria' => 'nullable|string|max:100',
|
||||||
|
'descrizione' => 'nullable|string',
|
||||||
|
'importo_previsto' => 'nullable|numeric|min:0',
|
||||||
|
'periodicita' => 'nullable|in:una_tantum,mensile,trimestrale,semestrale,annuale',
|
||||||
|
'tabella_millesimale_default_id' => 'required|exists:tabelle_millesimali,id',
|
||||||
|
'ripartizione_personalizzata' => 'nullable|boolean',
|
||||||
|
'stato' => 'required|in:attiva,inattiva,archiviata',
|
||||||
|
'note' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|string|max:500',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verifica che lo stabile appartenga all'amministratore
|
||||||
|
$stabile = Stabile::where('id', $request->stabile_id)
|
||||||
|
->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
// Verifica che la tabella millesimale appartenga allo stabile
|
||||||
|
$tabellaMillesimale = TabellaMillesimale::where('id', $request->tabella_millesimale_default_id)
|
||||||
|
->where('stabile_id', $request->stabile_id)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$voceSpesa = VoceSpesa::create([
|
||||||
|
'codice_spesa' => $this->generateCodiceSpesa(),
|
||||||
|
'stabile_id' => $request->stabile_id,
|
||||||
|
'denominazione' => $request->denominazione,
|
||||||
|
'categoria' => $request->categoria,
|
||||||
|
'sottocategoria' => $request->sottocategoria,
|
||||||
|
'descrizione' => $request->descrizione,
|
||||||
|
'importo_previsto' => $request->importo_previsto,
|
||||||
|
'periodicita' => $request->periodicita,
|
||||||
|
'tabella_millesimale_default_id' => $request->tabella_millesimale_default_id,
|
||||||
|
'ripartizione_personalizzata' => $request->boolean('ripartizione_personalizzata'),
|
||||||
|
'stato' => $request->stato,
|
||||||
|
'note' => $request->note,
|
||||||
|
'tags' => $request->tags,
|
||||||
|
'created_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.voci-spesa.index')
|
||||||
|
->with('success', 'Voce di spesa creata con successo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(VoceSpesa $voceSpesa)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('view', $voceSpesa);
|
||||||
|
|
||||||
|
$voceSpesa->load(['stabile', 'tabellaMillesimaleDefault', 'ripartizioniSpese.dettagli.unitaImmobiliare']);
|
||||||
|
|
||||||
|
return view('admin.voci-spesa.show', compact('voceSpesa'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(VoceSpesa $voceSpesa)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('update', $voceSpesa);
|
||||||
|
|
||||||
|
$stabili = Stabile::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$tabelleMillesimali = TabellaMillesimale::where('stabile_id', $voceSpesa->stabile_id)
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('admin.voci-spesa.edit', compact('voceSpesa', 'stabili', 'tabelleMillesimali'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, VoceSpesa $voceSpesa)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('update', $voceSpesa);
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'denominazione' => 'required|string|max:255',
|
||||||
|
'categoria' => 'required|string|max:100',
|
||||||
|
'sottocategoria' => 'nullable|string|max:100',
|
||||||
|
'descrizione' => 'nullable|string',
|
||||||
|
'importo_previsto' => 'nullable|numeric|min:0',
|
||||||
|
'periodicita' => 'nullable|in:una_tantum,mensile,trimestrale,semestrale,annuale',
|
||||||
|
'tabella_millesimale_default_id' => 'required|exists:tabelle_millesimali,id',
|
||||||
|
'ripartizione_personalizzata' => 'nullable|boolean',
|
||||||
|
'stato' => 'required|in:attiva,inattiva,archiviata',
|
||||||
|
'note' => 'nullable|string',
|
||||||
|
'tags' => 'nullable|string|max:500',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verifica che la tabella millesimale appartenga allo stabile
|
||||||
|
$tabellaMillesimale = TabellaMillesimale::where('id', $request->tabella_millesimale_default_id)
|
||||||
|
->where('stabile_id', $voceSpesa->stabile_id)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$voceSpesa->update([
|
||||||
|
'denominazione' => $request->denominazione,
|
||||||
|
'categoria' => $request->categoria,
|
||||||
|
'sottocategoria' => $request->sottocategoria,
|
||||||
|
'descrizione' => $request->descrizione,
|
||||||
|
'importo_previsto' => $request->importo_previsto,
|
||||||
|
'periodicita' => $request->periodicita,
|
||||||
|
'tabella_millesimale_default_id' => $request->tabella_millesimale_default_id,
|
||||||
|
'ripartizione_personalizzata' => $request->boolean('ripartizione_personalizzata'),
|
||||||
|
'stato' => $request->stato,
|
||||||
|
'note' => $request->note,
|
||||||
|
'tags' => $request->tags,
|
||||||
|
'updated_by' => Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('admin.voci-spesa.index')
|
||||||
|
->with('success', 'Voce di spesa aggiornata con successo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(VoceSpesa $voceSpesa)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('delete', $voceSpesa);
|
||||||
|
|
||||||
|
// Verifica che non ci siano ripartizioni associate
|
||||||
|
if ($voceSpesa->ripartizioniSpese()->exists()) {
|
||||||
|
return redirect()->route('admin.voci-spesa.index')
|
||||||
|
->with('error', 'Impossibile eliminare la voce di spesa: esistono ripartizioni associate.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$voceSpesa->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.voci-spesa.index')
|
||||||
|
->with('success', 'Voce di spesa eliminata con successo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplica una voce di spesa esistente
|
||||||
|
*/
|
||||||
|
public function duplicate(VoceSpesa $voceSpesa)
|
||||||
|
{
|
||||||
|
// Verifica autorizzazione
|
||||||
|
$this->authorize('view', $voceSpesa);
|
||||||
|
|
||||||
|
$nuovaVoceSpesa = $voceSpesa->replicate();
|
||||||
|
$nuovaVoceSpesa->codice_spesa = $this->generateCodiceSpesa();
|
||||||
|
$nuovaVoceSpesa->denominazione = $voceSpesa->denominazione . ' (Copia)';
|
||||||
|
$nuovaVoceSpesa->stato = 'inattiva';
|
||||||
|
$nuovaVoceSpesa->created_by = Auth::id();
|
||||||
|
$nuovaVoceSpesa->save();
|
||||||
|
|
||||||
|
return redirect()->route('admin.voci-spesa.edit', $nuovaVoceSpesa)
|
||||||
|
->with('success', 'Voce di spesa duplicata con successo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ottieni le tabelle millesimali per uno stabile (AJAX)
|
||||||
|
*/
|
||||||
|
public function getTabelleMillesimali(Request $request)
|
||||||
|
{
|
||||||
|
$stabileId = $request->get('stabile_id');
|
||||||
|
|
||||||
|
if (!$stabileId) {
|
||||||
|
return response()->json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica che lo stabile appartenga all'amministratore
|
||||||
|
$stabile = Stabile::where('id', $stabileId)
|
||||||
|
->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$stabile) {
|
||||||
|
return response()->json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabelle = TabellaMillesimale::where('stabile_id', $stabileId)
|
||||||
|
->where('stato', 'attiva')
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->get(['id', 'denominazione', 'tipo_tabella']);
|
||||||
|
|
||||||
|
return response()->json($tabelle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genera un codice spesa univoco
|
||||||
|
*/
|
||||||
|
private function generateCodiceSpesa(): string
|
||||||
|
{
|
||||||
|
do {
|
||||||
|
$codice = 'SP' . strtoupper(Str::random(6));
|
||||||
|
} while (VoceSpesa::where('codice_spesa', $codice)->exists());
|
||||||
|
|
||||||
|
return $codice;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/Policies/PianoRateizzazionePolicy.php
Normal file
90
app/Policies/PianoRateizzazionePolicy.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\PianoRateizzazione;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
|
||||||
|
class PianoRateizzazionePolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole(['admin', 'amministratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, PianoRateizzazione $pianoRateizzazione): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $pianoRateizzazione->ripartizioneSpese->voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole(['admin', 'amministratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, PianoRateizzazione $pianoRateizzazione): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $pianoRateizzazione->ripartizioneSpese->voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, PianoRateizzazione $pianoRateizzazione): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $pianoRateizzazione->ripartizioneSpese->voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the model.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, PianoRateizzazione $pianoRateizzazione): bool
|
||||||
|
{
|
||||||
|
return $this->delete($user, $pianoRateizzazione);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the model.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, PianoRateizzazione $pianoRateizzazione): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole('admin');
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/Policies/RataPolicy.php
Normal file
90
app/Policies/RataPolicy.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Rata;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
|
||||||
|
class RataPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole(['admin', 'amministratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Rata $rata): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $rata->pianoRateizzazione->ripartizioneSpese->voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole(['admin', 'amministratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Rata $rata): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $rata->pianoRateizzazione->ripartizioneSpese->voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Rata $rata): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $rata->pianoRateizzazione->ripartizioneSpese->voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the model.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, Rata $rata): bool
|
||||||
|
{
|
||||||
|
return $this->delete($user, $rata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the model.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, Rata $rata): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole('admin');
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/Policies/RipartizioneSpesaPolicy.php
Normal file
90
app/Policies/RipartizioneSpesaPolicy.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\RipartizioneSpese;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
|
||||||
|
class RipartizioneSpesaPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole(['admin', 'amministratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, RipartizioneSpese $ripartizioneSpese): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $ripartizioneSpese->voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole(['admin', 'amministratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, RipartizioneSpese $ripartizioneSpese): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $ripartizioneSpese->voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, RipartizioneSpese $ripartizioneSpese): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $ripartizioneSpese->voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the model.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, RipartizioneSpese $ripartizioneSpese): bool
|
||||||
|
{
|
||||||
|
return $this->delete($user, $ripartizioneSpese);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the model.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, RipartizioneSpese $ripartizioneSpese): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole('admin');
|
||||||
|
}
|
||||||
|
}
|
||||||
90
app/Policies/VoceSpesaPolicy.php
Normal file
90
app/Policies/VoceSpesaPolicy.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\VoceSpesa;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
|
||||||
|
class VoceSpesaPolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole(['admin', 'amministratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, VoceSpesa $voceSpesa): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole(['admin', 'amministratore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, VoceSpesa $voceSpesa): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, VoceSpesa $voceSpesa): bool
|
||||||
|
{
|
||||||
|
if ($user->hasRole('admin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('amministratore')) {
|
||||||
|
return $voceSpesa->stabile->amministratore_id === $user->amministratore->id_amministratore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the model.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, VoceSpesa $voceSpesa): bool
|
||||||
|
{
|
||||||
|
return $this->delete($user, $voceSpesa);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the model.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, VoceSpesa $voceSpesa): bool
|
||||||
|
{
|
||||||
|
return $user->hasRole('admin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,15 @@ use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvid
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Stabile;
|
use App\Models\Stabile;
|
||||||
|
use App\Models\VoceSpesa;
|
||||||
|
use App\Models\RipartizioneSpese;
|
||||||
|
use App\Models\PianoRateizzazione;
|
||||||
|
use App\Models\Rata;
|
||||||
use App\Policies\StabilePolicy;
|
use App\Policies\StabilePolicy;
|
||||||
|
use App\Policies\VoceSpesaPolicy;
|
||||||
|
use App\Policies\RipartizioneSpesaPolicy;
|
||||||
|
use App\Policies\PianoRateizzazionePolicy;
|
||||||
|
use App\Policies\RataPolicy;
|
||||||
|
|
||||||
|
|
||||||
class AuthServiceProvider extends ServiceProvider
|
class AuthServiceProvider extends ServiceProvider
|
||||||
|
|
@ -16,8 +24,11 @@ class AuthServiceProvider extends ServiceProvider
|
||||||
* @var array<class-string, class-string>
|
* @var array<class-string, class-string>
|
||||||
*/
|
*/
|
||||||
protected $policies = [
|
protected $policies = [
|
||||||
//
|
|
||||||
Stabile::class => StabilePolicy::class,
|
Stabile::class => StabilePolicy::class,
|
||||||
|
VoceSpesa::class => VoceSpesaPolicy::class,
|
||||||
|
RipartizioneSpese::class => RipartizioneSpesaPolicy::class,
|
||||||
|
PianoRateizzazione::class => PianoRateizzazionePolicy::class,
|
||||||
|
Rata::class => RataPolicy::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
242
resources/views/admin/voci-spesa/create.blade.php
Normal file
242
resources/views/admin/voci-spesa/create.blade.php
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Nuova Voce di Spesa')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-receipt"></i> Nuova Voce di Spesa
|
||||||
|
</h3>
|
||||||
|
<a href="{{ route('admin.voci-spesa.index') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Torna all'elenco
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ route('admin.voci-spesa.store') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Informazioni Base -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="stabile_id" class="form-label">Stabile <span class="text-danger">*</span></label>
|
||||||
|
<select name="stabile_id" id="stabile_id" class="form-control @error('stabile_id') is-invalid @enderror" required>
|
||||||
|
<option value="">Seleziona uno stabile</option>
|
||||||
|
@foreach($stabili as $stabile)
|
||||||
|
<option value="{{ $stabile->id }}" {{ old('stabile_id') == $stabile->id ? 'selected' : '' }}>
|
||||||
|
{{ $stabile->denominazione }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('stabile_id')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="denominazione" class="form-label">Denominazione <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="denominazione" id="denominazione"
|
||||||
|
class="form-control @error('denominazione') is-invalid @enderror"
|
||||||
|
value="{{ old('denominazione') }}" required>
|
||||||
|
@error('denominazione')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="categoria" class="form-label">Categoria <span class="text-danger">*</span></label>
|
||||||
|
<select name="categoria" id="categoria" class="form-control @error('categoria') is-invalid @enderror" required>
|
||||||
|
<option value="">Seleziona categoria</option>
|
||||||
|
<option value="Amministrazione" {{ old('categoria') == 'Amministrazione' ? 'selected' : '' }}>Amministrazione</option>
|
||||||
|
<option value="Manutenzione" {{ old('categoria') == 'Manutenzione' ? 'selected' : '' }}>Manutenzione</option>
|
||||||
|
<option value="Pulizia" {{ old('categoria') == 'Pulizia' ? 'selected' : '' }}>Pulizia</option>
|
||||||
|
<option value="Utenze" {{ old('categoria') == 'Utenze' ? 'selected' : '' }}>Utenze</option>
|
||||||
|
<option value="Assicurazioni" {{ old('categoria') == 'Assicurazioni' ? 'selected' : '' }}>Assicurazioni</option>
|
||||||
|
<option value="Riscaldamento" {{ old('categoria') == 'Riscaldamento' ? 'selected' : '' }}>Riscaldamento</option>
|
||||||
|
<option value="Ascensore" {{ old('categoria') == 'Ascensore' ? 'selected' : '' }}>Ascensore</option>
|
||||||
|
<option value="Altro" {{ old('categoria') == 'Altro' ? 'selected' : '' }}>Altro</option>
|
||||||
|
</select>
|
||||||
|
@error('categoria')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="sottocategoria" class="form-label">Sottocategoria</label>
|
||||||
|
<input type="text" name="sottocategoria" id="sottocategoria"
|
||||||
|
class="form-control @error('sottocategoria') is-invalid @enderror"
|
||||||
|
value="{{ old('sottocategoria') }}">
|
||||||
|
@error('sottocategoria')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="descrizione" class="form-label">Descrizione</label>
|
||||||
|
<textarea name="descrizione" id="descrizione" rows="3"
|
||||||
|
class="form-control @error('descrizione') is-invalid @enderror">{{ old('descrizione') }}</textarea>
|
||||||
|
@error('descrizione')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configurazione Ripartizione -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="importo_previsto" class="form-label">Importo Previsto (€)</label>
|
||||||
|
<input type="number" name="importo_previsto" id="importo_previsto"
|
||||||
|
class="form-control @error('importo_previsto') is-invalid @enderror"
|
||||||
|
value="{{ old('importo_previsto') }}" step="0.01" min="0">
|
||||||
|
@error('importo_previsto')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="periodicita" class="form-label">Periodicità</label>
|
||||||
|
<select name="periodicita" id="periodicita" class="form-control @error('periodicita') is-invalid @enderror">
|
||||||
|
<option value="">Non specificata</option>
|
||||||
|
<option value="una_tantum" {{ old('periodicita') == 'una_tantum' ? 'selected' : '' }}>Una tantum</option>
|
||||||
|
<option value="mensile" {{ old('periodicita') == 'mensile' ? 'selected' : '' }}>Mensile</option>
|
||||||
|
<option value="trimestrale" {{ old('periodicita') == 'trimestrale' ? 'selected' : '' }}>Trimestrale</option>
|
||||||
|
<option value="semestrale" {{ old('periodicita') == 'semestrale' ? 'selected' : '' }}>Semestrale</option>
|
||||||
|
<option value="annuale" {{ old('periodicita') == 'annuale' ? 'selected' : '' }}>Annuale</option>
|
||||||
|
</select>
|
||||||
|
@error('periodicita')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tabella_millesimale_default_id" class="form-label">Tabella Millesimale di Default <span class="text-danger">*</span></label>
|
||||||
|
<select name="tabella_millesimale_default_id" id="tabella_millesimale_default_id"
|
||||||
|
class="form-control @error('tabella_millesimale_default_id') is-invalid @enderror" required>
|
||||||
|
<option value="">Seleziona tabella millesimale</option>
|
||||||
|
@foreach($tabelleMillesimali as $tabella)
|
||||||
|
<option value="{{ $tabella->id }}" {{ old('tabella_millesimale_default_id') == $tabella->id ? 'selected' : '' }}>
|
||||||
|
{{ $tabella->denominazione }} ({{ $tabella->tipo_tabella }})
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@error('tabella_millesimale_default_id')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" name="ripartizione_personalizzata" id="ripartizione_personalizzata"
|
||||||
|
class="form-check-input" value="1" {{ old('ripartizione_personalizzata') ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="ripartizione_personalizzata">
|
||||||
|
Ripartizione personalizzata
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Consente di modificare manualmente la ripartizione per singola unità immobiliare
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="stato" class="form-label">Stato <span class="text-danger">*</span></label>
|
||||||
|
<select name="stato" id="stato" class="form-control @error('stato') is-invalid @enderror" required>
|
||||||
|
<option value="attiva" {{ old('stato', 'attiva') == 'attiva' ? 'selected' : '' }}>Attiva</option>
|
||||||
|
<option value="inattiva" {{ old('stato') == 'inattiva' ? 'selected' : '' }}>Inattiva</option>
|
||||||
|
</select>
|
||||||
|
@error('stato')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tags" class="form-label">Tags</label>
|
||||||
|
<input type="text" name="tags" id="tags"
|
||||||
|
class="form-control @error('tags') is-invalid @enderror"
|
||||||
|
value="{{ old('tags') }}" placeholder="Separati da virgola">
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Utilizzare per categorizzare e cercare più facilmente le voci di spesa
|
||||||
|
</small>
|
||||||
|
@error('tags')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="note" class="form-label">Note</label>
|
||||||
|
<textarea name="note" id="note" rows="3"
|
||||||
|
class="form-control @error('note') is-invalid @enderror">{{ old('note') }}</textarea>
|
||||||
|
@error('note')
|
||||||
|
<div class="invalid-feedback">{{ $message }}</div>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ route('admin.voci-spesa.index') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-times"></i> Annulla
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Salva Voce di Spesa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
// Carica le tabelle millesimali quando cambia lo stabile
|
||||||
|
document.getElementById('stabile_id').addEventListener('change', function() {
|
||||||
|
const stabileId = this.value;
|
||||||
|
const tabellaSelect = document.getElementById('tabella_millesimale_default_id');
|
||||||
|
|
||||||
|
// Reset della select
|
||||||
|
tabellaSelect.innerHTML = '<option value="">Caricamento...</option>';
|
||||||
|
|
||||||
|
if (stabileId) {
|
||||||
|
fetch(`{{ route('admin.ajax.tabelle-millesimali') }}?stabile_id=${stabileId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
tabellaSelect.innerHTML = '<option value="">Seleziona tabella millesimale</option>';
|
||||||
|
data.forEach(tabella => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = tabella.id;
|
||||||
|
option.textContent = `${tabella.denominazione} (${tabella.tipo_tabella})`;
|
||||||
|
tabellaSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Errore nel caricamento delle tabelle millesimali:', error);
|
||||||
|
tabellaSelect.innerHTML = '<option value="">Errore nel caricamento</option>';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tabellaSelect.innerHTML = '<option value="">Seleziona prima uno stabile</option>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
207
resources/views/admin/voci-spesa/index.blade.php
Normal file
207
resources/views/admin/voci-spesa/index.blade.php
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Voci di Spesa')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-receipt"></i> Voci di Spesa
|
||||||
|
</h3>
|
||||||
|
<a href="{{ route('admin.voci-spesa.create') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Nuova Voce di Spesa
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Filtri -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<form method="GET" action="{{ route('admin.voci-spesa.index') }}" class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="stabile_id" class="form-label">Stabile</label>
|
||||||
|
<select name="stabile_id" id="stabile_id" class="form-control">
|
||||||
|
<option value="">Tutti gli stabili</option>
|
||||||
|
@foreach($stabili as $stabile)
|
||||||
|
<option value="{{ $stabile->id }}" {{ request('stabile_id') == $stabile->id ? 'selected' : '' }}>
|
||||||
|
{{ $stabile->denominazione }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="categoria" class="form-label">Categoria</label>
|
||||||
|
<select name="categoria" id="categoria" class="form-control">
|
||||||
|
<option value="">Tutte le categorie</option>
|
||||||
|
@foreach($categorie as $categoria)
|
||||||
|
<option value="{{ $categoria }}" {{ request('categoria') == $categoria ? 'selected' : '' }}>
|
||||||
|
{{ $categoria }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="stato" class="form-label">Stato</label>
|
||||||
|
<select name="stato" id="stato" class="form-control">
|
||||||
|
<option value="">Tutti gli stati</option>
|
||||||
|
<option value="attiva" {{ request('stato') == 'attiva' ? 'selected' : '' }}>Attiva</option>
|
||||||
|
<option value="inattiva" {{ request('stato') == 'inattiva' ? 'selected' : '' }}>Inattiva</option>
|
||||||
|
<option value="archiviata" {{ request('stato') == 'archiviata' ? 'selected' : '' }}>Archiviata</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="search" class="form-label">Cerca</label>
|
||||||
|
<input type="text" name="search" id="search" class="form-control"
|
||||||
|
value="{{ request('search') }}" placeholder="Cerca per denominazione...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label"> </label>
|
||||||
|
<div class="btn-group d-block">
|
||||||
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-search"></i> Filtra
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('admin.voci-spesa.index') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-times"></i> Reset
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabella Voci di Spesa -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Codice</th>
|
||||||
|
<th>Denominazione</th>
|
||||||
|
<th>Stabile</th>
|
||||||
|
<th>Categoria</th>
|
||||||
|
<th>Importo Previsto</th>
|
||||||
|
<th>Periodicità</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th>Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($vociSpesa as $voceSpesa)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>{{ $voceSpesa->codice_spesa }}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>{{ $voceSpesa->denominazione }}</strong>
|
||||||
|
@if($voceSpesa->sottocategoria)
|
||||||
|
<br><small class="text-muted">{{ $voceSpesa->sottocategoria }}</small>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>{{ $voceSpesa->stabile->denominazione }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">{{ $voceSpesa->categoria }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($voceSpesa->importo_previsto)
|
||||||
|
<span class="text-success">€ {{ number_format($voceSpesa->importo_previsto, 2, ',', '.') }}</span>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">Non specificato</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($voceSpesa->periodicita)
|
||||||
|
<span class="badge bg-secondary">{{ ucfirst(str_replace('_', ' ', $voceSpesa->periodicita)) }}</span>
|
||||||
|
@else
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@switch($voceSpesa->stato)
|
||||||
|
@case('attiva')
|
||||||
|
<span class="badge bg-success">Attiva</span>
|
||||||
|
@break
|
||||||
|
@case('inattiva')
|
||||||
|
<span class="badge bg-warning">Inattiva</span>
|
||||||
|
@break
|
||||||
|
@case('archiviata')
|
||||||
|
<span class="badge bg-secondary">Archiviata</span>
|
||||||
|
@break
|
||||||
|
@endswitch
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{{ route('admin.voci-spesa.show', $voceSpesa) }}"
|
||||||
|
class="btn btn-sm btn-outline-primary" title="Visualizza">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('admin.voci-spesa.edit', $voceSpesa) }}"
|
||||||
|
class="btn btn-sm btn-outline-warning" title="Modifica">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ route('admin.voci-spesa.duplicate', $voceSpesa) }}"
|
||||||
|
style="display: inline;">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-info" title="Duplica">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="{{ route('admin.voci-spesa.destroy', $voceSpesa) }}"
|
||||||
|
style="display: inline;"
|
||||||
|
onsubmit="return confirm('Sei sicuro di voler eliminare questa voce di spesa?')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" title="Elimina">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center">
|
||||||
|
<div class="py-4">
|
||||||
|
<i class="fas fa-receipt fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">Nessuna voce di spesa trovata</p>
|
||||||
|
<a href="{{ route('admin.voci-spesa.create') }}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Crea la prima voce di spesa
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Paginazione -->
|
||||||
|
@if($vociSpesa->hasPages())
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
{{ $vociSpesa->appends(request()->query())->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
// Auto-submit form when select changes
|
||||||
|
document.getElementById('stabile_id').addEventListener('change', function() {
|
||||||
|
this.form.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('categoria').addEventListener('change', function() {
|
||||||
|
this.form.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('stato').addEventListener('change', function() {
|
||||||
|
this.form.submit();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
|
|
@ -13,6 +13,10 @@ use App\Http\Controllers\Admin\ContabilitaController;
|
||||||
use App\Http\Controllers\Admin\DocumentoController;
|
use App\Http\Controllers\Admin\DocumentoController;
|
||||||
use App\Http\Controllers\Admin\PreventivoController;
|
use App\Http\Controllers\Admin\PreventivoController;
|
||||||
use App\Http\Controllers\Admin\BilancioController;
|
use App\Http\Controllers\Admin\BilancioController;
|
||||||
|
use App\Http\Controllers\Admin\VoceSpesaController;
|
||||||
|
use App\Http\Controllers\Admin\RipartizioneSpesaController;
|
||||||
|
use App\Http\Controllers\Admin\PianoRateizzazioneController;
|
||||||
|
use App\Http\Controllers\Admin\RataController;
|
||||||
use App\Http\Controllers\Condomino\DashboardController as CondominoDashboardController;
|
use App\Http\Controllers\Condomino\DashboardController as CondominoDashboardController;
|
||||||
use App\Http\Controllers\Condomino\TicketController as CondominoTicketController;
|
use App\Http\Controllers\Condomino\TicketController as CondominoTicketController;
|
||||||
use App\Http\Controllers\Condomino\DocumentoController as CondominoDocumentoController;
|
use App\Http\Controllers\Condomino\DocumentoController as CondominoDocumentoController;
|
||||||
|
|
@ -139,7 +143,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||||
Route::get('/automazioni/dashboard', [BilancioController::class, 'automazioniDashboard'])->name('automazioni');
|
Route::get('/automazioni/dashboard', [BilancioController::class, 'automazioniDashboard'])->name('automazioni');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Contabilità
|
// Gestione Contabilità
|
||||||
Route::prefix('contabilita')->name('contabilita.')->group(function () {
|
Route::prefix('contabilita')->name('contabilita.')->group(function () {
|
||||||
Route::get('/', [ContabilitaController::class, 'index'])->name('index');
|
Route::get('/', [ContabilitaController::class, 'index'])->name('index');
|
||||||
Route::get('/movimenti', [ContabilitaController::class, 'movimenti'])->name('movimenti');
|
Route::get('/movimenti', [ContabilitaController::class, 'movimenti'])->name('movimenti');
|
||||||
|
|
@ -149,6 +153,30 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||||
Route::post('/import-xml', [ContabilitaController::class, 'importXml'])->name('import-xml.store');
|
Route::post('/import-xml', [ContabilitaController::class, 'importXml'])->name('import-xml.store');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gestione Voci di Spesa
|
||||||
|
Route::resource('voci-spesa', VoceSpesaController::class);
|
||||||
|
Route::post('voci-spesa/{voceSpesa}/duplicate', [VoceSpesaController::class, 'duplicate'])->name('voci-spesa.duplicate');
|
||||||
|
Route::get('ajax/tabelle-millesimali', [VoceSpesaController::class, 'getTabelleMillesimali'])->name('ajax.tabelle-millesimali');
|
||||||
|
|
||||||
|
// Gestione Ripartizioni Spesa
|
||||||
|
Route::resource('ripartizioni-spesa', RipartizioneSpesaController::class);
|
||||||
|
Route::post('ripartizioni-spesa/{ripartizioneSpesa}/conferma', [RipartizioneSpesaController::class, 'conferma'])->name('ripartizioni-spesa.conferma');
|
||||||
|
|
||||||
|
// Gestione Piani Rateizzazione
|
||||||
|
Route::resource('piani-rateizzazione', PianoRateizzazioneController::class);
|
||||||
|
Route::post('piani-rateizzazione/{pianoRateizzazione}/attiva', [PianoRateizzazioneController::class, 'attiva'])->name('piani-rateizzazione.attiva');
|
||||||
|
Route::post('piani-rateizzazione/{pianoRateizzazione}/sospendi', [PianoRateizzazioneController::class, 'sospendi'])->name('piani-rateizzazione.sospendi');
|
||||||
|
|
||||||
|
// Gestione Rate
|
||||||
|
Route::resource('rate', RataController::class)->only(['index', 'show', 'edit', 'update']);
|
||||||
|
Route::get('rate/{rata}/pagamento', [RataController::class, 'showPagamentoForm'])->name('rate.pagamento');
|
||||||
|
Route::post('rate/{rata}/registra-pagamento', [RataController::class, 'registraPagamento'])->name('rate.registra-pagamento');
|
||||||
|
Route::delete('rate/{rata}/annulla-pagamento', [RataController::class, 'annullaPagamento'])->name('rate.annulla-pagamento');
|
||||||
|
Route::get('rate/{rata}/posticipo', [RataController::class, 'showPosticipoForm'])->name('rate.posticipo');
|
||||||
|
Route::post('rate/{rata}/posticipa', [RataController::class, 'posticipa'])->name('rate.posticipa');
|
||||||
|
Route::get('rate/report', [RataController::class, 'report'])->name('rate.report');
|
||||||
|
Route::get('rate/export/csv', [RataController::class, 'exportCsv'])->name('rate.export.csv');
|
||||||
|
|
||||||
// Impostazioni e API Tokens
|
// Impostazioni e API Tokens
|
||||||
Route::get('impostazioni', [ImpostazioniController::class, 'index'])->name('impostazioni.index');
|
Route::get('impostazioni', [ImpostazioniController::class, 'index'])->name('impostazioni.index');
|
||||||
Route::post('impostazioni', [ImpostazioniController::class, 'store'])->name('impostazioni.store');
|
Route::post('impostazioni', [ImpostazioniController::class, 'store'])->name('impostazioni.store');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user