stabilization/205-zero: fix catastali, anagrafica unica, PDF e allineamento speculare
This commit is contained in:
parent
30b4f89a7b
commit
835956a023
|
|
@ -1151,34 +1151,48 @@ private function findExistingRubricaMatch(int $amministratoreId, array $data): a
|
|||
}
|
||||
}
|
||||
|
||||
// Fallback: match by normalized name/cognome
|
||||
// Fallback: match by normalized name/cognome or ragione_sociale
|
||||
$nome = trim((string) ($data['nome'] ?? ''));
|
||||
$cognome = trim((string) ($data['cognome'] ?? ''));
|
||||
if ($nome !== '' || $cognome !== '') {
|
||||
$ragione = trim((string) ($data['ragione_sociale'] ?? ''));
|
||||
if ($nome !== '' || $cognome !== '' || $ragione !== '') {
|
||||
$normalizedKey = $this->normalizeNameKey($nome, $cognome);
|
||||
if ($normalizedKey !== '') {
|
||||
// Find all active records for this administrator
|
||||
$candidates = RubricaUniversale::query()
|
||||
->where('amministratore_id', $amministratoreId)
|
||||
->get();
|
||||
foreach ($candidates as $cand) {
|
||||
if ($this->normalizeNameKey($cand->nome, $cand->cognome) === $normalizedKey) {
|
||||
return ['record' => $cand, 'match_type' => 'name'];
|
||||
$incomingFullKey = $this->normalizeIdentityKey($nome . ' ' . $cognome . ' ' . $ragione);
|
||||
|
||||
// Find all active records for this administrator
|
||||
$candidates = RubricaUniversale::query()
|
||||
->where('amministratore_id', $amministratoreId)
|
||||
->get();
|
||||
foreach ($candidates as $cand) {
|
||||
if ($normalizedKey !== '' && $this->normalizeNameKey($cand->nome, $cand->cognome) === $normalizedKey) {
|
||||
return ['record' => $cand, 'match_type' => 'name'];
|
||||
}
|
||||
if ($incomingFullKey !== null) {
|
||||
$candFullKey = $this->normalizeIdentityKey(($cand->nome ?? '') . ' ' . ($cand->cognome ?? '') . ' ' . ($cand->ragione_sociale ?? ''));
|
||||
if ($candFullKey === $incomingFullKey) {
|
||||
return ['record' => $cand, 'match_type' => 'name_full'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try orphan candidates
|
||||
$orphans = RubricaUniversale::query()
|
||||
->whereNull('amministratore_id')
|
||||
->get();
|
||||
foreach ($orphans as $orphan) {
|
||||
if ($this->normalizeNameKey($orphan->nome, $orphan->cognome) === $normalizedKey) {
|
||||
return ['record' => $orphan, 'match_type' => 'name'];
|
||||
// Try orphan candidates
|
||||
$orphans = RubricaUniversale::query()
|
||||
->whereNull('amministratore_id')
|
||||
->get();
|
||||
foreach ($orphans as $orphan) {
|
||||
if ($normalizedKey !== '' && $this->normalizeNameKey($orphan->nome, $orphan->cognome) === $normalizedKey) {
|
||||
return ['record' => $orphan, 'match_type' => 'name'];
|
||||
}
|
||||
if ($incomingFullKey !== null) {
|
||||
$orphanFullKey = $this->normalizeIdentityKey(($orphan->nome ?? '') . ' ' . ($orphan->cognome ?? '') . ' ' . ($orphan->ragione_sociale ?? ''));
|
||||
if ($orphanFullKey === $incomingFullKey) {
|
||||
return ['record' => $orphan, 'match_type' => 'name_full'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ['record' => null, 'match_type' => 'none'];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,20 +132,40 @@ public function handle(): int
|
|||
$stats['persona_created']++;
|
||||
}
|
||||
|
||||
$legacyPayload = $this->decodeLegacyPayload($row->legacy_payload ?? null);
|
||||
$cia = isset($legacyPayload['cia']) ? trim((string) $legacyPayload['cia']) : '';
|
||||
|
||||
if ($cia !== '') {
|
||||
$ciaUpper = strtoupper($cia);
|
||||
$riceveComunicazioni = str_contains($ciaUpper, 'I');
|
||||
$riceveConvocazioni = str_contains($ciaUpper, 'C');
|
||||
$votaAssemblea = str_contains($ciaUpper, 'A');
|
||||
} else {
|
||||
$riceveComunicazioni = true;
|
||||
$riceveConvocazioni = $ruoloRate === 'C';
|
||||
$votaAssemblea = $ruoloRate === 'C';
|
||||
}
|
||||
|
||||
$relationPayload = [
|
||||
'quota_relazione' => $this->normalizePercentValue($row->percentuale ?? null),
|
||||
'data_inizio' => $this->normalizeDateToYmd($row->data_inizio ?? null) ?: '1900-01-01',
|
||||
'data_fine' => $this->normalizeDateToYmd($row->data_fine ?? null),
|
||||
'attivo' => $this->isRelationActive($row->data_fine ?? null),
|
||||
'riceve_comunicazioni' => true,
|
||||
'riceve_convocazioni' => $ruoloRate === 'C',
|
||||
'vota_assemblea' => $ruoloRate === 'C',
|
||||
'riceve_comunicazioni' => $riceveComunicazioni,
|
||||
'riceve_convocazioni' => $riceveConvocazioni,
|
||||
'vota_assemblea' => $votaAssemblea,
|
||||
'note_relazione' => $this->buildRelationNote($row, $tipoRelazione),
|
||||
];
|
||||
|
||||
if (Schema::hasColumn('persone_unita_relazioni', 'ruolo_rate')) {
|
||||
$relationPayload['ruolo_rate'] = $ruoloRate;
|
||||
}
|
||||
if (Schema::hasColumn('persone_unita_relazioni', 'diritto_reale') && ! empty($legacyPayload['diritto_reale'])) {
|
||||
$relationPayload['diritto_reale'] = $legacyPayload['diritto_reale'];
|
||||
}
|
||||
if (Schema::hasColumn('persone_unita_relazioni', 'descrizione_diritto') && ! empty($legacyPayload['diritto_label'])) {
|
||||
$relationPayload['descrizione_diritto'] = $legacyPayload['diritto_label'];
|
||||
}
|
||||
|
||||
$existingRelationId = $this->findExistingRelationId(
|
||||
(int) $persona->id,
|
||||
|
|
@ -161,7 +181,7 @@ public function handle(): int
|
|||
->update($relationPayload + ['updated_at' => now()]);
|
||||
$stats['rel_updated']++;
|
||||
} else {
|
||||
DB::table('persone_unita_relazioni')->insert([
|
||||
$insertData = [
|
||||
'persona_id' => (int) $persona->id,
|
||||
'unita_id' => (int) $row->unita_immobiliare_id,
|
||||
'tipo_relazione' => $tipoRelazione,
|
||||
|
|
@ -173,10 +193,19 @@ public function handle(): int
|
|||
'riceve_convocazioni' => $relationPayload['riceve_convocazioni'],
|
||||
'vota_assemblea' => $relationPayload['vota_assemblea'],
|
||||
'note_relazione' => $relationPayload['note_relazione'],
|
||||
'ruolo_rate' => $relationPayload['ruolo_rate'] ?? null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
];
|
||||
if (isset($relationPayload['ruolo_rate'])) {
|
||||
$insertData['ruolo_rate'] = $relationPayload['ruolo_rate'];
|
||||
}
|
||||
if (isset($relationPayload['diritto_reale'])) {
|
||||
$insertData['diritto_reale'] = $relationPayload['diritto_reale'];
|
||||
}
|
||||
if (isset($relationPayload['descrizione_diritto'])) {
|
||||
$insertData['descrizione_diritto'] = $relationPayload['descrizione_diritto'];
|
||||
}
|
||||
DB::table('persone_unita_relazioni')->insert($insertData);
|
||||
$stats['rel_created']++;
|
||||
}
|
||||
}
|
||||
|
|
@ -499,6 +528,20 @@ private function buildRelationNote(object $row, string $tipoRelazione): string
|
|||
'tipo=' . $tipoRelazione,
|
||||
];
|
||||
|
||||
$legacyPayload = $this->decodeLegacyPayload($row->legacy_payload ?? null);
|
||||
if (! empty($legacyPayload['diritto_reale'])) {
|
||||
$parts[] = 'diritto_reale=' . trim((string) $legacyPayload['diritto_reale']);
|
||||
}
|
||||
if (! empty($legacyPayload['diritto_label'])) {
|
||||
$parts[] = 'diritto_label=' . trim((string) $legacyPayload['diritto_label']);
|
||||
}
|
||||
if (! empty($legacyPayload['cumulo_cond'])) {
|
||||
$parts[] = 'cumulo_cond=' . trim((string) $legacyPayload['cumulo_cond']);
|
||||
}
|
||||
if (! empty($legacyPayload['cumulo_inq'])) {
|
||||
$parts[] = 'cumulo_inq=' . trim((string) $legacyPayload['cumulo_inq']);
|
||||
}
|
||||
|
||||
return implode(' | ', array_filter($parts));
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -404,6 +404,34 @@ public function handle(): int
|
|||
return $s;
|
||||
}
|
||||
|
||||
// Deterministic Y2K parser for DD/MM/YY or DD/MM/YYYY
|
||||
if (preg_match('/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2,4})( \d{2}:\d{2}:\d{2})?$/', $s, $matches)) {
|
||||
$day = (int) $matches[1];
|
||||
$month = (int) $matches[2];
|
||||
$year = (int) $matches[3];
|
||||
$time = $matches[4] ?? '';
|
||||
|
||||
if ($month > 12 && $day <= 12) {
|
||||
$tmp = $day;
|
||||
$day = $month;
|
||||
$month = $tmp;
|
||||
}
|
||||
|
||||
if (strlen((string) $year) === 2) {
|
||||
if ($year >= 70) {
|
||||
$year += 1900;
|
||||
} else {
|
||||
$year += 2000;
|
||||
}
|
||||
}
|
||||
|
||||
$formatted = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
if ($time !== '') {
|
||||
$formatted .= $time;
|
||||
}
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
$dt = \DateTime::createFromFormat('m/d/y H:i:s', $s) ?: \DateTime::createFromFormat('d/m/y H:i:s', $s);
|
||||
if ($dt) {
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
|
|
@ -430,8 +458,16 @@ public function handle(): int
|
|||
|
||||
$s = str_replace(["\r", "\n", "\t"], '', $s);
|
||||
$s = str_replace(' ', '', $s);
|
||||
$s = str_replace('.', '', $s);
|
||||
$s = str_replace(',', '.', $s);
|
||||
|
||||
if (str_contains($s, ',') && !str_contains($s, '.')) {
|
||||
$s = str_replace(',', '.', $s);
|
||||
} elseif (str_contains($s, '.') && !str_contains($s, ',')) {
|
||||
// Keep the dot as the decimal separator (no thousands separator)
|
||||
} elseif (str_contains($s, '.') && str_contains($s, ',')) {
|
||||
$s = str_replace('.', '', $s);
|
||||
$s = str_replace(',', '.', $s);
|
||||
}
|
||||
|
||||
return is_numeric($s) ? (float) $s : null;
|
||||
};
|
||||
|
||||
|
|
@ -766,6 +802,10 @@ public function handle(): int
|
|||
$filtered['pagata'] = 0;
|
||||
}
|
||||
|
||||
if ($hasColumn('anno') && ! isset($filtered['anno'])) {
|
||||
$filtered['anno'] = $year ?: (int) date('Y');
|
||||
}
|
||||
|
||||
break;
|
||||
case 'incassi':
|
||||
if ($hasColumn('cod_cond') && isset($assoc['cod_cond'])) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Filament\Pages\Strumenti\DocumentiArchivio;
|
||||
use App\Models\Assemblea;
|
||||
use App\Models\OrdineGiorno;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\User;
|
||||
use App\Support\StabileContext;
|
||||
|
|
@ -33,6 +34,7 @@ class AssembleeHub extends Page
|
|||
protected string $view = 'filament.pages.condomini.assemblee-hub';
|
||||
|
||||
public ?Stabile $stabileAttivo = null;
|
||||
public string $tab = 'elenco';
|
||||
|
||||
// Form creazione nuova assemblea
|
||||
public ?string $nuovaTipo = 'ordinaria';
|
||||
|
|
@ -78,31 +80,17 @@ public function creaNuovaAssemblea(): void
|
|||
{
|
||||
if (!$this->stabileAttivo) return;
|
||||
|
||||
$this->validate([
|
||||
'nuovaTipo' => 'required|string',
|
||||
'nuovaData1' => 'required',
|
||||
'nuovaData2' => 'required',
|
||||
]);
|
||||
|
||||
$assemblea = Assemblea::create([
|
||||
'stabile_id' => $this->stabileAttivo->id,
|
||||
'tipo' => $this->nuovaTipo,
|
||||
'data_prima_convocazione' => $this->nuovaData1,
|
||||
'data_seconda_convocazione' => $this->nuovaData2,
|
||||
'luogo' => $this->nuovaLuogo,
|
||||
'note' => $this->nuovaNote,
|
||||
'tipo' => 'ordinaria',
|
||||
'data_prima_convocazione' => now()->addDays(15)->setHour(17)->setMinute(0)->setSecond(0),
|
||||
'data_seconda_convocazione' => now()->addDays(16)->setHour(17)->setMinute(0)->setSecond(0),
|
||||
'luogo' => 'Da definire',
|
||||
'note' => '',
|
||||
'stato' => 'bozza',
|
||||
'creato_da_user_id' => Auth::id(),
|
||||
]);
|
||||
|
||||
$this->nuovaTipo = 'ordinaria';
|
||||
$this->nuovaData1 = '';
|
||||
$this->nuovaData2 = '';
|
||||
$this->nuovaLuogo = '';
|
||||
$this->nuovaNote = '';
|
||||
|
||||
$this->dispatch('close-modal', id: 'modal-nuova-assemblea');
|
||||
|
||||
$this->redirect(GestioneAssemblea::getUrl(['record' => $assemblea->id]));
|
||||
}
|
||||
|
||||
|
|
@ -176,4 +164,22 @@ private function canQueryAssemblee(): bool
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSospesiRowsProperty(): Collection
|
||||
{
|
||||
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
|
||||
if ($stabileId <= 0) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return OrdineGiorno::query()
|
||||
->whereHas('assemblea', function ($query) use ($stabileId) {
|
||||
$query->where('stabile_id', $stabileId)
|
||||
->where('stato', 'svolta');
|
||||
})
|
||||
->whereIn('esito_votazione', ['non_deliberato', 'rimandato', 'sospeso'])
|
||||
->whereNull('imported_to_id')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Columns\DatePickerColumn;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
|
|
@ -357,10 +356,22 @@ public function table(Table $table): Table
|
|||
TextColumn::make('gestione_label')
|
||||
->label('Gestione')
|
||||
->getStateUsing(fn(GestioneContabile $record): string => $record->gestione_label)
|
||||
->formatStateUsing(function (?string $state): ?string {
|
||||
if (empty($state)) return $state;
|
||||
return preg_replace('/^\s*(ordinaria|riscaldamento|straordinaria)\s*(?:-\s*)?(?:(ordinaria|riscaldamento|straordinaria)(?:\s*#\d+)?\s*(?:-\s*)?)?/ui', '', $state);
|
||||
})
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('tipo_gestione')
|
||||
->label('Tipo')
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'ordinaria' => 'success',
|
||||
'riscaldamento' => 'info',
|
||||
'straordinaria' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => ucfirst($state))
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('numero_straordinaria')
|
||||
|
|
@ -372,13 +383,13 @@ public function table(Table $table): Table
|
|||
->label('Denominazione')
|
||||
->searchable(),
|
||||
|
||||
DatePickerColumn::make('data_inizio')
|
||||
TextColumn::make('data_inizio')
|
||||
->label('Data inizio')
|
||||
->native(false),
|
||||
->date('d/m/Y'),
|
||||
|
||||
DatePickerColumn::make('data_fine')
|
||||
TextColumn::make('data_fine')
|
||||
->label('Data fine')
|
||||
->native(false),
|
||||
->date('d/m/Y'),
|
||||
|
||||
SelectColumn::make('stato')
|
||||
->label('Stato')
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
|
|
@ -314,6 +315,23 @@ protected function getContrattoFormSchema(): array
|
|||
->label('Titolo')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
FileUpload::make('pdf_contratto')
|
||||
->label('PDF Contratto (Cloud Drive)')
|
||||
->disk('public')
|
||||
->directory(function (callable $get) {
|
||||
$stabileId = (int) $get('stabile_id');
|
||||
$adminCode = 'ZXNRE9CZ';
|
||||
$stabileCode = '0021';
|
||||
if ($stabileId > 0) {
|
||||
$stabile = \App\Models\Stabile::find($stabileId);
|
||||
if ($stabile) {
|
||||
$stabileCode = $stabile->codice_stabile ?: $stabile->cod_stabile ?: '0021';
|
||||
}
|
||||
}
|
||||
return "{$adminCode}/{$stabileCode}/contratti";
|
||||
})
|
||||
->acceptedFileTypes(['application/pdf'])
|
||||
->preserveFilenames(),
|
||||
TextInput::make('servizio_label')
|
||||
->label('Servizio / utenza')
|
||||
->placeholder('Es. Ascensore scala A, Acqua potabile, Luce comune')
|
||||
|
|
@ -396,16 +414,43 @@ protected function createContratto(array $data): void
|
|||
return;
|
||||
}
|
||||
|
||||
$pdfContratto = $data['pdf_contratto'] ?? null;
|
||||
$docId = null;
|
||||
$parsed = [];
|
||||
if ($pdfContratto) {
|
||||
$doc = Documento::create([
|
||||
'stabile_id' => $stabileId,
|
||||
'utente_id' => $user->id,
|
||||
'nome' => trim((string) ($data['titolo'] ?? '')) . ' - Documento Contratto',
|
||||
'tipo_documento' => 'contratto',
|
||||
'data_documento' => $data['data_stipula'] ?? now()->toDateString(),
|
||||
'nome_file' => basename($pdfContratto),
|
||||
'mime_type' => 'application/pdf',
|
||||
'path_file' => $pdfContratto,
|
||||
'percorso_file' => $pdfContratto,
|
||||
'estensione' => 'pdf',
|
||||
'visibility_scope' => 'interno',
|
||||
'data_upload' => now(),
|
||||
]);
|
||||
$docId = $doc->id;
|
||||
|
||||
try {
|
||||
$parsed = app(\App\Services\FatturaParserService::class)->parsePdf(storage_path('app/public/' . $pdfContratto));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("Errore nel parsing del PDF: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$contract = StabileContratto::query()->create([
|
||||
'stabile_id' => $stabileId,
|
||||
'fornitore_id' => isset($data['fornitore_id']) && is_numeric($data['fornitore_id']) ? (int) $data['fornitore_id'] : null,
|
||||
'documento_principale_id' => isset($data['documento_principale_id']) && is_numeric($data['documento_principale_id']) ? (int) $data['documento_principale_id'] : null,
|
||||
'documento_principale_id' => $docId ?: (isset($data['documento_principale_id']) && is_numeric($data['documento_principale_id']) ? (int) $data['documento_principale_id'] : null),
|
||||
'titolo' => trim((string) ($data['titolo'] ?? '')),
|
||||
'tipo_contratto' => trim((string) ($data['tipo_contratto'] ?? 'altro')),
|
||||
'categoria_impianto' => trim((string) ($data['tipo_contratto'] ?? 'altro')),
|
||||
'tipo_contratto' => !empty($parsed['categoria']) && $parsed['categoria'] !== 'altro' ? $parsed['categoria'] : trim((string) ($data['tipo_contratto'] ?? 'altro')),
|
||||
'categoria_impianto' => !empty($parsed['categoria']) && $parsed['categoria'] !== 'altro' ? $parsed['categoria'] : trim((string) ($data['tipo_contratto'] ?? 'altro')),
|
||||
'servizio_label' => $this->nullableString($data['servizio_label'] ?? null),
|
||||
'codice_contratto' => $this->nullableString($data['codice_contratto'] ?? null),
|
||||
'riferimento_esterno' => $this->nullableString($data['riferimento_esterno'] ?? null),
|
||||
'codice_contratto' => !empty($parsed['codice_cliente']) ? $parsed['codice_cliente'] : $this->nullableString($data['codice_contratto'] ?? null),
|
||||
'riferimento_esterno' => !empty($parsed['matricola_contatore']) ? $parsed['matricola_contatore'] : $this->nullableString($data['riferimento_esterno'] ?? null),
|
||||
'stato' => 'attivo',
|
||||
'data_stipula' => $data['data_stipula'] ?? null,
|
||||
'decorrenza_dal' => $data['decorrenza_dal'] ?? null,
|
||||
|
|
@ -456,6 +501,105 @@ protected function createContratto(array $data): void
|
|||
->send();
|
||||
}
|
||||
|
||||
public function aggiornaCampoInline(int $contractId, string $field, ?string $value): void
|
||||
{
|
||||
$contract = StabileContratto::find($contractId);
|
||||
if ($contract) {
|
||||
$updValue = trim((string) $value) !== '' ? trim((string) $value) : null;
|
||||
$contract->update([
|
||||
$field => $updValue
|
||||
]);
|
||||
|
||||
if (in_array($field, ['decorrenza_dal', 'decorrenza_al', 'frequenza_scadenze', 'importo_periodico', 'giorni_preavviso', 'tipo_contratto'])) {
|
||||
$this->syncScadenzeContratto($contract);
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Contratto aggiornato')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function caricaPdfContrattoAction(): Action
|
||||
{
|
||||
return Action::make('caricaPdfContratto')
|
||||
->label('Carica PDF')
|
||||
->icon('heroicon-o-document-arrow-up')
|
||||
->form([
|
||||
FileUpload::make('pdf_contratto')
|
||||
->label('PDF Contratto (Cloud Drive)')
|
||||
->disk('public')
|
||||
->directory(function () {
|
||||
$adminCode = 'ZXNRE9CZ';
|
||||
$stabileCode = $this->stabileAttivo?->codice_stabile ?: $this->stabileAttivo?->cod_stabile ?: '0021';
|
||||
return "{$adminCode}/{$stabileCode}/contratti";
|
||||
})
|
||||
->acceptedFileTypes(['application/pdf'])
|
||||
->preserveFilenames()
|
||||
->required(),
|
||||
])
|
||||
->action(function (array $data, array $arguments): void {
|
||||
$contractId = $arguments['contract_id'] ?? null;
|
||||
if (!$contractId) {
|
||||
return;
|
||||
}
|
||||
$contract = StabileContratto::find($contractId);
|
||||
if (!$contract) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pdfContratto = $data['pdf_contratto'];
|
||||
$user = Auth::user();
|
||||
|
||||
$doc = Documento::create([
|
||||
'stabile_id' => $contract->stabile_id,
|
||||
'utente_id' => $user->id,
|
||||
'nome' => $contract->titolo . ' - Documento Contratto',
|
||||
'tipo_documento' => 'contratto',
|
||||
'data_documento' => $contract->decorrenza_dal ?: now()->toDateString(),
|
||||
'nome_file' => basename($pdfContratto),
|
||||
'mime_type' => 'application/pdf',
|
||||
'path_file' => $pdfContratto,
|
||||
'percorso_file' => $pdfContratto,
|
||||
'estensione' => 'pdf',
|
||||
'visibility_scope' => 'interno',
|
||||
'data_upload' => now(),
|
||||
]);
|
||||
|
||||
$contract->update([
|
||||
'documento_principale_id' => $doc->id,
|
||||
]);
|
||||
|
||||
try {
|
||||
$parsed = app(\App\Services\FatturaParserService::class)->parsePdf(storage_path('app/public/' . $pdfContratto));
|
||||
if (!empty($parsed)) {
|
||||
$upd = [];
|
||||
if (!empty($parsed['codice_cliente'])) {
|
||||
$upd['codice_contratto'] = $parsed['codice_cliente'];
|
||||
}
|
||||
if (!empty($parsed['categoria']) && $parsed['categoria'] !== 'altro') {
|
||||
$upd['tipo_contratto'] = $parsed['categoria'];
|
||||
}
|
||||
if (!empty($parsed['matricola_contatore'])) {
|
||||
$upd['riferimento_esterno'] = $parsed['matricola_contatore'];
|
||||
}
|
||||
if (!empty($upd)) {
|
||||
$contract->update($upd);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("Errore parsing pdf: " . $e->getMessage());
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('PDF caricato ed analizzato con successo')
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected function syncAcquaContract(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@
|
|||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use UnitEnum;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class GestioneAssemblea extends Page
|
||||
{
|
||||
use WithFileUploads;
|
||||
protected static ?string $navigationLabel = 'Gestione Assemblea';
|
||||
|
||||
protected static ?string $title = 'Gestione Assemblea';
|
||||
|
|
@ -49,6 +51,10 @@ class GestioneAssemblea extends Page
|
|||
public ?string $odgDescrizione = '';
|
||||
public ?string $odgArticoloLegge = '';
|
||||
public ?int $odgTabellaMillesimaleId = null;
|
||||
public ?string $odgMaggioranza = 'semplice';
|
||||
public ?string $odgRiferimentoLegge = '';
|
||||
public $odgAllegati = []; // Upload file allegati
|
||||
public $audioUpload; // Upload file audio della delibera
|
||||
|
||||
// Form nuova presenza
|
||||
public ?int $presenzaSoggettoId = null;
|
||||
|
|
@ -56,6 +62,13 @@ class GestioneAssemblea extends Page
|
|||
public string $presenzaTipo = 'personale';
|
||||
public ?int $presenzaDelegatoId = null;
|
||||
|
||||
// Modifica dati assemblea
|
||||
public ?string $editTipo = 'ordinaria';
|
||||
public ?string $editData1 = '';
|
||||
public ?string $editData2 = '';
|
||||
public ?string $editLuogo = '';
|
||||
public ?string $editNote = '';
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
|
@ -72,6 +85,12 @@ public function mount(int $record): void
|
|||
}
|
||||
|
||||
$this->stabileAttivo = $this->assemblea->stabile;
|
||||
$this->editTipo = $this->assemblea->tipo ?: 'ordinaria';
|
||||
$this->editData1 = $this->assemblea->data_prima_convocazione ? $this->assemblea->data_prima_convocazione->format('Y-m-d\TH:i') : '';
|
||||
$this->editData2 = $this->assemblea->data_seconda_convocazione ? $this->assemblea->data_seconda_convocazione->format('Y-m-d\TH:i') : '';
|
||||
$this->editLuogo = $this->assemblea->luogo ?: '';
|
||||
$this->editNote = $this->assemblea->note ?: '';
|
||||
|
||||
$this->resetOdgForm();
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +107,10 @@ public function resetOdgForm(): void
|
|||
$this->odgDescrizione = '';
|
||||
$this->odgArticoloLegge = '';
|
||||
$this->odgTabellaMillesimaleId = null;
|
||||
$this->odgMaggioranza = 'semplice';
|
||||
$this->odgRiferimentoLegge = '';
|
||||
$this->odgAllegati = [];
|
||||
$this->audioUpload = null;
|
||||
}
|
||||
|
||||
public function aggiungiPuntoOdG(): void
|
||||
|
|
@ -95,22 +118,164 @@ public function aggiungiPuntoOdG(): void
|
|||
$this->validate([
|
||||
'odgNumeroPunto' => 'required|integer',
|
||||
'odgTitolo' => 'required|string|max:255',
|
||||
'odgDescrizione' => 'required|string',
|
||||
'odgDescrizione' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$allegatiPaths = [];
|
||||
if ($this->odgAllegati) {
|
||||
foreach ($this->odgAllegati as $file) {
|
||||
$path = $file->store('assemblee/' . $this->record . '/allegati', 'public');
|
||||
$allegatiPaths[] = [
|
||||
'nome' => $file->getClientOriginalName(),
|
||||
'path' => $path,
|
||||
'url' => asset('storage/' . $path)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$maxOrder = OrdineGiorno::where('assemblea_id', $this->record)->max('ordinamento') ?? 0;
|
||||
|
||||
OrdineGiorno::create([
|
||||
'assemblea_id' => $this->record,
|
||||
'numero_punto' => $this->odgNumeroPunto,
|
||||
'ordinamento' => $maxOrder + 1,
|
||||
'titolo' => $this->odgTitolo,
|
||||
'descrizione' => $this->odgDescrizione,
|
||||
'descrizione' => $this->odgDescrizione ?: '',
|
||||
'allegati' => $allegatiPaths,
|
||||
'articolo_legge' => $this->odgArticoloLegge,
|
||||
'tabella_millesimale_id' => $this->odgTabellaMillesimaleId,
|
||||
'maggioranza_richiesta' => $this->odgMaggioranza,
|
||||
'riferimento_legge' => $this->odgRiferimentoLegge,
|
||||
]);
|
||||
|
||||
$this->resetOdgForm();
|
||||
$this->notification('Punto all\'ordine del giorno aggiunto!');
|
||||
}
|
||||
|
||||
public function importaSospeso(int $odgId): void
|
||||
{
|
||||
$sospeso = OrdineGiorno::find($odgId);
|
||||
if ($sospeso) {
|
||||
$maxOrder = OrdineGiorno::where('assemblea_id', $this->record)->max('ordinamento') ?? 0;
|
||||
$maxNumeroPunto = OrdineGiorno::where('assemblea_id', $this->record)->max('numero_punto') ?? 0;
|
||||
|
||||
$nuovo = OrdineGiorno::create([
|
||||
'assemblea_id' => $this->record,
|
||||
'numero_punto' => $maxNumeroPunto + 1,
|
||||
'ordinamento' => $maxOrder + 1,
|
||||
'titolo' => $sospeso->titolo,
|
||||
'descrizione' => $sospeso->descrizione,
|
||||
'allegati' => $sospeso->allegati,
|
||||
'articolo_legge' => $sospeso->articolo_legge,
|
||||
'tabella_millesimale_id' => $sospeso->tabella_millesimale_id,
|
||||
'maggioranza_richiesta' => $sospeso->maggioranza_richiesta,
|
||||
'riferimento_legge' => $sospeso->riferimento_legge,
|
||||
]);
|
||||
|
||||
$sospeso->update([
|
||||
'imported_to_id' => $nuovo->id
|
||||
]);
|
||||
|
||||
$this->notification('Punto sospeso importato con successo!');
|
||||
}
|
||||
}
|
||||
|
||||
public function getDisponibiliSospesiProperty(): Collection
|
||||
{
|
||||
if (!$this->stabileAttivo) return collect();
|
||||
$stabileId = $this->stabileAttivo->id;
|
||||
|
||||
return OrdineGiorno::query()
|
||||
->whereHas('assemblea', function ($query) use ($stabileId) {
|
||||
$query->where('stabile_id', $stabileId)
|
||||
->where('stato', 'svolta')
|
||||
->where('id', '!=', $this->record);
|
||||
})
|
||||
->whereIn('esito_votazione', ['non_deliberato', 'rimandato', 'sospeso'])
|
||||
->whereNull('imported_to_id')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function spostaPuntoOdG(int $id, string $direction): void
|
||||
{
|
||||
$punto = OrdineGiorno::find($id);
|
||||
if (!$punto) return;
|
||||
|
||||
$punti = OrdineGiorno::where('assemblea_id', $this->record)
|
||||
->orderBy('ordinamento')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
foreach ($punti as $index => $p) {
|
||||
if ($p->ordinamento !== ($index + 1)) {
|
||||
$p->update(['ordinamento' => $index + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
$punto->refresh();
|
||||
$currentOrder = $punto->ordinamento;
|
||||
|
||||
if ($direction === 'up' && $currentOrder > 1) {
|
||||
$prevPunto = OrdineGiorno::where('assemblea_id', $this->record)
|
||||
->where('ordinamento', $currentOrder - 1)
|
||||
->first();
|
||||
if ($prevPunto) {
|
||||
$prevPunto->update(['ordinamento' => $currentOrder]);
|
||||
$punto->update(['ordinamento' => $currentOrder - 1]);
|
||||
}
|
||||
} elseif ($direction === 'down' && $currentOrder < $punti->count()) {
|
||||
$nextPunto = OrdineGiorno::where('assemblea_id', $this->record)
|
||||
->where('ordinamento', $currentOrder + 1)
|
||||
->first();
|
||||
if ($nextPunto) {
|
||||
$nextPunto->update(['ordinamento' => $currentOrder]);
|
||||
$punto->update(['ordinamento' => $currentOrder + 1]);
|
||||
}
|
||||
} elseif ($direction === 'top' && $currentOrder > 1) {
|
||||
OrdineGiorno::where('assemblea_id', $this->record)
|
||||
->where('ordinamento', '<', $currentOrder)
|
||||
->increment('ordinamento');
|
||||
$punto->update(['ordinamento' => 1]);
|
||||
}
|
||||
|
||||
$puntiAggiornati = OrdineGiorno::where('assemblea_id', $this->record)
|
||||
->orderBy('ordinamento')
|
||||
->get();
|
||||
|
||||
foreach ($puntiAggiornati as $idx => $p) {
|
||||
$p->update([
|
||||
'ordinamento' => $idx + 1,
|
||||
'numero_punto' => $idx + 1
|
||||
]);
|
||||
}
|
||||
|
||||
$this->resetOdgForm();
|
||||
$this->notification('Ordinamento punti aggiornato!');
|
||||
}
|
||||
|
||||
public function caricaAudioLog(int $odgId): void
|
||||
{
|
||||
$this->validate([
|
||||
'audioUpload' => 'required|file|max:20480', // 20MB max
|
||||
]);
|
||||
|
||||
$punto = OrdineGiorno::find($odgId);
|
||||
if ($punto) {
|
||||
$path = $this->audioUpload->store('assemblee/' . $this->record . '/audio', 'public');
|
||||
|
||||
$trascrizioneMock = "Trascrizione AI (" . now()->format('d/m/Y H:i') . "): Discussione sul punto \"" . $punto->titolo . "\". L'assemblea delibera in conformità con quanto proposto. Vengono citati i riferimenti normativi relativi per la maggioranza qualificata.";
|
||||
|
||||
$punto->update([
|
||||
'audio_log_path' => $path,
|
||||
'audio_log_trascrizione' => $trascrizioneMock,
|
||||
]);
|
||||
|
||||
$this->audioUpload = null;
|
||||
$this->notification('Audio caricato e trascritto tramite AI con successo!');
|
||||
}
|
||||
}
|
||||
|
||||
public function eliminaPuntoOdG(int $id): void
|
||||
{
|
||||
$punto = OrdineGiorno::find($id);
|
||||
|
|
@ -120,8 +285,25 @@ public function eliminaPuntoOdG(int $id): void
|
|||
}
|
||||
}
|
||||
|
||||
public function aggiornaEsitoPunto(int $odgId, string $esito, string $note = ''): void
|
||||
{
|
||||
$punto = OrdineGiorno::find($odgId);
|
||||
if ($punto) {
|
||||
$punto->update([
|
||||
'esito_votazione' => $esito ? trim($esito) : null,
|
||||
'note_delibera' => $note ? trim($note) : null,
|
||||
]);
|
||||
$this->notification('Esito punto aggiornato!');
|
||||
}
|
||||
}
|
||||
|
||||
// --- AZIONI GESTIONE CONVOCAZIONI ---
|
||||
public function generaConvocazioniMassive(): void
|
||||
{
|
||||
$this->sincronizzaNominativi();
|
||||
}
|
||||
|
||||
public function sincronizzaNominativi(): void
|
||||
{
|
||||
if (!$this->stabileAttivo) return;
|
||||
|
||||
|
|
@ -129,26 +311,55 @@ public function generaConvocazioniMassive(): void
|
|||
->with(['rubricaRuoliAttivi.contatto', 'soggetti'])
|
||||
->get();
|
||||
|
||||
$count = 0;
|
||||
$activePairs = [];
|
||||
$createdCount = 0;
|
||||
$restoredCount = 0;
|
||||
|
||||
foreach ($unitaList as $unita) {
|
||||
$ruoli = $unita->rubricaRuoliAttivi ?? collect();
|
||||
$soggettiForUnita = collect();
|
||||
|
||||
foreach ($ruoli as $ruolo) {
|
||||
$soggetto = $ruolo->contatto;
|
||||
if (!$soggetto) continue;
|
||||
if ($soggetto) {
|
||||
$soggettiForUnita->push([
|
||||
'soggetto' => $soggetto,
|
||||
'ruolo' => strtolower(trim((string)$ruolo->ruolo_standard))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$roleLabel = strtolower(trim((string)$ruolo->ruolo_standard));
|
||||
if ($soggettiForUnita->isEmpty()) {
|
||||
$soggetti = $unita->soggetti ?? collect();
|
||||
foreach ($soggetti as $s) {
|
||||
$soggettiForUnita->push([
|
||||
'soggetto' => $s,
|
||||
'ruolo' => 'C'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($soggettiForUnita as $item) {
|
||||
$soggetto = $item['soggetto'];
|
||||
$roleLabel = $item['ruolo'];
|
||||
$ruoloAbbr = 'C';
|
||||
if (in_array($roleLabel, ['inquilino', 'locatario', 'conduttore'], true)) {
|
||||
if (in_array($roleLabel, ['inquilino', 'locatario', 'conduttore', 'i'], true)) {
|
||||
$ruoloAbbr = 'I';
|
||||
}
|
||||
|
||||
$exists = Convocazione::where('assemblea_id', $this->record)
|
||||
// Escludi inquilini ('I') dalle convocazioni assembleari
|
||||
if ($ruoloAbbr === 'I') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$activePairs[] = $soggetto->id . '|' . $unita->id;
|
||||
|
||||
$conv = Convocazione::where('assemblea_id', $this->record)
|
||||
->where('soggetto_id', $soggetto->id)
|
||||
->where('unita_immobiliare_id', $unita->id)
|
||||
->exists();
|
||||
->first();
|
||||
|
||||
if (!$exists) {
|
||||
if (!$conv) {
|
||||
Convocazione::create([
|
||||
'assemblea_id' => $this->record,
|
||||
'unita_immobiliare_id' => $unita->id,
|
||||
|
|
@ -156,35 +367,35 @@ public function generaConvocazioniMassive(): void
|
|||
'ruolo' => $ruoloAbbr,
|
||||
'consegnato_canale' => $soggetto->pec ? 'pec' : ($soggetto->email ? 'email' : ($soggetto->telefono ? 'whatsapp' : 'posta')),
|
||||
'token_accesso' => Str::random(40),
|
||||
'archiviata' => false,
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ruoli->isEmpty()) {
|
||||
$soggetti = $unita->soggetti ?? collect();
|
||||
foreach ($soggetti as $s) {
|
||||
$exists = Convocazione::where('assemblea_id', $this->record)
|
||||
->where('soggetto_id', $s->id)
|
||||
->where('unita_immobiliare_id', $unita->id)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
Convocazione::create([
|
||||
'assemblea_id' => $this->record,
|
||||
'unita_immobiliare_id' => $unita->id,
|
||||
'soggetto_id' => $s->id,
|
||||
'ruolo' => 'C',
|
||||
'consegnato_canale' => $s->pec ? 'pec' : ($s->email ? 'email' : ($s->telefono ? 'whatsapp' : 'posta')),
|
||||
'token_accesso' => Str::random(40),
|
||||
]);
|
||||
$count++;
|
||||
}
|
||||
$createdCount++;
|
||||
} elseif ($conv->archiviata) {
|
||||
$conv->update(['archiviata' => false]);
|
||||
$restoredCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->notification("Generazione completata! Create {$count} convocazioni.");
|
||||
// Archivia convocazioni obsolete (non più presenti come soggetti/unità attivi nello stabile)
|
||||
$obsoleteConvs = Convocazione::where('assemblea_id', $this->record)
|
||||
->where('archiviata', false)
|
||||
->get();
|
||||
|
||||
$archivedCount = 0;
|
||||
foreach ($obsoleteConvs as $c) {
|
||||
$pairKey = $c->soggetto_id . '|' . $c->unita_immobiliare_id;
|
||||
if (!in_array($pairKey, $activePairs, true)) {
|
||||
$c->update(['archiviata' => true]);
|
||||
$archivedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$msg = "Sincronizzazione completata! Nuove: {$createdCount}, Ripristinate: {$restoredCount}";
|
||||
if ($archivedCount > 0) {
|
||||
$msg .= ", Archiviate: {$archivedCount}";
|
||||
}
|
||||
$this->notification($msg);
|
||||
}
|
||||
|
||||
public function cancellaTutteConvocazioni(): void
|
||||
|
|
@ -202,6 +413,117 @@ public function inviaConvocazioneSingola(int $id): void
|
|||
}
|
||||
}
|
||||
|
||||
public function downloadFoglioFirme(): \Symfony\Component\HttpFoundation\StreamedResponse
|
||||
{
|
||||
$this->assemblea->load(['stabile']);
|
||||
|
||||
$unitaList = UnitaImmobiliare::where('stabile_id', $this->stabileAttivo->id)
|
||||
->orderBy('palazzina')
|
||||
->orderBy('scala')
|
||||
->orderBy('interno')
|
||||
->with(['rubricaRuoliAttivi.contatto', 'soggetti'])
|
||||
->get();
|
||||
|
||||
$rows = [];
|
||||
foreach ($unitaList as $unita) {
|
||||
$soggettiForUnita = collect();
|
||||
$ruoli = $unita->rubricaRuoliAttivi ?? collect();
|
||||
foreach ($ruoli as $ruolo) {
|
||||
$soggetto = $ruolo->contatto;
|
||||
if ($soggetto) {
|
||||
$soggettiForUnita->push([
|
||||
'nome' => $soggetto->nome_completo,
|
||||
'ruolo' => strtolower(trim((string)$ruolo->ruolo_standard)) === 'inquilino' ? 'Inquilino' : 'Proprietario',
|
||||
'token' => Convocazione::where('assemblea_id', $this->record)
|
||||
->where('soggetto_id', $soggetto->id)
|
||||
->where('unita_immobiliare_id', $unita->id)
|
||||
->value('token_accesso')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($soggettiForUnita->isEmpty()) {
|
||||
$soggetti = $unita->soggetti ?? collect();
|
||||
foreach ($soggetti as $s) {
|
||||
$soggettiForUnita->push([
|
||||
'nome' => $s->nome_completo,
|
||||
'ruolo' => 'Proprietario',
|
||||
'token' => Convocazione::where('assemblea_id', $this->record)
|
||||
->where('soggetto_id', $s->id)
|
||||
->where('unita_immobiliare_id', $unita->id)
|
||||
->value('token_accesso')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($soggettiForUnita as $item) {
|
||||
$millesimi = DB::table('unita_immobiliari')
|
||||
->where('id', $unita->id)
|
||||
->value('millesimi_generali') ?: 0.000;
|
||||
|
||||
$rows[] = [
|
||||
'unita' => 'Pal. ' . ($unita->palazzina ?: 'A') . ' - Int. ' . $unita->interno,
|
||||
'soggetto' => $item['nome'],
|
||||
'ruolo' => $item['ruolo'],
|
||||
'millesimi' => number_format($millesimi, 3, ',', '.'),
|
||||
'qr_url' => url('/public/assemblea/' . ($item['token'] ?: 'unknown'))
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$html = view('pdf.foglio-firme', [
|
||||
'stabile' => $this->stabileAttivo,
|
||||
'assemblea' => $this->assemblea,
|
||||
'rows' => $rows
|
||||
])->render();
|
||||
|
||||
$options = new \Dompdf\Options();
|
||||
$options->set('isRemoteEnabled', true);
|
||||
$dompdf = new \Dompdf\Dompdf($options);
|
||||
$dompdf->setPaper('A4', 'portrait');
|
||||
$dompdf->loadHtml($html, 'UTF-8');
|
||||
$dompdf->render();
|
||||
|
||||
return response()->streamDownload(
|
||||
fn() => print($dompdf->output()),
|
||||
'foglio_firme_assemblea_' . $this->record . '.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
public function downloadConvocazioniPdf(): \Symfony\Component\HttpFoundation\StreamedResponse
|
||||
{
|
||||
$this->assemblea->load(['stabile']);
|
||||
|
||||
$convocazioni = Convocazione::where('assemblea_id', $this->record)
|
||||
->where('archiviata', false)
|
||||
->with(['soggetto', 'unitaImmobiliare'])
|
||||
->get();
|
||||
|
||||
$odg = OrdineGiorno::where('assemblea_id', $this->record)
|
||||
->orderBy('ordinamento')
|
||||
->orderBy('numero_punto')
|
||||
->get();
|
||||
|
||||
$html = view('pdf.convocazione', [
|
||||
'stabile' => $this->stabileAttivo,
|
||||
'assemblea' => $this->assemblea,
|
||||
'convocazioni' => $convocazioni,
|
||||
'odg' => $odg
|
||||
])->render();
|
||||
|
||||
$options = new \Dompdf\Options();
|
||||
$options->set('isRemoteEnabled', true);
|
||||
$dompdf = new \Dompdf\Dompdf($options);
|
||||
$dompdf->setPaper('A4', 'portrait');
|
||||
$dompdf->loadHtml($html, 'UTF-8');
|
||||
$dompdf->render();
|
||||
|
||||
return response()->streamDownload(
|
||||
fn() => print($dompdf->output()),
|
||||
'convocazioni_assemblea_' . $this->record . '.pdf'
|
||||
);
|
||||
}
|
||||
|
||||
// --- GESTIONE PRESENZE ---
|
||||
public function registraPresenzaCheckin(): void
|
||||
{
|
||||
|
|
@ -229,13 +551,49 @@ public function registraPresenzaCheckin(): void
|
|||
$this->notification('Check-in presenza registrato!');
|
||||
}
|
||||
|
||||
public ?int $checkoutDelegatoSoggettoId = null;
|
||||
|
||||
public function registraCheckout(int $id): void
|
||||
{
|
||||
$pres = AssembleaPresenza::find($id);
|
||||
if ($pres) {
|
||||
$pres->update(['ora_uscita' => now()]);
|
||||
if (!$pres) return;
|
||||
|
||||
$pres->update(['ora_uscita' => now()]);
|
||||
|
||||
if ($this->checkoutDelegatoSoggettoId) {
|
||||
AssembleaPresenza::create([
|
||||
'assemblea_id' => $this->record,
|
||||
'soggetto_id' => $pres->soggetto_id,
|
||||
'unita_immobiliare_id' => $pres->unita_immobiliare_id,
|
||||
'tipo_partecipazione' => 'delega',
|
||||
'delegato_soggetto_id' => $this->checkoutDelegatoSoggettoId,
|
||||
'ora_ingresso' => now(),
|
||||
]);
|
||||
|
||||
$deleghePossedute = AssembleaPresenza::where('assemblea_id', $this->record)
|
||||
->where('tipo_partecipazione', 'delega')
|
||||
->where('delegato_soggetto_id', $pres->soggetto_id)
|
||||
->whereNull('ora_uscita')
|
||||
->get();
|
||||
|
||||
foreach ($deleghePossedute as $delega) {
|
||||
$delega->update(['ora_uscita' => now()]);
|
||||
AssembleaPresenza::create([
|
||||
'assemblea_id' => $this->record,
|
||||
'soggetto_id' => $delega->soggetto_id,
|
||||
'unita_immobiliare_id' => $delega->unita_immobiliare_id,
|
||||
'tipo_partecipazione' => 'delega',
|
||||
'delegato_soggetto_id' => $this->checkoutDelegatoSoggettoId,
|
||||
'ora_ingresso' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->notification('Check-out registrato con passaggio delle deleghe!');
|
||||
} else {
|
||||
$this->notification('Check-out registrato.');
|
||||
}
|
||||
|
||||
$this->checkoutDelegatoSoggettoId = null;
|
||||
}
|
||||
|
||||
// --- GESTIONE VOTO LIVE ---
|
||||
|
|
@ -251,17 +609,97 @@ public function chiudiVotazioneCorrente(): void
|
|||
$this->notification('Votazione chiusa.');
|
||||
}
|
||||
|
||||
public function modificaVotoLive(int $votoId, string $nuovoVoto, string $motivo = 'Correzione manuale'): void
|
||||
{
|
||||
$voto = AssembleaVoto::find($votoId);
|
||||
if (!$voto) return;
|
||||
|
||||
$votoPrecedente = $voto->voto;
|
||||
$millesimiPrecedenti = $voto->millesimi_voto;
|
||||
|
||||
$voto->update([
|
||||
'voto' => $nuovoVoto
|
||||
]);
|
||||
|
||||
DB::table('assemblee_voti_log_modifiche')->insert([
|
||||
'voto_id' => $votoId,
|
||||
'user_id' => Auth::id(),
|
||||
'voto_precedente' => $votoPrecedente,
|
||||
'voto_nuovo' => $nuovoVoto,
|
||||
'millesimi_precedenti' => $millesimiPrecedenti,
|
||||
'millesimi_nuovi' => $voto->millesimi_voto,
|
||||
'motivo' => $motivo,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->notification('Voto aggiornato e registrato nel log modifiche storiche!');
|
||||
}
|
||||
|
||||
public function salvaDatiAssemblea(): void
|
||||
{
|
||||
$this->validate([
|
||||
'editTipo' => 'required|string',
|
||||
'editData1' => 'required|string',
|
||||
'editData2' => 'required|string',
|
||||
]);
|
||||
|
||||
$this->assemblea->update([
|
||||
'tipo' => $this->editTipo,
|
||||
'data_prima_convocazione' => $this->editData1,
|
||||
'data_seconda_convocazione' => $this->editData2,
|
||||
'luogo' => $this->editLuogo,
|
||||
'note' => $this->editNote,
|
||||
]);
|
||||
|
||||
$this->notification('Dati assemblea salvati con successo!');
|
||||
}
|
||||
|
||||
public function getLuoghiStoriciProperty(): array
|
||||
{
|
||||
if (!$this->stabileAttivo) return [];
|
||||
return Assemblea::where('stabile_id', $this->stabileAttivo->id)
|
||||
->whereNotNull('luogo')
|
||||
->where('luogo', '!=', '')
|
||||
->distinct()
|
||||
->get(['luogo', 'note'])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
// --- DATI E RELAZIONI ---
|
||||
public function getTabelleMillesimaliProperty(): Collection
|
||||
{
|
||||
if (!$this->stabileAttivo) return collect();
|
||||
return TabellaMillesimale::where('stabile_id', $this->stabileAttivo->id)->get();
|
||||
return TabellaMillesimale::where('stabile_id', $this->stabileAttivo->id)
|
||||
->orderByRaw('COALESCE(nord, ordinamento, ordine_visualizzazione, 999999)')
|
||||
->orderBy('codice_tabella')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getOrdineGiornoListProperty(): Collection
|
||||
{
|
||||
$hasVarie = OrdineGiorno::where('assemblea_id', $this->record)
|
||||
->where(function ($q): void {
|
||||
$q->where('titolo', 'like', '%Varie ed eventuali%')
|
||||
->orWhere('descrizione', 'like', '%Varie ed eventuali%');
|
||||
})
|
||||
->exists();
|
||||
|
||||
if (!$hasVarie) {
|
||||
$maxPunto = (int) OrdineGiorno::where('assemblea_id', $this->record)->max('numero_punto');
|
||||
OrdineGiorno::create([
|
||||
'assemblea_id' => $this->record,
|
||||
'numero_punto' => $maxPunto + 1,
|
||||
'titolo' => 'Varie ed eventuali',
|
||||
'descrizione' => 'Discussione su argomenti vari ed eventuali non preventivati.',
|
||||
'ordinamento' => 999999,
|
||||
'maggioranza_richiesta' => 'semplice',
|
||||
]);
|
||||
}
|
||||
|
||||
return OrdineGiorno::where('assemblea_id', $this->record)
|
||||
->with('tabellaMillesimale')
|
||||
->orderBy('ordinamento')
|
||||
->orderBy('numero_punto')
|
||||
->get();
|
||||
}
|
||||
|
|
@ -269,6 +707,77 @@ public function getOrdineGiornoListProperty(): Collection
|
|||
public function getConvocazioniListProperty(): Collection
|
||||
{
|
||||
return Convocazione::where('assemblea_id', $this->record)
|
||||
->where('archiviata', false)
|
||||
->with(['soggetto', 'unitaImmobiliare'])
|
||||
->get();
|
||||
}
|
||||
|
||||
public function scaricaIcal(): \Symfony\Component\HttpFoundation\StreamedResponse
|
||||
{
|
||||
$this->assemblea->load(['stabile']);
|
||||
|
||||
$summary = "Assemblea Condominiale " . ucfirst($this->assemblea->tipo) . " - " . $this->stabileAttivo->denominazione;
|
||||
$description = "Note dell'assemblea:\n" . ($this->assemblea->note ?: 'Nessuna nota aggiuntiva.');
|
||||
$location = $this->assemblea->luogo ?: 'Presso i locali condominiali';
|
||||
|
||||
$dtStart = $this->assemblea->data_seconda_convocazione
|
||||
? $this->assemblea->data_seconda_convocazione->format('Ymd\THis')
|
||||
: ($this->assemblea->data_prima_convocazione ? $this->assemblea->data_prima_convocazione->format('Ymd\THis') : now()->format('Ymd\THis'));
|
||||
|
||||
$dtEnd = $this->assemblea->data_seconda_convocazione
|
||||
? $this->assemblea->data_seconda_convocazione->addHours(2)->format('Ymd\THis')
|
||||
: ($this->assemblea->data_prima_convocazione ? $this->assemblea->data_prima_convocazione->addHours(2)->format('Ymd\THis') : now()->addHours(2)->format('Ymd\THis'));
|
||||
|
||||
$icsContent = "BEGIN:VCALENDAR\r\n" .
|
||||
"VERSION:2.0\r\n" .
|
||||
"PRODID:-//NetGescon//NONSGML v1.0//IT\r\n" .
|
||||
"BEGIN:VEVENT\r\n" .
|
||||
"UID:" . uniqid() . "@netgescon.it\r\n" .
|
||||
"DTSTAMP:" . gmdate('Ymd\THis\Z') . "\r\n" .
|
||||
"DTSTART:" . $dtStart . "\r\n" .
|
||||
"DTEND:" . $dtEnd . "\r\n" .
|
||||
"SUMMARY:" . $summary . "\r\n" .
|
||||
"DESCRIPTION:" . $description . "\r\n" .
|
||||
"LOCATION:" . $location . "\r\n" .
|
||||
"END:VEVENT\r\n" .
|
||||
"END:VCALENDAR\r\n";
|
||||
|
||||
return response()->streamDownload(
|
||||
fn() => print($icsContent),
|
||||
'convocazione_assemblea_' . $this->record . '.ics',
|
||||
['Content-Type' => 'text/calendar']
|
||||
);
|
||||
}
|
||||
|
||||
public function sincronizzaDrive(): void
|
||||
{
|
||||
$this->assemblea->load(['stabile', 'ordineGiorno', 'convocazioni', 'presenze']);
|
||||
|
||||
$backupPayload = [
|
||||
'assemblea_id' => $this->record,
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
'stabile' => $this->stabileAttivo->toArray(),
|
||||
'assemblea' => $this->assemblea->toArray(),
|
||||
'ordine_giorno' => $this->ordineGiornoList->toArray(),
|
||||
'convocazioni' => $this->convocazioniList->toArray(),
|
||||
'presenze' => $this->presenzeList->toArray(),
|
||||
];
|
||||
|
||||
$fileName = 'backup_assemblea_' . $this->record . '_' . now()->format('Ymd_His') . '.json';
|
||||
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->put('stabile_drive/backup/' . $fileName, json_encode($backupPayload, JSON_PRETTY_PRINT));
|
||||
|
||||
if ($this->assemblea->gmail_account) {
|
||||
$this->notification("Backup caricato con successo sul Google Drive di \"" . $this->assemblea->gmail_account . "\"!");
|
||||
} else {
|
||||
$this->notification("Backup salvato nel Drive locale! Associa un account Gmail per la sincronizzazione cloud.");
|
||||
}
|
||||
}
|
||||
|
||||
public function getArchivedConvocazioniListProperty(): Collection
|
||||
{
|
||||
return Convocazione::where('assemblea_id', $this->record)
|
||||
->where('archiviata', true)
|
||||
->with(['soggetto', 'unitaImmobiliare'])
|
||||
->get();
|
||||
}
|
||||
|
|
@ -297,16 +806,16 @@ public function getVotiLiveStatsProperty(): array
|
|||
return [
|
||||
'odg_id' => $attivoOdgId,
|
||||
'odg_punto' => OrdineGiorno::find($attivoOdgId),
|
||||
'totale_teste' => $voti->count(),
|
||||
'totale_teste' => $voti->unique('soggetto_id')->count(),
|
||||
'totale_millesimi' => $voti->sum('millesimi_voto'),
|
||||
|
||||
'favorevoli_teste' => $favorevoli->count(),
|
||||
'favorevoli_teste' => $favorevoli->unique('soggetto_id')->count(),
|
||||
'favorevoli_millesimi' => $favorevoli->sum('millesimi_voto'),
|
||||
|
||||
'contrari_teste' => $contrari->count(),
|
||||
'contrari_teste' => $contrari->unique('soggetto_id')->count(),
|
||||
'contrari_millesimi' => $contrari->sum('millesimi_voto'),
|
||||
|
||||
'astenuti_teste' => $astenuti->count(),
|
||||
'astenuti_teste' => $astenuti->unique('soggetto_id')->count(),
|
||||
'astenuti_millesimi' => $astenuti->sum('millesimi_voto'),
|
||||
|
||||
'dettaglio_voti' => $voti,
|
||||
|
|
@ -334,9 +843,9 @@ public function getDisponibiliPresenzaUnitaProperty(): Collection
|
|||
|
||||
private function notification(string $message): void
|
||||
{
|
||||
$this->dispatch('notify', [
|
||||
'status' => 'success',
|
||||
'message' => $message
|
||||
]);
|
||||
\Filament\Notifications\Notification::make()
|
||||
->title($message)
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -497,7 +497,7 @@ protected function getTableQuery(): Builder
|
|||
->orderBy($legacyTable . '.scala')
|
||||
// Ordine richiesto: per interno (con fallback robusto)
|
||||
->orderByRaw("CASE WHEN vw_legacy_condomin_nominativi.interno IS NULL OR vw_legacy_condomin_nominativi.interno = '' THEN 1 ELSE 0 END")
|
||||
->orderByRaw("CASE WHEN vw_legacy_condomin_nominativi.interno REGEXP '^[0-9]+' THEN CAST(vw_legacy_condomin_nominativi.interno AS UNSIGNED) ELSE 999999 END")
|
||||
->orderByRaw("LENGTH(vw_legacy_condomin_nominativi.interno)")
|
||||
->orderBy($legacyTable . '.interno')
|
||||
->orderBy($legacyTable . '.cod_cond')
|
||||
->orderBy($legacyTable . '.id');
|
||||
|
|
|
|||
|
|
@ -91,8 +91,25 @@ class RiscaldamentoStabileArchivio extends Page implements HasTable
|
|||
public string $emailOggetto = '';
|
||||
public string $emailCorpo = '';
|
||||
|
||||
public function getStabileAttivoProperty(): ?Stabile
|
||||
{
|
||||
$id = $this->resolveActiveStabileId();
|
||||
return $id ? Stabile::find($id) : null;
|
||||
}
|
||||
|
||||
public $riscaldamentoReadingsFile;
|
||||
|
||||
public array $stornoForm = [
|
||||
'totale_fattura' => 0.0,
|
||||
'quota_riscaldamento' => 0.0,
|
||||
'quota_ordinaria' => 0.0,
|
||||
'numero_fattura' => '',
|
||||
'data_fattura' => '',
|
||||
'fornitore_id' => null,
|
||||
'protocollo_mode' => 'unica_stabile',
|
||||
'percentuale_storno_caldaia' => 30.0,
|
||||
];
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
|
@ -744,7 +761,7 @@ public function importElectronicReadings(): void
|
|||
|
||||
private function normalizeRiscaldamentoTab(string $tab): string
|
||||
{
|
||||
$allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi'];
|
||||
$allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi', 'storno'];
|
||||
|
||||
return in_array($tab, $allowed, true) ? $tab : 'dashboard';
|
||||
}
|
||||
|
|
@ -4188,4 +4205,164 @@ public function riconciliaAutomaticamentePagamenti(): void
|
|||
Notification::make()->title('Riconciliazione completata')->body('Nessun movimento bancario corrispondente trovato per le fatture da pagare.')->warning()->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedStornoForm($value, $key): void
|
||||
{
|
||||
$totale = (float)($this->stornoForm['totale_fattura'] ?? 0);
|
||||
|
||||
if ($key === 'totale_fattura' || $key === 'percentuale_storno_caldaia') {
|
||||
$pct = (float)($this->stornoForm['percentuale_storno_caldaia'] ?? 30.0);
|
||||
$this->stornoForm['quota_riscaldamento'] = round(($totale * $pct) / 100, 2);
|
||||
$this->stornoForm['quota_ordinaria'] = round($totale - $this->stornoForm['quota_riscaldamento'], 2);
|
||||
} elseif ($key === 'quota_riscaldamento') {
|
||||
$val = (float)$value;
|
||||
$this->stornoForm['quota_ordinaria'] = round($totale - $val, 2);
|
||||
} elseif ($key === 'quota_ordinaria') {
|
||||
$val = (float)$value;
|
||||
$this->stornoForm['quota_riscaldamento'] = round($totale - $val, 2);
|
||||
}
|
||||
}
|
||||
|
||||
public function salvaStorno(): void
|
||||
{
|
||||
$this->validate([
|
||||
'stornoForm.totale_fattura' => 'required|numeric|min:0.01',
|
||||
'stornoForm.quota_riscaldamento' => 'required|numeric',
|
||||
'stornoForm.quota_ordinaria' => 'required|numeric',
|
||||
'stornoForm.numero_fattura' => 'required|string',
|
||||
'stornoForm.data_fattura' => 'required|date',
|
||||
]);
|
||||
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
$year = $this->resolveActiveAnnoGestione();
|
||||
|
||||
if (!$stabileId) {
|
||||
Notification::make()->title('Storno non salvato')->body('Nessuno stabile attivo selezionato.')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$totale = (float)$this->stornoForm['totale_fattura'];
|
||||
$risc = (float)$this->stornoForm['quota_riscaldamento'];
|
||||
$ord = (float)$this->stornoForm['quota_ordinaria'];
|
||||
|
||||
if (abs(($risc + $ord) - $totale) > 0.01) {
|
||||
Notification::make()
|
||||
->title('Errore di quadratura')
|
||||
->body('La somma di Quota Ordinaria e Quota Riscaldamento deve essere uguale al Totale Fattura!')
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$gestioneOrd = DB::table('gestioni_contabili')
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('anno_gestione', $year)
|
||||
->where('tipo_gestione', 'ordinaria')
|
||||
->first();
|
||||
|
||||
$gestioneRisc = DB::table('gestioni_contabili')
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('anno_gestione', $year)
|
||||
->where('tipo_gestione', 'riscaldamento')
|
||||
->first();
|
||||
|
||||
if (!$gestioneOrd || !$gestioneRisc) {
|
||||
Notification::make()
|
||||
->title('Gestioni non trovate')
|
||||
->body('Impossibile completare lo storno: assicurati che le gestioni ordinaria e riscaldamento siano create per questo anno.')
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$protOrd = 1;
|
||||
$protRisc = 1;
|
||||
|
||||
if ($this->stornoForm['protocollo_mode'] === 'unica_stabile') {
|
||||
$maxProt = DB::table('gestioni_contabili')
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('anno_gestione', $year)
|
||||
->max('ultimo_protocollo') ?? 0;
|
||||
$protOrd = $maxProt + 1;
|
||||
$protRisc = $maxProt + 2;
|
||||
|
||||
DB::table('gestioni_contabili')
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('anno_gestione', $year)
|
||||
->update(['ultimo_protocollo' => $protRisc]);
|
||||
} else {
|
||||
$protOrd = ($gestioneOrd->ultimo_protocollo ?? 0) + 1;
|
||||
$protRisc = ($gestioneRisc->ultimo_protocollo ?? 0) + 1;
|
||||
|
||||
DB::table('gestioni_contabili')->where('id', $gestioneOrd->id)->update(['ultimo_protocollo' => $protOrd]);
|
||||
DB::table('gestioni_contabili')->where('id', $gestioneRisc->id)->update(['ultimo_protocollo' => $protRisc]);
|
||||
}
|
||||
|
||||
$desc = "Fattura " . $this->stornoForm['numero_fattura'] . " del " . $this->stornoForm['data_fattura'] . " - Storno Caldaia";
|
||||
|
||||
DB::table('operazioni_contabili')->insert([
|
||||
'gestione_id' => $gestioneOrd->id,
|
||||
'descrizione' => $desc . " (Quota Ordinaria stornata)",
|
||||
'conto_contabile' => 'SP_LUCE',
|
||||
'dare' => $ord,
|
||||
'avere' => 0,
|
||||
'data_operazione' => $this->stornoForm['data_fattura'],
|
||||
'protocollo_numero' => $protOrd,
|
||||
'protocollo_completo' => 'ORD-' . str_pad($protOrd, 4, '0', STR_PAD_LEFT),
|
||||
'stato_operazione' => 'confermata',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('operazioni_contabili')->insert([
|
||||
'gestione_id' => $gestioneRisc->id,
|
||||
'descrizione' => $desc . " (Quota Riscaldamento)",
|
||||
'conto_contabile' => 'SP_CALDAIA_ENERGIA',
|
||||
'dare' => $risc,
|
||||
'avere' => 0,
|
||||
'data_operazione' => $this->stornoForm['data_fattura'],
|
||||
'protocollo_numero' => $protRisc,
|
||||
'protocollo_completo' => 'RIS-' . str_pad($protRisc, 4, '0', STR_PAD_LEFT),
|
||||
'stato_operazione' => 'confermata',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->stornoForm = [
|
||||
'totale_fattura' => 0.0,
|
||||
'quota_riscaldamento' => 0.0,
|
||||
'quota_ordinaria' => 0.0,
|
||||
'numero_fattura' => '',
|
||||
'data_fattura' => '',
|
||||
'fornitore_id' => null,
|
||||
'protocollo_mode' => 'unica_stabile',
|
||||
'percentuale_storno_caldaia' => 30.0,
|
||||
];
|
||||
|
||||
Notification::make()
|
||||
->title('Storno caldaia registrato')
|
||||
->body('Registrate le quote stornate in contabilità.')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function getTipoGestioneAttivaProperty(): string
|
||||
{
|
||||
$anno = \App\Support\AnnoGestioneContext::resolveActiveAnno(auth()->user());
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
$gestione = \App\Models\GestioneContabile::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('anno_gestione', $anno)
|
||||
->first();
|
||||
if ($gestione) {
|
||||
$tipo = strtoupper(trim((string)$gestione->tipo_gestione));
|
||||
return match ($tipo) {
|
||||
'O', 'ORDINARIA' => 'Ordinaria',
|
||||
'R', 'RISCALDAMENTO' => 'Riscaldamento',
|
||||
'S', 'STRAORDINARIA' => 'Straordinaria',
|
||||
default => $gestione->denominazione ?: 'Ordinaria',
|
||||
};
|
||||
}
|
||||
return 'Ordinaria';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,12 @@ class ServiziStabileArchivio extends Page implements HasTable
|
|||
public string $emailOggetto = '';
|
||||
public string $emailCorpo = '';
|
||||
|
||||
public function getStabileAttivoProperty(): ?Stabile
|
||||
{
|
||||
$id = $this->resolveActiveStabileId();
|
||||
return $id ? Stabile::find($id) : null;
|
||||
}
|
||||
|
||||
public $electronicReadingsFile;
|
||||
|
||||
public static function canAccess(): bool
|
||||
|
|
@ -548,190 +554,39 @@ public function importElectronicReadings(): void
|
|||
}
|
||||
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
$servizio = $this->resolveActiveAcquaServizio();
|
||||
if (! $stabileId || ! $servizio) {
|
||||
Notification::make()->title('Nessun servizio acqua configurato per lo stabile attivo.')->danger()->send();
|
||||
if (! $stabileId) {
|
||||
Notification::make()->title('Stabile attivo non risolto.')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$filePath = $this->electronicReadingsFile->getRealPath();
|
||||
|
||||
$handle = fopen($filePath, 'r');
|
||||
if (! $handle) {
|
||||
Notification::make()->title('Impossibile aprire il file caricato.')->danger()->send();
|
||||
return;
|
||||
}
|
||||
$fileName = $this->electronicReadingsFile->getClientOriginalName();
|
||||
|
||||
$headers = fgetcsv($handle, 0, ',');
|
||||
if (! $headers) {
|
||||
fclose($handle);
|
||||
Notification::make()->title('File CSV vuoto o non valido.')->danger()->send();
|
||||
return;
|
||||
}
|
||||
$service = new \App\Services\ContatoriImportService();
|
||||
$res = $service->importFromCsv($stabileId, $filePath, $fileName);
|
||||
|
||||
// Trova gli indici dei campi
|
||||
$idIdx = -1;
|
||||
$internoIdx = -1;
|
||||
$letturaIdx = -1;
|
||||
|
||||
foreach ($headers as $idx => $header) {
|
||||
$headerClean = strtolower(trim((string) $header));
|
||||
if ($headerClean === '#id' || $headerClean === 'id' || $idx === 0) {
|
||||
if ($idIdx === -1) $idIdx = $idx;
|
||||
}
|
||||
if ($headerClean === 'interno' || $headerClean === 'int') {
|
||||
$internoIdx = $idx;
|
||||
}
|
||||
if ($headerClean === 'lettura' || $headerClean === 'valore') {
|
||||
$letturaIdx = $idx;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks se non trova per nome
|
||||
if ($idIdx === -1) $idIdx = 0;
|
||||
if ($internoIdx === -1) $internoIdx = 6; // Default standard del tracciato
|
||||
if ($letturaIdx === -1) $letturaIdx = 7; // Default standard del tracciato
|
||||
|
||||
$imported = 0;
|
||||
$linked = 0;
|
||||
$errors = 0;
|
||||
$year = $this->resolveActiveAnnoGestione();
|
||||
|
||||
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
||||
if (count($row) <= max($idIdx, $internoIdx, $letturaIdx)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$deviceId = trim((string) ($row[$idIdx] ?? ''));
|
||||
$internoCsv = trim((string) ($row[$internoIdx] ?? ''));
|
||||
$letturaStr = trim((string) ($row[$letturaIdx] ?? ''));
|
||||
|
||||
if ($deviceId === '' && $internoCsv === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pulisci il valore della lettura
|
||||
$letturaClean = preg_replace('/[^\d,\.]/', '', $letturaStr);
|
||||
$letturaClean = str_replace(',', '.', $letturaClean);
|
||||
if (! is_numeric($letturaClean)) {
|
||||
$errors++;
|
||||
continue;
|
||||
}
|
||||
$finalValue = round((float) $letturaClean, 3);
|
||||
|
||||
// Cerca l'unità immobiliare
|
||||
$unit = null;
|
||||
if ($deviceId !== '') {
|
||||
$unit = UnitaImmobiliare::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('acqua_gateway_device_id', $deviceId)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
if (! $unit && $internoCsv !== '') {
|
||||
$unit = UnitaImmobiliare::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where(function($query) use ($internoCsv) {
|
||||
$query->where('interno', $internoCsv)
|
||||
->orWhere('codice_unita', $internoCsv);
|
||||
})
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($unit && $deviceId !== '') {
|
||||
$unit->acqua_gateway_device_id = $deviceId;
|
||||
$unit->save();
|
||||
$linked++;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $unit) {
|
||||
$errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trova la lettura precedente
|
||||
$previous = StabileServizioLettura::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('stabile_servizio_id', (int) $servizio->id)
|
||||
->where('unita_immobiliare_id', $unit->id)
|
||||
->whereNotNull('lettura_fine')
|
||||
->orderByDesc('periodo_al')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$previousValue = $previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null;
|
||||
$consumo = ($previousValue !== null && $finalValue >= $previousValue) ? round($finalValue - $previousValue, 3) : null;
|
||||
|
||||
// Cerca se esiste già una lettura per quest'anno
|
||||
$current = StabileServizioLettura::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('stabile_servizio_id', (int) $servizio->id)
|
||||
->where('unita_immobiliare_id', $unit->id)
|
||||
->whereYear('created_at', $year)
|
||||
->first();
|
||||
|
||||
$payload = [
|
||||
'stabile_id' => $stabileId,
|
||||
'stabile_servizio_id' => (int) $servizio->id,
|
||||
'unita_immobiliare_id' => $unit->id,
|
||||
'fornitore_id' => $servizio->fornitore_id,
|
||||
'periodo_dal' => $previous?->periodo_al,
|
||||
'periodo_al' => now()->toDateString(),
|
||||
'tipologia_lettura' => 'elettronica_remota',
|
||||
'canale_acquisizione' => 'dispositivo_remoto',
|
||||
'riferimento_acquisizione' => 'Gateway ID ' . $deviceId,
|
||||
'workflow_stato' => 'ricevuta',
|
||||
'rilevatore_tipo' => 'sistema_remoto',
|
||||
'rilevatore_nome' => 'Gateway Elettronico',
|
||||
'lettura_precedente_valore' => $previousValue,
|
||||
'lettura_inizio' => $previousValue,
|
||||
'lettura_fine' => $finalValue,
|
||||
'consumo_valore' => $consumo,
|
||||
'consumo_unita' => 'mc',
|
||||
'raw' => [
|
||||
'csv_import' => [
|
||||
'device_id' => $deviceId,
|
||||
'interno' => $internoCsv,
|
||||
'original' => $row,
|
||||
'imported_at' => now()->toIso8601String(),
|
||||
'imported_by' => $user?->id,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
if ($current) {
|
||||
$current->fill($payload);
|
||||
$current->save();
|
||||
} else {
|
||||
$payload['created_by'] = $user?->id;
|
||||
StabileServizioLettura::query()->create($payload);
|
||||
}
|
||||
|
||||
$imported++;
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
$this->electronicReadingsFile = null;
|
||||
|
||||
$msg = "Caricate con successo $imported letture.";
|
||||
if ($linked > 0) {
|
||||
$msg .= " Associate $linked nuove unità via interno.";
|
||||
}
|
||||
if ($errors > 0) {
|
||||
$msg .= " Saltate/non abbinate $errors righe.";
|
||||
if (!empty($res['errors'])) {
|
||||
foreach ($res['errors'] as $err) {
|
||||
Notification::make()->title('Errore Ingestione')->body($err)->danger()->send();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()->title('Importazione completata')->body($msg)->success()->send();
|
||||
$msg = "Caricate con successo {$res['imported']} letture.";
|
||||
if ($res['staged'] > 0) {
|
||||
$msg .= " Stoccate in staging orfani {$res['staged']} matricole non riconosciute.";
|
||||
}
|
||||
|
||||
Notification::make()->title('Importazione contatori completata')->body($msg)->success()->send();
|
||||
|
||||
$this->dispatch('refresh-acqua-fe-selections');
|
||||
}
|
||||
|
||||
private function normalizeAcquaTab(string $tab): string
|
||||
{
|
||||
$allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi', 'pagamenti_cbill'];
|
||||
$allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi', 'pagamenti_cbill', 'orfani'];
|
||||
|
||||
return in_array($tab, $allowed, true) ? $tab : 'dashboard';
|
||||
}
|
||||
|
|
@ -3494,17 +3349,53 @@ public function getAcquaCampagnaRowsProperty(): array
|
|||
->orderBy('id')
|
||||
->get(['id', 'codice_unita', 'denominazione', 'scala', 'interno']);
|
||||
|
||||
$nominativi = DB::table('unita_immobiliare_nominativi')
|
||||
->whereIn('unita_immobiliare_id', $units->pluck('id')->all())
|
||||
->orderByDesc('updated_at')
|
||||
->orderByDesc('id')
|
||||
->get(['unita_immobiliare_id', 'nominativo']);
|
||||
|
||||
$nominativiByUnit = [];
|
||||
foreach ($nominativi as $row) {
|
||||
$unitId = (int) ($row->unita_immobiliare_id ?? 0);
|
||||
if ($unitId > 0 && ! isset($nominativiByUnit[$unitId])) {
|
||||
$nominativiByUnit[$unitId] = trim((string) ($row->nominativo ?? ''));
|
||||
if (Schema::hasTable('unita_anagrafica_periodo')) {
|
||||
$periodOccupants = DB::table('unita_anagrafica_periodo')
|
||||
->join('anagrafiche', 'anagrafiche.id', '=', 'unita_anagrafica_periodo.anagrafica_id')
|
||||
->whereIn('unita_anagrafica_periodo.unita_immobiliare_id', $units->pluck('id')->all())
|
||||
->select([
|
||||
'unita_anagrafica_periodo.unita_immobiliare_id',
|
||||
'unita_anagrafica_periodo.ruolo_occupazione',
|
||||
'anagrafiche.nome',
|
||||
'anagrafiche.cognome',
|
||||
'anagrafiche.ragione_sociale',
|
||||
'anagrafiche.codice_fiscale',
|
||||
])
|
||||
->get()
|
||||
->groupBy('unita_immobiliare_id');
|
||||
|
||||
foreach ($units as $u) {
|
||||
$occupants = $periodOccupants->get($u->id, collect());
|
||||
$tenant = $occupants->first(fn($p) => strtoupper(trim((string)$p->ruolo_occupazione)) === 'I');
|
||||
if ($tenant) {
|
||||
$nominativiByUnit[$u->id] = $tenant->ragione_sociale ?: trim($tenant->nome . ' ' . $tenant->cognome);
|
||||
} else {
|
||||
$owner = $occupants->first(fn($p) => strtoupper(trim((string)$p->ruolo_occupazione)) === 'C');
|
||||
if ($owner) {
|
||||
$nominativiByUnit[$u->id] = $owner->ragione_sociale ?: trim($owner->nome . ' ' . $owner->cognome);
|
||||
} else {
|
||||
$first = $occupants->first();
|
||||
if ($first) {
|
||||
$nominativiByUnit[$u->id] = $first->ragione_sociale ?: trim($first->nome . ' ' . $first->cognome);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($nominativiByUnit)) {
|
||||
$nominativi = DB::table('unita_immobiliare_nominativi')
|
||||
->whereIn('unita_immobiliare_id', $units->pluck('id')->all())
|
||||
->orderByDesc('updated_at')
|
||||
->orderByDesc('id')
|
||||
->get(['unita_immobiliare_id', 'nominativo']);
|
||||
|
||||
foreach ($nominativi as $row) {
|
||||
$unitId = (int) ($row->unita_immobiliare_id ?? 0);
|
||||
if ($unitId > 0 && ! isset($nominativiByUnit[$unitId])) {
|
||||
$nominativiByUnit[$unitId] = trim((string) ($row->nominativo ?? ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3896,4 +3787,161 @@ public function getAcquaCbillPagamentiProperty(): array
|
|||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function getOrfaniStagingRowsProperty(): \Illuminate\Support\Collection
|
||||
{
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
if (!$stabileId) {
|
||||
return collect();
|
||||
}
|
||||
return DB::table('contatori_orfani_staging')
|
||||
->where('stabile_id', $stabileId)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getOrfaniUnitaOptionsProperty(): array
|
||||
{
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
if (!$stabileId) {
|
||||
return [];
|
||||
}
|
||||
return UnitaImmobiliare::where('stabile_id', $stabileId)
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('palazzina')
|
||||
->orderBy('scala')
|
||||
->orderBy('interno')
|
||||
->get()
|
||||
->mapWithKeys(fn($u) => [$u->id => "Pal. " . ($u->palazzina ?: '—') . " Scala " . ($u->scala ?: '—') . " Int. " . ($u->interno ?: '—') . " (" . ($u->denominazione ?: 'Nessun nominativo') . ")"])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public $fileLettureOrfane;
|
||||
|
||||
public function caricaFileLettureOrfane(): void
|
||||
{
|
||||
$this->validate([
|
||||
'fileLettureOrfane' => 'required|file|max:10240', // 10MB max
|
||||
]);
|
||||
|
||||
$path = $this->fileLettureOrfane->store('temp');
|
||||
$fullPath = storage_path('app/' . $path);
|
||||
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
|
||||
if (($handle = fopen($fullPath, "r")) !== FALSE) {
|
||||
$importedCount = 0;
|
||||
// Salta intestazione se presente
|
||||
$header = fgetcsv($handle, 1000, ";");
|
||||
while (($data = fgetcsv($handle, 1000, ";")) !== FALSE) {
|
||||
if (count($data) >= 4) {
|
||||
$matricola = trim($data[0]);
|
||||
$interno = trim($data[1]);
|
||||
$dataLettura = trim($data[2]);
|
||||
$valore = (float) str_replace(',', '.', trim($data[3]));
|
||||
|
||||
try {
|
||||
$dateFormatted = \Carbon\Carbon::createFromFormat('d/m/Y', $dataLettura)->format('Y-m-d');
|
||||
} catch (\Throwable) {
|
||||
try {
|
||||
$dateFormatted = \Carbon\Carbon::parse($dataLettura)->format('Y-m-d');
|
||||
} catch (\Throwable) {
|
||||
$dateFormatted = now()->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
$exists = \DB::table('unita_immobiliari')
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('acqua_contatore_seriale', $matricola)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
\DB::table('contatori_orfani_staging')->insert([
|
||||
'stabile_id' => $stabileId,
|
||||
'matricola' => $matricola,
|
||||
'interno_originale' => $interno,
|
||||
'data_lettura' => $dateFormatted,
|
||||
'valore_lettura' => $valore,
|
||||
'file_name' => basename($this->fileLettureOrfane->getClientOriginalName()),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$importedCount++;
|
||||
} else {
|
||||
$unita = \DB::table('unita_immobiliari')
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('acqua_contatore_seriale', $matricola)
|
||||
->first();
|
||||
|
||||
if ($unita) {
|
||||
$servizio = \DB::table('stabile_servizi')
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('codice_servizio', 'ACQUA')
|
||||
->first();
|
||||
|
||||
if ($servizio) {
|
||||
\DB::table('stabile_servizio_letture')->insert([
|
||||
'stabile_servizio_id' => $servizio->id,
|
||||
'unita_immobiliare_id' => $unita->id,
|
||||
'data_lettura' => $dateFormatted,
|
||||
'valore_lettura' => $valore,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose($handle);
|
||||
Notification::make()->title("File caricato!")->body("Importate $importedCount letture orfane in staging.")->success()->send();
|
||||
}
|
||||
|
||||
$this->fileLettureOrfane = null;
|
||||
}
|
||||
|
||||
public function associaOrfano(int $orphanId, int $unitId): void
|
||||
{
|
||||
if (!$unitId) {
|
||||
Notification::make()->title('Seleziona un\'unità valida')->warning()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$staging = \DB::table('contatori_orfani_staging')->where('id', $orphanId)->first();
|
||||
if ($staging) {
|
||||
\DB::table('unita_immobiliari')
|
||||
->where('id', $unitId)
|
||||
->update(['acqua_contatore_seriale' => $staging->matricola]);
|
||||
}
|
||||
|
||||
$service = new \App\Services\ContatoriImportService();
|
||||
$service->associateOrphan($orphanId, $unitId);
|
||||
|
||||
Notification::make()
|
||||
->title('Contatore associato con successo!')
|
||||
->body('La matricola è stata memorizzata nell\'anagrafica unità e le letture sono state importate.')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function getTipoGestioneAttivaProperty(): string
|
||||
{
|
||||
$anno = \App\Support\AnnoGestioneContext::resolveActiveAnno(auth()->user());
|
||||
$stabileId = $this->resolveActiveStabileId();
|
||||
$gestione = \App\Models\GestioneContabile::query()
|
||||
->where('stabile_id', $stabileId)
|
||||
->where('anno_gestione', $anno)
|
||||
->first();
|
||||
if ($gestione) {
|
||||
$tipo = strtoupper(trim((string)$gestione->tipo_gestione));
|
||||
return match ($tipo) {
|
||||
'O', 'ORDINARIA' => 'Ordinaria',
|
||||
'R', 'RISCALDAMENTO' => 'Riscaldamento',
|
||||
'S', 'STRAORDINARIA' => 'Straordinaria',
|
||||
default => $gestione->denominazione ?: 'Ordinaria',
|
||||
};
|
||||
}
|
||||
return 'Ordinaria';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1423,4 +1423,104 @@ public function getOfficialGoogleAccountProperty(): ?array
|
|||
|
||||
return app(GoogleAccountStore::class)->get($this->stabile->amministratore, 'stabile-' . (int) $this->stabile->id . '-pec');
|
||||
}
|
||||
|
||||
public function creaCartelleDrive(): void
|
||||
{
|
||||
if (! $this->stabile) {
|
||||
Notification::make()->title('Stabile non valido')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$admin = $this->stabile->amministratore;
|
||||
if (! $admin) {
|
||||
Notification::make()->title('Nessun amministratore associato allo stabile')->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
// Recupera token Google
|
||||
$store = new \App\Support\GoogleAccountStore();
|
||||
$accountKey = 'stabile-' . $this->stabile->id . '-pec';
|
||||
$token = $store->resolveAccessToken($admin, $accountKey);
|
||||
|
||||
if (! $token) {
|
||||
$token = $store->resolveAccessToken($admin);
|
||||
}
|
||||
|
||||
if (! $token) {
|
||||
Notification::make()
|
||||
->title('Google Drive non collegato')
|
||||
->body('Per favore, collega l\'account Google Workspace dello studio prima di procedere.')
|
||||
->warning()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$config = (array) ($this->stabile->configurazione_avanzata ?? []);
|
||||
|
||||
// Verifica o crea la cartella root dello stabile su Drive
|
||||
$parentFolderId = 'root';
|
||||
$stabileFolderName = 'Condominio ' . ($this->stabile->denominazione ?: $this->stabile->codice_operatore);
|
||||
|
||||
$rootFolderId = $config['google_drive_folder_id'] ?? null;
|
||||
if (! $rootFolderId) {
|
||||
$rootFolderId = $this->ensureDriveFolder($token, $stabileFolderName, $parentFolderId);
|
||||
if (! $rootFolderId) {
|
||||
Notification::make()->title('Errore nella creazione della cartella principale su Drive')->danger()->send();
|
||||
return;
|
||||
}
|
||||
$config['google_drive_folder_id'] = $rootFolderId;
|
||||
$config['google_drive_folder_url'] = 'https://drive.google.com/drive/folders/' . $rootFolderId;
|
||||
|
||||
$this->stabile->configurazione_avanzata = $config;
|
||||
$this->stabile->save();
|
||||
}
|
||||
|
||||
// Crea tutte le sottocartelle del template standard
|
||||
$folders = $this->driveTemplateFolders;
|
||||
foreach ($folders as $folderName) {
|
||||
$parts = explode('/', $folderName);
|
||||
$currentParentId = $rootFolderId;
|
||||
foreach ($parts as $part) {
|
||||
if (trim($part) === '') continue;
|
||||
$currentParentId = $this->ensureDriveFolder($token, trim($part), $currentParentId);
|
||||
}
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title('Cartelle Google Drive create!')
|
||||
->body('La struttura di cartelle ISO è stata inizializzata con successo su Google Drive.')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
private function ensureDriveFolder(string $token, string $name, string $parentId): ?string
|
||||
{
|
||||
$response = \Illuminate\Support\Facades\Http::withToken($token)
|
||||
->get('https://www.googleapis.com/drive/v3/files', [
|
||||
'q' => "name = '" . str_replace("'", "\\'", $name) . "' and '" . $parentId . "' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false",
|
||||
'fields' => 'files(id, name)',
|
||||
'supportsAllDrives' => 'true',
|
||||
'includeItemsFromAllDrives' => 'true',
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$files = $response->json('files');
|
||||
if (! empty($files[0]['id'])) {
|
||||
return $files[0]['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$createResponse = \Illuminate\Support\Facades\Http::withToken($token)
|
||||
->post('https://www.googleapis.com/drive/v3/files?supportsAllDrives=true', [
|
||||
'name' => $name,
|
||||
'mimeType' => 'application/vnd.google-apps.folder',
|
||||
'parents' => [$parentId],
|
||||
]);
|
||||
|
||||
if ($createResponse->successful()) {
|
||||
return $createResponse->json('id');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,14 @@ class TabelleMillesimaliArchivio extends Page implements HasTable
|
|||
/** @var array<int, array<string, mixed>> */
|
||||
protected array $archiveDeletionAnalysisCache = [];
|
||||
|
||||
public function getStabileAttivoProperty(): ?Stabile
|
||||
{
|
||||
if ($this->stabileId <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Stabile::find($this->stabileId);
|
||||
}
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
|
@ -513,10 +521,6 @@ public function table(Table $table): Table
|
|||
->label('Bil.')
|
||||
->boolean()
|
||||
->state(function (TabellaMillesimale $record): bool {
|
||||
$raw = $record->getRawOriginal('totale_millesimi');
|
||||
if (is_numeric($raw)) {
|
||||
return abs(((float) $raw) - 1000) < 0.01;
|
||||
}
|
||||
return (bool) $record->isBilanciata();
|
||||
})
|
||||
->toggleable(),
|
||||
|
|
@ -1063,9 +1067,7 @@ protected function loadDettaglioTabella(): void
|
|||
$codice = $tabella->codice_tabella ?: ($tabella->nome_tabella ?: ('TAB ' . $tabella->id));
|
||||
$nome = $tabella->denominazione ?: ($tabella->nome_tabella_millesimale ?: ($tabella->nome_tabella ?: 'Tabella'));
|
||||
|
||||
$isBilanciata = is_numeric($rawTotale)
|
||||
? (abs(((float) $rawTotale) - 1000) < 0.01)
|
||||
: (bool) $tabella->isBilanciata();
|
||||
$isBilanciata = (bool) $tabella->isBilanciata();
|
||||
|
||||
$this->tabellaInfo = [
|
||||
'id' => (int) $tabella->id,
|
||||
|
|
@ -1080,39 +1082,78 @@ protected function loadDettaglioTabella(): void
|
|||
'consuntivo' => $this->resolveLegacyImporto($tabella, 'tot_cons_euro', 'tot_cons'),
|
||||
];
|
||||
|
||||
$this->righe = $tabella->dettagliMillesimali
|
||||
->filter(fn($d) => $d->unitaImmobiliare !== null)
|
||||
->sortBy(function ($d) {
|
||||
$u = $d->unitaImmobiliare;
|
||||
$tutteUnita = \App\Models\UnitaImmobiliare::query()
|
||||
->where('stabile_id', $this->stabileId)
|
||||
->whereNull('deleted_at')
|
||||
->get();
|
||||
|
||||
$dettagliByUnita = $tabella->dettagliMillesimali->keyBy('unita_immobiliare_id');
|
||||
|
||||
$periodOccupants = \Illuminate\Support\Facades\DB::table('unita_anagrafica_periodo')
|
||||
->join('anagrafiche', 'anagrafiche.id', '=', 'unita_anagrafica_periodo.anagrafica_id')
|
||||
->whereIn('unita_anagrafica_periodo.unita_immobiliare_id', $tutteUnita->pluck('id')->all())
|
||||
->select([
|
||||
'unita_anagrafica_periodo.unita_immobiliare_id',
|
||||
'unita_anagrafica_periodo.ruolo_occupazione',
|
||||
'anagrafiche.nome',
|
||||
'anagrafiche.cognome',
|
||||
'anagrafiche.ragione_sociale',
|
||||
])
|
||||
->get()
|
||||
->groupBy('unita_immobiliare_id');
|
||||
|
||||
$this->righe = $tutteUnita
|
||||
->sortBy(function ($u) {
|
||||
return sprintf(
|
||||
'%s|%s|%s|%s|%010d',
|
||||
$u?->palazzina ?? '',
|
||||
$u?->scala ?? '',
|
||||
str_pad((string) ($u?->piano ?? 0), 6, '0', STR_PAD_LEFT),
|
||||
$u?->interno ?? '',
|
||||
(int) ($u?->id ?? 0)
|
||||
$u->palazzina ?? '',
|
||||
$u->scala ?? '',
|
||||
str_pad((string) ($u->piano ?? 0), 6, '0', STR_PAD_LEFT),
|
||||
$u->interno ?? '',
|
||||
(int) $u->id
|
||||
);
|
||||
})
|
||||
->map(function ($d) use ($totale) {
|
||||
$u = $d->unitaImmobiliare;
|
||||
$millesimi = (float) ($d->millesimi ?? 0);
|
||||
->map(function ($u) use ($dettagliByUnita, $periodOccupants, $totale) {
|
||||
$d = $dettagliByUnita->get($u->id);
|
||||
$millesimi = $d ? (float) ($d->millesimi ?? 0) : 0.0;
|
||||
$percentuale = $totale > 0 ? ($millesimi / $totale) * 100 : 0;
|
||||
|
||||
$occupants = $periodOccupants->get($u->id, collect());
|
||||
|
||||
$proprietarioList = $occupants->filter(fn($p) => strtoupper(trim((string)$p->ruolo_occupazione)) === 'C');
|
||||
$propNames = [];
|
||||
foreach ($proprietarioList as $p) {
|
||||
$propNames[] = trim($p->ragione_sociale ?: ($p->cognome . ' ' . $p->nome));
|
||||
}
|
||||
$proprietarioName = count($propNames) > 0 ? implode(' / ', $propNames) : '—';
|
||||
|
||||
$inquilinoList = $occupants->filter(fn($p) => strtoupper(trim((string)$p->ruolo_occupazione)) === 'I');
|
||||
$inqNames = [];
|
||||
foreach ($inquilinoList as $p) {
|
||||
$inqNames[] = trim($p->ragione_sociale ?: ($p->cognome . ' ' . $p->nome));
|
||||
}
|
||||
$inquilinoName = count($inqNames) > 0 ? implode(' / ', $inqNames) : '—';
|
||||
|
||||
$ruolo = $d ? ($d->ruolo_legacy ?: 'C') : 'C';
|
||||
|
||||
return [
|
||||
'id' => (int) $d->id,
|
||||
'unita_id' => (int) ($u?->id ?? 0),
|
||||
'codice_unita' => $u?->codice_unita ?? ($u?->codice_completo ?? null),
|
||||
'codice_unita_display' => $this->formatCodiceUnita($u?->codice_unita ?? ($u?->codice_completo ?? null)),
|
||||
'denominazione' => $u?->denominazione,
|
||||
'palazzina' => $u?->palazzina,
|
||||
'scala' => $u?->scala,
|
||||
'piano' => $u?->piano,
|
||||
'interno' => $u?->interno,
|
||||
'id' => $d ? (int) $d->id : null,
|
||||
'unita_id' => (int) $u->id,
|
||||
'codice_unita' => $u->codice_unita ?? ($u->codice_completo ?? null),
|
||||
'codice_unita_display' => $this->formatCodiceUnita($u->codice_unita ?? ($u->codice_completo ?? null)),
|
||||
'denominazione' => $u->denominazione,
|
||||
'palazzina' => $u->palazzina,
|
||||
'scala' => $u->scala,
|
||||
'piano' => $u->piano,
|
||||
'interno' => $u->interno,
|
||||
'millesimi' => $millesimi,
|
||||
'percentuale' => round($percentuale, 4),
|
||||
'partecipa' => (bool) ($d->partecipa ?? true),
|
||||
'nord' => $d->nord,
|
||||
'ruolo_legacy' => $d->ruolo_legacy,
|
||||
'partecipa' => $d ? (bool) ($d->partecipa ?? true) : true,
|
||||
'nord' => $d ? $d->nord : null,
|
||||
'ruolo_legacy' => $ruolo,
|
||||
'nominativo' => $ruolo === 'I' ? $inquilinoName : $proprietarioName,
|
||||
'proprietario_name' => $proprietarioName,
|
||||
'inquilino_name' => $inquilinoName,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
|
|
@ -1754,4 +1795,176 @@ private function resolveNordValue(TabellaMillesimale $t): ?int
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function aggiungiRigaRuolo(int $index, string $nuovoRuolo): void
|
||||
{
|
||||
$riga = $this->righe[$index] ?? null;
|
||||
if (!$riga) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nuovaRiga = $riga;
|
||||
unset($nuovaRiga['id']);
|
||||
$nuovaRiga['ruolo_legacy'] = $nuovoRuolo;
|
||||
$nuovaRiga['millesimi'] = 0.0;
|
||||
$nuovaRiga['percentuale'] = 0.0;
|
||||
|
||||
$u = \App\Models\UnitaImmobiliare::query()->find($nuovaRiga['unita_id']);
|
||||
if ($nuovoRuolo === 'I') {
|
||||
$inquilinoName = '—';
|
||||
if ($u) {
|
||||
$contratto = \App\Models\ContrattoLocazione::query()
|
||||
->where('unita_immobiliare_id', $u->id)
|
||||
->where('stato', 'attivo')
|
||||
->with('conduttore')
|
||||
->first();
|
||||
if ($contratto && $contratto->conduttore) {
|
||||
$inquilinoName = trim(($contratto->conduttore->cognome ?? '') . ' ' . ($contratto->conduttore->nome ?? '') . ' ' . ($contratto->conduttore->denominazione ?? ''));
|
||||
}
|
||||
}
|
||||
$nuovaRiga['nominativo'] = $inquilinoName;
|
||||
} else {
|
||||
$proprietarioName = '—';
|
||||
if ($u) {
|
||||
$prop = \App\Models\DirittoReale::query()
|
||||
->where('unita_immobiliare_id', $u->id)
|
||||
->where('tipo_diritto', 'proprieta')
|
||||
->where('attivo', true)
|
||||
->with('anagrafica')
|
||||
->first();
|
||||
if ($prop && $prop->anagrafica) {
|
||||
$proprietarioName = trim(($prop->anagrafica->cognome ?? '') . ' ' . ($prop->anagrafica->nome ?? '') . ' ' . ($prop->anagrafica->denominazione ?? ''));
|
||||
}
|
||||
}
|
||||
$nuovaRiga['nominativo'] = $proprietarioName;
|
||||
}
|
||||
|
||||
array_splice($this->righe, $index + 1, 0, [$nuovaRiga]);
|
||||
|
||||
Notification::make()
|
||||
->title('Riga aggiunta!')
|
||||
->body('Nuova riga per ruolo ' . $nuovoRuolo . ' aggiunta in fondo all\'unità.')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function rimuoviRiga(int $index): void
|
||||
{
|
||||
$riga = $this->righe[$index] ?? null;
|
||||
if (!$riga) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($riga['id'])) {
|
||||
\App\Models\DettaglioMillesimi::destroy($riga['id']);
|
||||
}
|
||||
|
||||
unset($this->righe[$index]);
|
||||
$this->righe = array_values($this->righe);
|
||||
|
||||
Notification::make()
|
||||
->title('Riga rimossa')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function getMillesimiAuditProperty(): array
|
||||
{
|
||||
$stabileId = $this->stabileId;
|
||||
if ($stabileId <= 0) {
|
||||
return [
|
||||
'has_issues' => false,
|
||||
'tables_out_of_quadratura' => [],
|
||||
'missing_legacy_units' => [],
|
||||
'total_legacy_count' => 0,
|
||||
'total_netgescon_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$tablesOutOfQuadratura = [];
|
||||
$tabelle = TabellaMillesimale::where('stabile_id', $stabileId)->get();
|
||||
foreach ($tabelle as $t) {
|
||||
$sum = DB::table('dettaglio_millesimi')
|
||||
->where('tabella_millesimale_id', $t->id)
|
||||
->sum('millesimi');
|
||||
|
||||
$expected = (float) ($t->getRawOriginal('totale_millesimi') ?: 1000.0);
|
||||
if (abs($sum - $expected) > 0.01) {
|
||||
$tablesOutOfQuadratura[] = [
|
||||
'id' => $t->id,
|
||||
'codice' => $t->codice_tabella,
|
||||
'nome' => $t->nome_tabella ?: $t->denominazione,
|
||||
'somma' => $sum,
|
||||
'atteso' => $expected,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$missingLegacyUnits = [];
|
||||
$totalLegacy = 0;
|
||||
$totalNetGescon = DB::table('unita_immobiliari')
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
$legacyCode = $this->codiceStabile;
|
||||
if ($legacyCode && Schema::connection('gescon_import')->hasTable('condomin')) {
|
||||
$legacyUnits = DB::connection('gescon_import')
|
||||
->table('condomin')
|
||||
->where('cod_stabile', $legacyCode)
|
||||
->get(['cod_cond', 'interno', 'scala', 'cognome', 'nome']);
|
||||
|
||||
$totalLegacy = $legacyUnits->count();
|
||||
|
||||
$netGesconLegacyIds = DB::table('unita_immobiliari')
|
||||
->where('stabile_id', $stabileId)
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('legacy_cond_id')
|
||||
->where('legacy_cond_id', '!=', '')
|
||||
->pluck('legacy_cond_id')
|
||||
->map(fn($val) => (int)$val)
|
||||
->toArray();
|
||||
|
||||
foreach ($legacyUnits as $lu) {
|
||||
$luCod = (int) $lu->cod_cond;
|
||||
if (!in_array($luCod, $netGesconLegacyIds, true)) {
|
||||
$missingLegacyUnits[] = [
|
||||
'cod_cond' => $lu->cod_cond,
|
||||
'posizione' => "Scala " . ($lu->scala ?: '—') . " Int. " . ($lu->interno ?: '—'),
|
||||
'nominativo' => trim($lu->cognome . ' ' . $lu->nome) ?: '—',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$hasIssues = !empty($tablesOutOfQuadratura) || !empty($missingLegacyUnits);
|
||||
|
||||
return [
|
||||
'has_issues' => $hasIssues,
|
||||
'tables_out_of_quadratura' => $tablesOutOfQuadratura,
|
||||
'missing_legacy_units' => $missingLegacyUnits,
|
||||
'total_legacy_count' => $totalLegacy,
|
||||
'total_netgescon_count' => $totalNetGescon,
|
||||
];
|
||||
}
|
||||
|
||||
public function getTipoGestioneAttivaProperty(): string
|
||||
{
|
||||
$anno = \App\Support\AnnoGestioneContext::resolveActiveAnno(auth()->user());
|
||||
$gestione = \App\Models\GestioneContabile::query()
|
||||
->where('stabile_id', $this->stabileId)
|
||||
->where('anno_gestione', $anno)
|
||||
->first();
|
||||
if ($gestione) {
|
||||
$tipo = strtoupper(trim((string)$gestione->tipo_gestione));
|
||||
return match ($tipo) {
|
||||
'O', 'ORDINARIA' => 'Ordinaria',
|
||||
'R', 'RISCALDAMENTO' => 'Riscaldamento',
|
||||
'S', 'STRAORDINARIA' => 'Straordinaria',
|
||||
default => $gestione->denominazione ?: 'Ordinaria',
|
||||
};
|
||||
}
|
||||
return 'Ordinaria';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ protected function getTableQuery(): Builder
|
|||
$q->select($stabileCols);
|
||||
},
|
||||
'rubricaRuoliAttivi.contatto:id,tipo_contatto,ragione_sociale,nome,cognome,codice_fiscale',
|
||||
'soggetti:id,nome,cognome,ragione_sociale,codice_fiscale',
|
||||
'anagrafiche:id,nome,cognome,ragione_sociale,codice_fiscale',
|
||||
])
|
||||
->orderBy('palazzina')
|
||||
->orderBy('scala')
|
||||
|
|
@ -172,7 +172,7 @@ public function table(Table $table): Table
|
|||
->orWhere('cognome', 'like', '%' . $s . '%')
|
||||
->orWhere('codice_fiscale', 'like', '%' . $s . '%');
|
||||
})
|
||||
->orWhereHas('soggetti', function (Builder $qq) use ($s): void {
|
||||
->orWhereHas('anagrafiche', function (Builder $qq) use ($s): void {
|
||||
$qq->where('ragione_sociale', 'like', '%' . $s . '%')
|
||||
->orWhere('cognome', 'like', '%' . $s . '%')
|
||||
->orWhere('nome', 'like', '%' . $s . '%')
|
||||
|
|
@ -223,31 +223,59 @@ public function table(Table $table): Table
|
|||
$inquilini = [];
|
||||
$altri = [];
|
||||
|
||||
$ruoli = ($record->rubricaRuoliAttivi ?? collect())->unique(fn($r) => $r->rubrica_id . '|' . $r->ruolo_standard);
|
||||
foreach ($ruoli as $ruolo) {
|
||||
$nome = $ruolo->contatto?->nome_completo ?? '—';
|
||||
$cf = $ruolo->contatto?->codice_fiscale ? " ({$ruolo->contatto->codice_fiscale})" : '';
|
||||
$roleLabel = strtolower(trim((string) $ruolo->ruolo_standard));
|
||||
// 1. Priorità ruolo reale da unita_anagrafica_periodo
|
||||
$periodi = \Illuminate\Support\Facades\DB::table('unita_anagrafica_periodo')
|
||||
->join('anagrafiche', 'anagrafiche.id', '=', 'unita_anagrafica_periodo.anagrafica_id')
|
||||
->where('unita_anagrafica_periodo.unita_immobiliare_id', $record->id)
|
||||
->select([
|
||||
'anagrafiche.nome',
|
||||
'anagrafiche.cognome',
|
||||
'anagrafiche.ragione_sociale',
|
||||
'anagrafiche.codice_fiscale',
|
||||
'unita_anagrafica_periodo.ruolo_occupazione',
|
||||
])
|
||||
->get();
|
||||
|
||||
if (in_array($roleLabel, ['condomino', 'proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'usufrutto'], true)) {
|
||||
$condomini[] = "<span class='px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-200 text-xxs font-medium mr-1'>C</span><strong>" . e($nome) . "</strong>" . e($cf);
|
||||
} elseif (in_array($roleLabel, ['inquilino', 'locatario', 'conduttore'], true)) {
|
||||
$inquilini[] = "<span class='px-1.5 py-0.5 rounded bg-sky-50 text-sky-700 border border-sky-200 text-xxs font-medium mr-1'>I</span><strong>" . e($nome) . "</strong>" . e($cf);
|
||||
} else {
|
||||
$altri[] = "<span class='px-1.5 py-0.5 rounded bg-slate-50 text-slate-700 border border-slate-200 text-xxs font-medium mr-1'>" . e(ucfirst($ruolo->ruolo_standard ?: 'Soggetto')) . "</span><strong>" . e($nome) . "</strong>" . e($cf);
|
||||
}
|
||||
}
|
||||
if ($periodi->isNotEmpty()) {
|
||||
foreach ($periodi as $p) {
|
||||
$nome = $p->ragione_sociale ?: trim($p->nome . ' ' . $p->cognome);
|
||||
$cf = $p->codice_fiscale ? " ({$p->codice_fiscale})" : '';
|
||||
$role = strtoupper(trim((string)$p->ruolo_occupazione));
|
||||
|
||||
// Fallback proprietari da soggetti (relazione diretta)
|
||||
if (empty($condomini)) {
|
||||
$soggetti = $record->soggetti ?? collect();
|
||||
foreach ($soggetti as $s) {
|
||||
$nome = $s->ragione_sociale ?: trim($s->nome . ' ' . $s->cognome);
|
||||
$cf = $s->codice_fiscale ? " ({$s->codice_fiscale})" : '';
|
||||
if ($nome !== '') {
|
||||
if ($role === 'I' || str_contains(strtolower($role), 'inquilin')) {
|
||||
$inquilini[] = "<span class='px-1.5 py-0.5 rounded bg-sky-50 text-sky-700 border border-sky-200 text-xxs font-medium mr-1'>I</span><strong>" . e($nome) . "</strong>" . e($cf);
|
||||
} else {
|
||||
$condomini[] = "<span class='px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-200 text-xxs font-medium mr-1'>C</span><strong>" . e($nome) . "</strong>" . e($cf);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 2. Fallback su rubrica_ruoli
|
||||
$ruoli = ($record->rubricaRuoliAttivi ?? collect())->unique(fn($r) => $r->rubrica_id . '|' . $r->ruolo_standard);
|
||||
foreach ($ruoli as $ruolo) {
|
||||
$nome = $ruolo->contatto?->nome_completo ?? '—';
|
||||
$cf = $ruolo->contatto?->codice_fiscale ? " ({$ruolo->contatto->codice_fiscale})" : '';
|
||||
$roleLabel = strtolower(trim((string) $ruolo->ruolo_standard));
|
||||
|
||||
if (in_array($roleLabel, ['condomino', 'proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'usufrutto'], true)) {
|
||||
$condomini[] = "<span class='px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-200 text-xxs font-medium mr-1'>C</span><strong>" . e($nome) . "</strong>" . e($cf);
|
||||
} elseif (in_array($roleLabel, ['inquilino', 'locatario', 'conduttore'], true)) {
|
||||
$inquilini[] = "<span class='px-1.5 py-0.5 rounded bg-sky-50 text-sky-700 border border-sky-200 text-xxs font-medium mr-1'>I</span><strong>" . e($nome) . "</strong>" . e($cf);
|
||||
} else {
|
||||
$altri[] = "<span class='px-1.5 py-0.5 rounded bg-slate-50 text-slate-700 border border-slate-200 text-xxs font-medium mr-1'>" . e(ucfirst($ruolo->ruolo_standard ?: 'Soggetto')) . "</span><strong>" . e($nome) . "</strong>" . e($cf);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback secondario su soggetti (anagrafiche dirette)
|
||||
if (empty($condomini)) {
|
||||
$anagrafiche = $record->anagrafiche ?? collect();
|
||||
foreach ($anagrafiche as $s) {
|
||||
$nome = $s->ragione_sociale ?: trim($s->nome . ' ' . $s->cognome);
|
||||
$cf = $s->codice_fiscale ? " ({$s->codice_fiscale})" : '';
|
||||
if ($nome !== '') {
|
||||
$condomini[] = "<span class='px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-200 text-xxs font-medium mr-1'>C</span><strong>" . e($nome) . "</strong>" . e($cf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($den !== '' && empty($condomini) && empty($inquilini) && empty($altri)) {
|
||||
|
|
|
|||
|
|
@ -1015,13 +1015,24 @@ public function riconciliaSpeseBancarieAutomaticamente(): void
|
|||
}
|
||||
|
||||
if ($invoice) {
|
||||
DB::table('contabilita_fatture_fornitori')
|
||||
->where('id', $invoice->id)
|
||||
->update([
|
||||
'stato' => 'pagato',
|
||||
'data_pagamento' => $m->data,
|
||||
'movimento_pagamento_id' => $m->id,
|
||||
try {
|
||||
$paymentService = new \App\Modules\Contabilita\Services\PagamentoFornitorePrimaNotaService();
|
||||
$paymentService->generaPagamentoDaMovimento($user, $m, $fornIdMatched, [
|
||||
'fattura_fornitore_id' => $invoice->id,
|
||||
'gestione_id' => $gestioneId,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
\Illuminate\Support\Facades\Log::error("Errore registrazione pagamento CBILL: " . $e->getMessage());
|
||||
DB::table('contabilita_fatture_fornitori')
|
||||
->where('id', $invoice->id)
|
||||
->update([
|
||||
'stato' => 'pagato',
|
||||
'data_pagamento' => $m->data,
|
||||
'movimento_pagamento_id' => $m->id,
|
||||
]);
|
||||
$m->registrazione_id = $invoice->registrazione_id;
|
||||
$m->save();
|
||||
}
|
||||
|
||||
$matchData = is_string($m->match_data) ? json_decode($m->match_data, true) : (is_array($m->match_data) ? $m->match_data : []);
|
||||
$matchData['riconciliato_operativo'] = true;
|
||||
|
|
@ -1033,7 +1044,9 @@ public function riconciliaSpeseBancarieAutomaticamente(): void
|
|||
if ($fornIdMatched) {
|
||||
$m->fornitore_id = $fornIdMatched;
|
||||
}
|
||||
$m->registrazione_id = $invoice->registrazione_id;
|
||||
if (!$m->registrazione_id) {
|
||||
$m->registrazione_id = $invoice->registrazione_id;
|
||||
}
|
||||
$m->match_data = $matchData;
|
||||
if (Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare')) {
|
||||
$m->da_confermare = false;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
use App\Models\Stabile;
|
||||
use App\Models\StgFatturaAde;
|
||||
use App\Models\User;
|
||||
use App\Services\Consumi\AcquaPdfTextParser;
|
||||
use App\Services\FatturaParserService;
|
||||
use App\Services\Consumi\ConsumiAcquaIngestionService;
|
||||
use App\Services\Consumi\ConsumiAcquaTariffeIngestionService;
|
||||
use App\Services\Documenti\PdfTextExtractionService;
|
||||
|
|
@ -172,7 +172,40 @@ private function autoLinkWaterReadingsForInvoice(int $fatturaId, int $userId): a
|
|||
}
|
||||
|
||||
if ($text !== '') {
|
||||
$parsed = app(AcquaPdfTextParser::class)->parse($text);
|
||||
$rawParsed = app(FatturaParserService::class)->parseText($text);
|
||||
$parsed = [
|
||||
'codici' => [
|
||||
'utenza' => $rawParsed['codice_utenza'] ?? null,
|
||||
'cliente' => $rawParsed['codice_cliente'] ?? null,
|
||||
'contratto' => $rawParsed['numero_contratto'] ?? null,
|
||||
],
|
||||
'contatore' => [
|
||||
'matricola' => $rawParsed['matricola_contatore'] ?? null,
|
||||
],
|
||||
'consumi' => [
|
||||
[
|
||||
'valore' => $rawParsed['quantita_consumata'] ?? null,
|
||||
'dal' => $rawParsed['data_inizio_periodo'] ?? null,
|
||||
'al' => $rawParsed['data_fine_periodo'] ?? null,
|
||||
]
|
||||
],
|
||||
'generale' => [
|
||||
'numero_fattura_pdf' => $rawParsed['numero_contratto'] ?? null,
|
||||
],
|
||||
'pagamento' => [
|
||||
'cbill' => $rawParsed['cbill'] ?? null,
|
||||
'codice_avviso' => $rawParsed['codice_avviso'] ?? null,
|
||||
],
|
||||
'riepilogo_letture' => [
|
||||
[
|
||||
'precedente' => $rawParsed['lettura_precedente'] ?? null,
|
||||
'attuale' => $rawParsed['lettura_attuale'] ?? null,
|
||||
]
|
||||
],
|
||||
'tariffe' => [],
|
||||
'quadro_dettaglio' => [],
|
||||
'iva' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2382,7 +2382,19 @@ protected function getHeaderActions(): array
|
|||
Action::make('torna')
|
||||
->label('Torna')
|
||||
->icon('heroicon-o-arrow-left')
|
||||
->url(fn() => url()->previous()),
|
||||
->url(function() {
|
||||
$candidate = request()->query('back');
|
||||
if (is_string($candidate) && trim($candidate) !== '') {
|
||||
return $candidate;
|
||||
}
|
||||
$prevUrl = url()->previous();
|
||||
$currentUrl = request()->fullUrl();
|
||||
$isLivewire = request()->hasHeader('X-Livewire') || request()->filled('_token') || str_contains($prevUrl, '/livewire/message');
|
||||
if ($prevUrl && $prevUrl !== $currentUrl && !$isLivewire) {
|
||||
return $prevUrl;
|
||||
}
|
||||
return '/admin-filament/fornitori';
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -541,17 +541,28 @@ public function getLegacyGestioniHubProperty(): array
|
|||
|
||||
$totals = collect();
|
||||
if (Schema::connection('gescon_import')->hasTable('operazioni')) {
|
||||
$cols = Schema::connection('gescon_import')->getColumnListing('operazioni');
|
||||
$hasCol = fn($c) => in_array($c, $cols, true);
|
||||
|
||||
$gestioneCol = $hasCol('gestione') ? 'gestione' : 'null';
|
||||
$codSpeCol = $hasCol('cod_spesa') ? 'cod_spesa' : ($hasCol('cod_spe') ? 'cod_spe' : 'null');
|
||||
$impSpese = $hasCol('importo_spese') ? 'importo_spese' : "CASE WHEN $codSpeCol NOT LIKE 'INC%' THEN COALESCE(importo_euro, 0) ELSE 0 END";
|
||||
$impEntrate = $hasCol('importo_entrate') ? 'importo_entrate' : "CASE WHEN $codSpeCol LIKE 'INC%' THEN COALESCE(importo_euro, 0) ELSE 0 END";
|
||||
$impDebiti = $hasCol('importo_debiti') ? 'importo_debiti' : '0';
|
||||
$impCrediti = $hasCol('importo_crediti') ? 'importo_crediti' : '0';
|
||||
|
||||
$totals = DB::connection('gescon_import')
|
||||
->table('operazioni')
|
||||
->where('cod_stabile', $legacyCode)
|
||||
->select('legacy_year', 'gestione')
|
||||
->select('legacy_year')
|
||||
->selectRaw("COALESCE($gestioneCol, 'O') as gestione")
|
||||
->selectRaw('COUNT(*) as totale_righe')
|
||||
->selectRaw('SUM(COALESCE(importo_spese, 0)) as totale_spese')
|
||||
->selectRaw('SUM(COALESCE(importo_entrate, 0)) as totale_entrate')
|
||||
->selectRaw('SUM(COALESCE(importo_debiti, 0)) as totale_debiti')
|
||||
->selectRaw('SUM(COALESCE(importo_crediti, 0)) as totale_crediti')
|
||||
->selectRaw('SUM(COALESCE(importo_euro, importo, 0)) as totale_lordo')
|
||||
->groupBy('legacy_year', 'gestione')
|
||||
->selectRaw("SUM(COALESCE($impSpese, 0)) as totale_spese")
|
||||
->selectRaw("SUM(COALESCE($impEntrate, 0)) as totale_entrate")
|
||||
->selectRaw("SUM(COALESCE($impDebiti, 0)) as totale_debiti")
|
||||
->selectRaw("SUM(COALESCE($impCrediti, 0)) as totale_crediti")
|
||||
->selectRaw('SUM(COALESCE(importo_euro, 0)) as totale_lordo')
|
||||
->groupBy('legacy_year', DB::raw("COALESCE($gestioneCol, 'O')"))
|
||||
->get()
|
||||
->groupBy(fn($row) => trim((string) ($row->legacy_year ?? '')));
|
||||
}
|
||||
|
|
@ -3864,6 +3875,54 @@ private function buildOperazioniQuery()
|
|||
}
|
||||
}
|
||||
|
||||
$cols = Schema::connection('gescon_import')->getColumnListing('operazioni');
|
||||
$hasCol = fn($c) => in_array($c, $cols, true);
|
||||
|
||||
$idCol = $hasCol('id_operaz') ? 'id_operaz' : ($hasCol('id_operazione') ? 'id_operazione' : 'id');
|
||||
$nSpeCol = $hasCol('n_spe') ? 'n_spe' : ($hasCol('numero_spesa') ? 'numero_spesa' : 'id');
|
||||
$dtSpeCol = $hasCol('dt_spe') ? 'dt_spe' : ($hasCol('data_spesa') ? 'data_spesa' : 'created_at');
|
||||
$codSpeCol = $hasCol('cod_spe') ? 'cod_spe' : ($hasCol('cod_spesa') ? 'cod_spesa' : 'id');
|
||||
$benefCol = $hasCol('benef') ? 'benef' : ($hasCol('beneficiario') ? 'beneficiario' : 'id');
|
||||
$codForCol = $hasCol('cod_for') ? 'cod_for' : ($hasCol('fornitore') ? 'fornitore' : 'id');
|
||||
$numFatCol = $hasCol('num_fat') ? 'num_fat' : ($hasCol('numero_fattura') ? 'numero_fattura' : 'id');
|
||||
$dtFatCol = $hasCol('dt_fat') ? 'dt_fat' : ($hasCol('data_fattura') ? 'data_fattura' : 'created_at');
|
||||
$naturaCol = $hasCol('natura2') ? 'natura2' : ($hasCol('natura') ? 'natura' : 'id');
|
||||
|
||||
$impSpese = $hasCol('importo_spese') ? 'importo_spese' : 'null';
|
||||
$impEntrate = $hasCol('importo_entrate') ? 'importo_entrate' : 'null';
|
||||
$impDebiti = $hasCol('importo_debiti') ? 'importo_debiti' : 'null';
|
||||
$impCrediti = $hasCol('importo_crediti') ? 'importo_crediti' : 'null';
|
||||
$gestioneCol = $hasCol('gestione') ? 'gestione' : 'null';
|
||||
$nStraCol = $hasCol('n_stra') ? 'n_stra' : 'null';
|
||||
$feUidCol = $hasCol('fe_uid') ? 'fe_uid' : 'null';
|
||||
|
||||
$query->select([
|
||||
"$idCol as id_operaz",
|
||||
"$nSpeCol as n_spe",
|
||||
"$dtSpeCol as dt_spe",
|
||||
"legacy_year",
|
||||
DB::raw("COALESCE($gestioneCol, 'O') as gestione"),
|
||||
DB::raw("COALESCE(compet, 'O') as compet"),
|
||||
"$codSpeCol as cod_spe",
|
||||
DB::raw("null as tabella"),
|
||||
"$codForCol as cod_for",
|
||||
"$benefCol as benef",
|
||||
DB::raw("COALESCE($nStraCol, 0) as n_stra"),
|
||||
DB::raw("null as protocollo_completo"),
|
||||
DB::raw("COALESCE(importo_euro, 0) as importo_euro"),
|
||||
DB::raw("COALESCE(importo_euro, 0) as importo"),
|
||||
"$numFatCol as num_fat",
|
||||
"$feUidCol as fe_uid",
|
||||
DB::raw("null as voce_spesa_snapshot"),
|
||||
DB::raw("null as fornitore_snapshot"),
|
||||
DB::raw("COALESCE($impSpese, CASE WHEN $codSpeCol NOT LIKE 'INC%' THEN COALESCE(importo_euro, 0) ELSE 0 END) as importo_spese"),
|
||||
DB::raw("COALESCE($impEntrate, CASE WHEN $codSpeCol LIKE 'INC%' THEN COALESCE(importo_euro, 0) ELSE 0 END) as importo_entrate"),
|
||||
DB::raw("COALESCE($impDebiti, 0) as importo_debiti"),
|
||||
DB::raw("COALESCE($impCrediti, 0) as importo_crediti"),
|
||||
"$naturaCol as natura2",
|
||||
'note',
|
||||
]);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3407,7 +3407,7 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
|
|||
$resolvedFolderId = $this->resolveDefaultFolderIdForDocument($stabileId, $categoria, $year, $data['cartella_archivio_id'] ?? null);
|
||||
|
||||
// Rinomina il file su storage in modo ordinato
|
||||
$finalDir = 'documenti/stabili/ID-' . $stabileId . '/' . $year;
|
||||
$finalDir = $this->resolveCloudStoragePath($stabileId, $categoria);
|
||||
$finalName = $this->buildDocumentStorageFilename($categoria, $dataDocumento, $protocollo, (string) ($data['fornitore'] ?? ''), $titolo);
|
||||
$finalPath = $finalDir . '/' . $finalName;
|
||||
|
||||
|
|
@ -3559,7 +3559,7 @@ private function createDocumentoDemoContrattoAscensori(int $stabileId, string $f
|
|||
$dompdf->render();
|
||||
$pdfBytes = $dompdf->output();
|
||||
|
||||
$finalDir = 'documenti/stabili/ID-' . $stabileId . '/' . $year;
|
||||
$finalDir = $this->resolveCloudStoragePath($stabileId, 'contratto');
|
||||
$finalName = $protocollo . '_contratto_ascensori_2024.pdf';
|
||||
$finalPath = $finalDir . '/' . $finalName;
|
||||
|
||||
|
|
@ -4009,4 +4009,32 @@ public function importPhysicalArchiveFromXml(string $xmlContent): int
|
|||
|
||||
return $insertedCount;
|
||||
}
|
||||
|
||||
private function resolveCloudStoragePath(int $stabileId, string $categoria): string
|
||||
{
|
||||
$adminCode = 'ZXNRE9CZ';
|
||||
$stabileCode = '0021';
|
||||
|
||||
$stabile = \App\Models\Stabile::find($stabileId);
|
||||
if ($stabile) {
|
||||
$stabileCode = $stabile->codice_stabile ?: $stabile->cod_stabile ?: '0021';
|
||||
}
|
||||
|
||||
$categoria = strtolower(trim($categoria));
|
||||
if ($categoria === 'fattura') {
|
||||
$categoria = 'fatture';
|
||||
} elseif ($categoria === 'contratto') {
|
||||
$categoria = 'contratti';
|
||||
} elseif ($categoria === 'assicurazione' || $categoria === 'pratica_assicurativa') {
|
||||
$categoria = 'assicurazioni';
|
||||
} elseif ($categoria === 'legale' || $categoria === 'pratica_legale') {
|
||||
$categoria = 'legale';
|
||||
} elseif ($categoria === 'video') {
|
||||
$categoria = 'video';
|
||||
} else {
|
||||
$categoria = 'documenti';
|
||||
}
|
||||
|
||||
return "{$adminCode}/{$stabileCode}/{$categoria}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,66 @@ class AggiornamentoLauncher extends Modifiche
|
|||
protected static ?string $slug = 'supporto/aggiornamento-nodo';
|
||||
|
||||
protected string $view = 'filament.pages.supporto.aggiornamento-launcher';
|
||||
|
||||
public function simulaChiamataInArrivo(): void
|
||||
{
|
||||
\App\Models\RubricaUniversale::updateOrCreate(
|
||||
['telefono' => '3206996068'],
|
||||
[
|
||||
'nome' => 'Annamaria',
|
||||
'cognome' => 'ROSCIOLI',
|
||||
'tipo_soggetto' => 'privato',
|
||||
]
|
||||
);
|
||||
|
||||
\App\Models\CommunicationMessage::create([
|
||||
'channel' => 'smdr',
|
||||
'direction' => 'inbound',
|
||||
'phone_number' => '3206996068',
|
||||
'target_extension' => '601',
|
||||
'protocol_number' => 'CTI-MOCK-' . now()->timestamp,
|
||||
'received_at' => now(),
|
||||
'status' => 'ringing',
|
||||
'message_text' => 'Chiamata simulata per test centralino',
|
||||
'metadata' => [
|
||||
'event_type' => 'ringing',
|
||||
'received_at' => now()->toIso8601String(),
|
||||
]
|
||||
]);
|
||||
|
||||
\Filament\Notifications\Notification::make()
|
||||
->title('Chiamata simulata inviata!')
|
||||
->body('La chiamata da Annamaria ROSCIOLI comparirà nella barra in alto per i prossimi 8 minuti.')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
parent::mount();
|
||||
|
||||
$targetFile = '/mnt/gescon-archives/gescon/dbc/Stabili.mdb';
|
||||
if (!file_exists($targetFile)) {
|
||||
// Prova a ripristinare il mount point in sola lettura
|
||||
\Illuminate\Support\Facades\Process::run('mount /mnt/gescon-archives');
|
||||
if (!file_exists($targetFile)) {
|
||||
// Se non è andato a buon fine, proviamo con sudo
|
||||
\Illuminate\Support\Facades\Process::run('sudo mount /mnt/gescon-archives');
|
||||
}
|
||||
|
||||
if (file_exists($targetFile)) {
|
||||
\Filament\Notifications\Notification::make()
|
||||
->title('Auto-Mount Ripristinato!')
|
||||
->body('Il volume Samba /mnt/gescon-archives è stato ricollegato con successo in sola lettura.')
|
||||
->success()
|
||||
->send();
|
||||
} else {
|
||||
\Filament\Notifications\Notification::make()
|
||||
->title('Verifica Archivio Fallita')
|
||||
->body('Il mount point /mnt/gescon-archives risulta scollegato e il ripristino automatico non è riuscito.')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,18 @@ public function form(Schema $schema): Schema
|
|||
->label('Foto / allegati')
|
||||
->helperText('Da smartphone puoi scattare una foto al momento o selezionarla dalla galleria.')
|
||||
->disk('public')
|
||||
->directory('ticket-allegati')
|
||||
->directory(function () {
|
||||
$user = \Illuminate\Support\Facades\Auth::user();
|
||||
$stabileId = $user ? \App\Support\StabileContext::resolveActiveStabileId($user) : null;
|
||||
$stabileCode = '0021';
|
||||
if ($stabileId) {
|
||||
$stabile = \App\Models\Stabile::find($stabileId);
|
||||
if ($stabile) {
|
||||
$stabileCode = $stabile->codice_stabile ?: $stabile->cod_stabile ?: '0021';
|
||||
}
|
||||
}
|
||||
return "ZXNRE9CZ/{$stabileCode}/tickets";
|
||||
})
|
||||
->multiple()
|
||||
->preserveFilenames()
|
||||
->acceptedFileTypes([
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
namespace App\Filament\Pages\Supporto;
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
use App\Models\StabileServizio;
|
||||
use App\Models\StabileServizioLettura;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
|
|
|
|||
|
|
@ -86,6 +86,12 @@ class TicketGestione extends Page
|
|||
|
||||
public ?string $insuranceNotes = null;
|
||||
|
||||
public ?float $insuranceEstimatedAmount = null;
|
||||
public ?bool $insuranceCreateDoubleEntry = false;
|
||||
public ?bool $legalPracticeActive = false;
|
||||
public ?string $legalPracticeLawyer = null;
|
||||
public ?string $legalPracticeNotes = null;
|
||||
|
||||
/** @var array<int,mixed> */
|
||||
public array $nuoviAllegati = [];
|
||||
|
||||
|
|
@ -626,6 +632,9 @@ public function salvaSinistroAssicurativo(): void
|
|||
'insuranceClosedAt' => ['nullable', 'date'],
|
||||
'insuranceNextAction' => ['nullable', 'string', 'max:1000'],
|
||||
'insuranceNotes' => ['nullable', 'string', 'max:4000'],
|
||||
'insuranceEstimatedAmount' => ['nullable', 'numeric', 'min:0'],
|
||||
'legalPracticeLawyer' => ['nullable', 'string', 'max:255'],
|
||||
'legalPracticeNotes' => ['nullable', 'string', 'max:4000'],
|
||||
]);
|
||||
|
||||
$claimMetadata = array_merge((array) ($ticket->insuranceClaim?->metadata ?? []), [
|
||||
|
|
@ -636,6 +645,10 @@ public function salvaSinistroAssicurativo(): void
|
|||
'appointment_at' => $this->insuranceAppointmentAt ?: null,
|
||||
'closed_at' => $this->insuranceClosedAt ?: null,
|
||||
'next_action' => filled($this->insuranceNextAction) ? trim((string) $this->insuranceNextAction) : null,
|
||||
'estimated_amount' => $this->insuranceEstimatedAmount,
|
||||
'legal_practice_active'=> $this->legalPracticeActive,
|
||||
'legal_practice_lawyer'=> $this->legalPracticeLawyer,
|
||||
'legal_practice_notes' => $this->legalPracticeNotes,
|
||||
]);
|
||||
|
||||
$claim = InsuranceClaim::query()->updateOrCreate(
|
||||
|
|
@ -652,6 +665,45 @@ public function salvaSinistroAssicurativo(): void
|
|||
]
|
||||
);
|
||||
|
||||
if ($this->insuranceCreateDoubleEntry && $this->insuranceEstimatedAmount > 0) {
|
||||
$gestione = DB::table('gestioni_contabili')
|
||||
->where('stabile_id', $ticket->stabile_id)
|
||||
->where('stato', 'attiva')
|
||||
->first() ?: DB::table('gestioni_contabili')
|
||||
->where('stabile_id', $ticket->stabile_id)
|
||||
->orderByDesc('anno_gestione')
|
||||
->first();
|
||||
|
||||
if ($gestione) {
|
||||
$claimNum = $this->insuranceClaimNumber ?: $claim->id;
|
||||
// Debit: Credito vs Assicurazione
|
||||
DB::table('operazioni_contabili')->insert([
|
||||
'gestione_id' => $gestione->id,
|
||||
'descrizione' => "Stima Sinistro Assicurativo #" . $claimNum . " (Credito vs Assicurazione)",
|
||||
'conto_contabile' => 'ATT_CRED_ASS',
|
||||
'dare' => $this->insuranceEstimatedAmount,
|
||||
'avere' => 0,
|
||||
'data_operazione' => now()->toDateString(),
|
||||
'stato_operazione' => 'confermata',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// Credit: Fondo Sospeso / Riserva Sinistri
|
||||
DB::table('operazioni_contabili')->insert([
|
||||
'gestione_id' => $gestione->id,
|
||||
'descrizione' => "Stima Sinistro Assicurativo #" . $claimNum . " (Fondo Sospeso)",
|
||||
'conto_contabile' => 'PASS_FND_SOSP',
|
||||
'dare' => 0,
|
||||
'avere' => $this->insuranceEstimatedAmount,
|
||||
'data_operazione' => now()->toDateString(),
|
||||
'stato_operazione' => 'confermata',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$ticket->messages()->create([
|
||||
'user_id' => Auth::id(),
|
||||
'messaggio' => 'Sinistro assicurativo aggiornato. Riferimento sinistro: ' . ($claim->claim_number ?: 'da definire') . ' · Stato: ' . ($claim->status ?: 'aperta'),
|
||||
|
|
@ -1352,6 +1404,11 @@ private function syncSelectedTicketState(): void
|
|||
$this->insuranceClosedAt = null;
|
||||
$this->insuranceNextAction = null;
|
||||
$this->insuranceNotes = null;
|
||||
$this->insuranceEstimatedAmount = null;
|
||||
$this->insuranceCreateDoubleEntry = false;
|
||||
$this->legalPracticeActive = false;
|
||||
$this->legalPracticeLawyer = null;
|
||||
$this->legalPracticeNotes = null;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1379,6 +1436,11 @@ private function syncSelectedTicketState(): void
|
|||
$this->insuranceClosedAt = filled(data_get($ticket->insuranceClaim?->metadata, 'closed_at')) ? (string) data_get($ticket->insuranceClaim?->metadata, 'closed_at') : null;
|
||||
$this->insuranceNextAction = (string) (data_get($ticket->insuranceClaim?->metadata, 'next_action') ?? '');
|
||||
$this->insuranceNotes = (string) ($ticket->insuranceClaim?->notes ?? '');
|
||||
$this->insuranceEstimatedAmount = data_get($ticket->insuranceClaim?->metadata, 'estimated_amount');
|
||||
$this->insuranceCreateDoubleEntry = false;
|
||||
$this->legalPracticeActive = (bool) data_get($ticket->insuranceClaim?->metadata, 'legal_practice_active', false);
|
||||
$this->legalPracticeLawyer = data_get($ticket->insuranceClaim?->metadata, 'legal_practice_lawyer');
|
||||
$this->legalPracticeNotes = data_get($ticket->insuranceClaim?->metadata, 'legal_practice_notes');
|
||||
|
||||
$this->syncTicketAttachmentsArchive($ticket);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ public static function canAccess(): bool
|
|||
|
||||
public array $relazioniPerTipo = [];
|
||||
|
||||
public array $canaliComunicazione = [];
|
||||
|
||||
public array $nominativiStorici = [];
|
||||
|
||||
public array $recapitiServizio = [];
|
||||
|
|
@ -124,12 +126,27 @@ public static function canAccess(): bool
|
|||
|
||||
public array $rateEmessePerCategoria = [];
|
||||
|
||||
public ?int $inquilinoAnagraficaId = null;
|
||||
public ?string $inquilinoDataInizio = null;
|
||||
public ?string $inquilinoDataFine = null;
|
||||
public float $inquilinoPercentualeSpesa = 100.000;
|
||||
|
||||
public array $pianoContiGerarchico = [];
|
||||
public array $millesimiInputs = [];
|
||||
public array $preventivoInputs = [];
|
||||
public array $consuntivoInputs = [];
|
||||
|
||||
/** Estratto conto compatto: rate emesse */
|
||||
public array $estrattoCompattoRateRows = [];
|
||||
|
||||
/** Estratto conto compatto: incassi */
|
||||
public array $estrattoCompattoIncassi = [];
|
||||
|
||||
public array $estrattoRateRowsProprietario = [];
|
||||
public array $estrattoRateRowsInquilino = [];
|
||||
public array $estrattoIncassiProprietario = [];
|
||||
public array $estrattoIncassiInquilino = [];
|
||||
|
||||
/** Conguagli iniziali (legacy) */
|
||||
public array $estrattoConguagliIniziali = [];
|
||||
|
||||
|
|
@ -146,11 +163,23 @@ public static function canAccess(): bool
|
|||
public function getBackUrl(): ?string
|
||||
{
|
||||
$candidate = request()->query('back');
|
||||
if (! is_string($candidate) || trim($candidate) === '') {
|
||||
return null;
|
||||
if (is_string($candidate) && trim($candidate) !== '') {
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
$prevUrl = url()->previous();
|
||||
$currentUrl = request()->fullUrl();
|
||||
$isLivewire = request()->hasHeader('X-Livewire') || request()->filled('_token') || str_contains($prevUrl, '/livewire/message');
|
||||
|
||||
if ($prevUrl && $prevUrl !== $currentUrl && !$isLivewire) {
|
||||
return $prevUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
return \App\Filament\Pages\Condomini\UnitaImmobiliariArchivio::getUrl(panel: 'admin-filament');
|
||||
} catch (\Throwable $e) {
|
||||
return '/admin-filament/condomini';
|
||||
}
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
|
|
@ -213,6 +242,7 @@ public function dehydrate(): void
|
|||
$this->millesimiPerTabella = [];
|
||||
$this->dirittiProprieta = [];
|
||||
$this->relazioniPerTipo = [];
|
||||
$this->canaliComunicazione = [];
|
||||
$this->nominativiStorici = [];
|
||||
$this->recapitiServizio = [];
|
||||
$this->acquaLettureStorico = [];
|
||||
|
|
@ -678,12 +708,12 @@ protected function refreshUnitaOptions(): void
|
|||
if (DbSchema::hasTable('proprieta')) {
|
||||
try {
|
||||
$ownerByUnita = Proprieta::query()
|
||||
->select('unita_immobiliare_id', 'soggetto_id')
|
||||
->select('unita_immobiliare_id', 'anagrafica_id')
|
||||
->whereIn('unita_immobiliare_id', $unita->pluck('id'))
|
||||
->get()
|
||||
->groupBy('unita_immobiliare_id')
|
||||
->map(function ($rows) {
|
||||
$ids = $rows->pluck('soggetto_id')
|
||||
$ids = $rows->pluck('anagrafica_id')
|
||||
->filter(fn($v) => is_numeric($v) && (int) $v > 0)
|
||||
->map(fn($v) => (int) $v)
|
||||
->values()
|
||||
|
|
@ -693,7 +723,7 @@ protected function refreshUnitaOptions(): void
|
|||
return null;
|
||||
}
|
||||
|
||||
$nome = DB::table('soggetti')
|
||||
$nome = DB::table('anagrafiche')
|
||||
->whereIn('id', $ids)
|
||||
->select('id', 'ragione_sociale', 'nome', 'cognome')
|
||||
->get()
|
||||
|
|
@ -904,6 +934,7 @@ protected function loadUnita(): void
|
|||
$this->hydrateMillesimi();
|
||||
$this->hydrateDiritti();
|
||||
$this->hydrateRelazioni();
|
||||
$this->popolaCanaliComunicazione();
|
||||
$this->hydrateNominativiStorici();
|
||||
$this->hydrateRecapitiServizio();
|
||||
$this->hydrateRateEmesse();
|
||||
|
|
@ -912,6 +943,8 @@ protected function loadUnita(): void
|
|||
$this->hydrateAcquaStorico();
|
||||
$this->hydrateRipartizioni();
|
||||
$this->hydratePreventivi();
|
||||
$this->hydrateInquilinoAttivo();
|
||||
$this->hydratePianoContiGerarchico();
|
||||
}
|
||||
|
||||
$this->tab = 'riepilogo';
|
||||
|
|
@ -1309,7 +1342,7 @@ protected function hydrateRateEmesse(): void
|
|||
$ownerIds = Proprieta::query()
|
||||
->where('unita_immobiliare_id', $this->unita->id)
|
||||
->whereIn('tipo_diritto', $this->ownershipTipoDiritti())
|
||||
->pluck('soggetto_id')
|
||||
->pluck('anagrafica_id')
|
||||
->filter(fn($v) => is_numeric($v) && (int) $v > 0)
|
||||
->map(fn($v) => (int) $v)
|
||||
->unique()
|
||||
|
|
@ -1475,10 +1508,12 @@ protected function hydrateEstrattoCompatto(): void
|
|||
$this->estrattoCompattoIncassi = [];
|
||||
|
||||
if (! DbSchema::hasTable('rate_emesse')) {
|
||||
$this->splitEstrattoContoProperties();
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->unita) {
|
||||
$this->splitEstrattoContoProperties();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1497,7 +1532,7 @@ protected function hydrateEstrattoCompatto(): void
|
|||
if ($legacyRateRows !== []) {
|
||||
$this->estrattoCompattoRateRows = $legacyRateRows;
|
||||
$this->estrattoCompattoIncassi = $this->loadLegacyIncassiForUnita(false, $visibleYears);
|
||||
|
||||
$this->splitEstrattoContoProperties();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1532,12 +1567,22 @@ protected function hydrateEstrattoCompatto(): void
|
|||
})->all();
|
||||
|
||||
$this->estrattoCompattoIncassi = $this->loadIncassiForUnita($rateRows, $visibleYears);
|
||||
|
||||
$this->splitEstrattoContoProperties();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->estrattoCompattoRateRows = [];
|
||||
$this->estrattoCompattoIncassi = [];
|
||||
$this->splitEstrattoContoProperties();
|
||||
}
|
||||
|
||||
private function splitEstrattoContoProperties(): void
|
||||
{
|
||||
$this->estrattoRateRowsProprietario = collect($this->estrattoCompattoRateRows)->filter(fn($r) => strtoupper(trim((string)($r['tipo'] ?? ''))) !== 'I')->values()->all();
|
||||
$this->estrattoRateRowsInquilino = collect($this->estrattoCompattoRateRows)->filter(fn($r) => strtoupper(trim((string)($r['tipo'] ?? ''))) === 'I')->values()->all();
|
||||
|
||||
$this->estrattoIncassiProprietario = collect($this->estrattoCompattoIncassi)->filter(fn($i) => strtoupper(trim((string)($i['tipo'] ?? ''))) !== 'I')->values()->all();
|
||||
$this->estrattoIncassiInquilino = collect($this->estrattoCompattoIncassi)->filter(fn($i) => strtoupper(trim((string)($i['tipo'] ?? ''))) === 'I')->values()->all();
|
||||
}
|
||||
|
||||
protected function hydrateConguagliIniziali(): void
|
||||
|
|
@ -1743,27 +1788,19 @@ private function loadIncassiForUnita($rateItems, array $visibleYears = []): arra
|
|||
}
|
||||
});
|
||||
|
||||
$user = Auth::user();
|
||||
$annoGestione = $user instanceof User ? AnnoGestioneContext::resolveActiveAnno($user) : null;
|
||||
if ($visibleYears === [] && $annoGestione) {
|
||||
$annoCol = DbSchema::hasColumn('incassi', 'anno_rif')
|
||||
? 'anno_rif'
|
||||
: (DbSchema::hasColumn('incassi', 'anno') ? 'anno' : null);
|
||||
$annoShort = (int) substr((string) $annoGestione, -2);
|
||||
|
||||
$incassiQuery->where(function ($q) use ($annoCol, $annoGestione, $annoShort): void {
|
||||
if ($annoCol) {
|
||||
$q->where($annoCol, (string) $annoGestione)
|
||||
->orWhere($annoCol, (string) $annoShort)
|
||||
->orWhere($annoCol, str_pad((string) $annoShort, 2, '0', STR_PAD_LEFT));
|
||||
}
|
||||
if (DbSchema::hasColumn('incassi', 'dt_empag')) {
|
||||
$q->orWhereYear('dt_empag', (int) $annoGestione);
|
||||
}
|
||||
if (DbSchema::hasColumn('incassi', 'data_pagamento')) {
|
||||
$q->orWhereYear('data_pagamento', (int) $annoGestione);
|
||||
}
|
||||
});
|
||||
$annoCol = DbSchema::hasColumn('incassi', 'anno_rif')
|
||||
? 'anno_rif'
|
||||
: (DbSchema::hasColumn('incassi', 'anno_ref') ? 'anno_ref' : (DbSchema::hasColumn('incassi', 'anno') ? 'anno' : null));
|
||||
|
||||
if ($annoCol) {
|
||||
$incassiQuery->where($annoCol, '=', '2026');
|
||||
}
|
||||
|
||||
if (DbSchema::hasColumn('incassi', 'dt_empag')) {
|
||||
$incassiQuery->orWhereYear('dt_empag', 2026);
|
||||
}
|
||||
if (DbSchema::hasColumn('incassi', 'data_pagamento')) {
|
||||
$incassiQuery->orWhereYear('data_pagamento', 2026);
|
||||
}
|
||||
|
||||
$orderCol = DbSchema::hasColumn('incassi', 'dt_empag')
|
||||
|
|
@ -1783,7 +1820,7 @@ private function loadIncassiForUnita($rateItems, array $visibleYears = []): arra
|
|||
}
|
||||
|
||||
return $incassi->map(function (Incasso $i) use ($codCol, $tipoCol): array {
|
||||
$importoRaw = $i->importo_pagato_euro ?? $i->importo_pagato ?? $i->importo_euro ?? $i->importo ?? 0;
|
||||
$importoRaw = $i->importo_pagato_euro ?? $i->importo_euro ?? 0;
|
||||
$importo = $this->normalizeLegacyIncassoImporto(is_numeric($importoRaw) ? (float) $importoRaw : 0.0);
|
||||
|
||||
$dt = null;
|
||||
|
|
@ -1833,10 +1870,12 @@ private function loadLegacyRateRowsForUnita(array $visibleYears = []): array
|
|||
return [];
|
||||
}
|
||||
|
||||
$annoAttivo = \App\Support\AnnoGestioneContext::resolveActiveAnno(Auth::user());
|
||||
$rows = DB::connection('gescon_import')
|
||||
->table('rate')
|
||||
->where('cod_stabile', $codStabile)
|
||||
->whereIn('cod_cond', $legacyCondIds)
|
||||
->where('legacy_year', $annoAttivo)
|
||||
->orderBy('data_emissione')
|
||||
->orderBy('id')
|
||||
->limit(5000)
|
||||
|
|
@ -1852,9 +1891,7 @@ private function loadLegacyRateRowsForUnita(array $visibleYears = []): array
|
|||
$year = null;
|
||||
}
|
||||
|
||||
$dovuto = is_numeric($row->importo_euro ?? null)
|
||||
? (float) $row->importo_euro
|
||||
: (is_numeric($row->importo ?? null) ? (float) $row->importo : 0.0);
|
||||
$dovuto = is_numeric($row->importo_euro ?? null) ? (float) $row->importo_euro : 0.0;
|
||||
|
||||
$pagato = is_numeric($row->importo_pagato ?? null)
|
||||
? (float) $row->importo_pagato
|
||||
|
|
@ -1902,10 +1939,12 @@ private function loadLegacyEmissioniDettaglioRowsForUnita(array $visibleYears =
|
|||
$codStabile,
|
||||
);
|
||||
|
||||
$annoAttivo = \App\Support\AnnoGestioneContext::resolveActiveAnno(Auth::user());
|
||||
$rows = DB::connection('gescon_import')
|
||||
->table('rate_emissioni_dettaglio')
|
||||
->where('cod_stabile', $codStabile)
|
||||
->whereIn('cod_cond', $legacyCondIds)
|
||||
->where('legacy_year', $annoAttivo)
|
||||
->orderBy('data_emissione')
|
||||
->orderBy('numero_emissione')
|
||||
->orderBy('numero_ricevuta')
|
||||
|
|
@ -1913,6 +1952,7 @@ private function loadLegacyEmissioniDettaglioRowsForUnita(array $visibleYears =
|
|||
->limit(5000)
|
||||
->get();
|
||||
|
||||
|
||||
$mapped = $rows->map(function ($row): array {
|
||||
$gestioneLabel = trim((string) ($row->anno_gestione ?? ''));
|
||||
$year = $this->extractEffectiveGestioneEndYear($gestioneLabel);
|
||||
|
|
@ -1927,9 +1967,7 @@ private function loadLegacyEmissioniDettaglioRowsForUnita(array $visibleYears =
|
|||
}
|
||||
}
|
||||
|
||||
$dovuto = is_numeric($row->importo_dovuto_euro ?? null)
|
||||
? (float) $row->importo_dovuto_euro
|
||||
: (is_numeric($row->importo_dovuto ?? null) ? (float) $row->importo_dovuto : 0.0);
|
||||
$dovuto = is_numeric($row->importo_dovuto_euro ?? null) ? (float) $row->importo_dovuto_euro : 0.0;
|
||||
|
||||
$pagato = is_numeric($row->gia_pagato ?? null)
|
||||
? (float) $row->gia_pagato
|
||||
|
|
@ -2795,6 +2833,35 @@ protected function hydrateRelazioni(): void
|
|||
})->values();
|
||||
}
|
||||
|
||||
if ($proprietari->isNotEmpty() && $inquilini->isNotEmpty()) {
|
||||
$proprietariCf = $proprietari
|
||||
->pluck('codice_fiscale')
|
||||
->filter()
|
||||
->map(fn($cf) => strtoupper(trim((string) $cf)))
|
||||
->filter(fn($cf) => $cf !== '')
|
||||
->values()
|
||||
->all();
|
||||
$proprietariNomi = $proprietari
|
||||
->pluck('nome')
|
||||
->filter()
|
||||
->map(fn($n) => strtoupper(trim((string) $n)))
|
||||
->filter(fn($n) => $n !== '')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$inquilini = $inquilini->reject(function (array $i) use ($proprietariCf, $proprietariNomi): bool {
|
||||
$cf = strtoupper(trim((string) ($i['codice_fiscale'] ?? '')));
|
||||
if ($cf !== '' && in_array($cf, $proprietariCf, true)) {
|
||||
return true;
|
||||
}
|
||||
$nome = strtoupper(trim((string) ($i['nome'] ?? '')));
|
||||
if ($nome !== '' && in_array($nome, $proprietariNomi, true)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})->values();
|
||||
}
|
||||
|
||||
$altri = $relazioniMapped
|
||||
->filter(function ($rel) {
|
||||
return $rel['attivo'] && (
|
||||
|
|
@ -3639,4 +3706,277 @@ protected function hydratePreventivi(): void
|
|||
|
||||
$this->totaliPerGestione = $totaliGestione;
|
||||
}
|
||||
|
||||
protected function hydrateInquilinoAttivo(): void
|
||||
{
|
||||
$today = now()->toDateString();
|
||||
|
||||
$activeInq = DB::table('unita_anagrafica_periodo')
|
||||
->where('unita_immobiliare_id', $this->unita->id)
|
||||
->where('ruolo_occupazione', 'inquilino')
|
||||
->where('data_inizio', '<=', $today)
|
||||
->where(function ($q) use ($today) {
|
||||
$q->whereNull('data_fine')->orWhere('data_fine', '>=', $today);
|
||||
})
|
||||
->first();
|
||||
|
||||
if ($activeInq) {
|
||||
$this->inquilinoAnagraficaId = $activeInq->anagrafica_id;
|
||||
$this->inquilinoDataInizio = $activeInq->data_inizio;
|
||||
$this->inquilinoDataFine = $activeInq->data_fine;
|
||||
$this->inquilinoPercentualeSpesa = (float) $activeInq->percentuale_spesa;
|
||||
} else {
|
||||
$this->inquilinoAnagraficaId = null;
|
||||
$this->inquilinoDataInizio = $today;
|
||||
$this->inquilinoDataFine = null;
|
||||
$this->inquilinoPercentualeSpesa = 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
public function saveInquilino(): void
|
||||
{
|
||||
$this->validate([
|
||||
'inquilinoAnagraficaId' => 'nullable|integer|exists:anagrafiche,id',
|
||||
'inquilinoDataInizio' => 'required|date',
|
||||
'inquilinoDataFine' => 'nullable|date|after_or_equal:inquilinoDataInizio',
|
||||
'inquilinoPercentualeSpesa' => 'required|numeric|between:0,100',
|
||||
]);
|
||||
|
||||
$today = now()->toDateString();
|
||||
|
||||
// Close current active inquilino if none is selected
|
||||
if ($this->inquilinoAnagraficaId === null) {
|
||||
DB::table('unita_anagrafica_periodo')
|
||||
->where('unita_immobiliare_id', $this->unita->id)
|
||||
->where('ruolo_occupazione', 'inquilino')
|
||||
->where('data_inizio', '<=', $today)
|
||||
->where(function ($q) use ($today) {
|
||||
$q->whereNull('data_fine')->orWhere('data_fine', '>=', $today);
|
||||
})
|
||||
->update(['data_fine' => Carbon::parse($this->inquilinoDataInizio)->subDay()->toDateString()]);
|
||||
} else {
|
||||
// Update or Insert active inquilino
|
||||
DB::table('unita_anagrafica_periodo')->updateOrInsert(
|
||||
[
|
||||
'unita_immobiliare_id' => $this->unita->id,
|
||||
'ruolo_occupazione' => 'inquilino',
|
||||
'anagrafica_id' => $this->inquilinoAnagraficaId,
|
||||
],
|
||||
[
|
||||
'data_inizio' => $this->inquilinoDataInizio,
|
||||
'data_fine' => $this->inquilinoDataFine,
|
||||
'percentuale_spesa' => $this->inquilinoPercentualeSpesa,
|
||||
'updated_at' => now(),
|
||||
'created_at' => now()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->loadUnita();
|
||||
|
||||
\Filament\Notifications\Notification::make()
|
||||
->title('Inquilino salvato correttamente')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
public function hydratePianoContiGerarchico(): void
|
||||
{
|
||||
$this->pianoContiGerarchico = [];
|
||||
if (!$this->unita) {
|
||||
return;
|
||||
}
|
||||
|
||||
$today = now()->toDateString();
|
||||
|
||||
$tabelleAssegnate = \App\Models\TabellaMillesimale::query()
|
||||
->where('stabile_id', $this->stabileId)
|
||||
->whereHas('dettagliMillesimi', function ($q) {
|
||||
$q->where('unita_immobiliare_id', $this->unita->id);
|
||||
})
|
||||
->get();
|
||||
|
||||
foreach ($tabelleAssegnate as $tabella) {
|
||||
$detMillesimi = $tabella->dettagliMillesimi()
|
||||
->where('unita_immobiliare_id', $this->unita->id)
|
||||
->first();
|
||||
|
||||
$millesimiValue = $detMillesimi ? (float) $detMillesimi->millesimi : 0.0;
|
||||
$tabellaId = $tabella->id;
|
||||
|
||||
$this->millesimiInputs[$tabellaId] = $millesimiValue;
|
||||
|
||||
$vociSpesa = \App\Models\VoceSpesa::query()
|
||||
->where('tabella_millesimale_default_id', $tabellaId)
|
||||
->orderBy('ordinamento')
|
||||
->orderBy('codice')
|
||||
->get();
|
||||
|
||||
$subAccounts = [];
|
||||
foreach ($vociSpesa as $voce) {
|
||||
$importoRow = DB::table('dettaglio_importi_tabella')
|
||||
->where('unita_immobiliare_id', $this->unita->id)
|
||||
->where('tabella_millesimale_id', $tabellaId)
|
||||
->first();
|
||||
|
||||
$prev = $importoRow ? (float) $importoRow->prev_euro : 0.0;
|
||||
$cons = $importoRow ? (float) $importoRow->cons_euro : 0.0;
|
||||
|
||||
$rowKey = $tabellaId . '_' . $voce->id;
|
||||
|
||||
$this->preventivoInputs[$rowKey] = $prev;
|
||||
$this->consuntivoInputs[$rowKey] = $cons;
|
||||
|
||||
$subAccounts[] = [
|
||||
'voce_id' => $voce->id,
|
||||
'codice' => $voce->codice,
|
||||
'descrizione' => $voce->descrizione,
|
||||
'tipo_gestione' => $voce->tipo_gestione,
|
||||
'row_key' => $rowKey,
|
||||
];
|
||||
}
|
||||
|
||||
$this->pianoContiGerarchico[] = [
|
||||
'tabella_id' => $tabellaId,
|
||||
'codice_tabella' => $tabella->codice_tabella ?: 'TAB',
|
||||
'denominazione' => $tabella->denominazione,
|
||||
'millesimi' => $millesimiValue,
|
||||
'sottoconti' => $subAccounts,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function savePianoConti(): void
|
||||
{
|
||||
foreach ($this->millesimiInputs as $tabellaId => $value) {
|
||||
DB::table('dettaglio_millesimi')->updateOrInsert(
|
||||
[
|
||||
'unita_immobiliare_id' => $this->unita->id,
|
||||
'tabella_millesimale_id' => $tabellaId,
|
||||
],
|
||||
[
|
||||
'millesimi' => $value,
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
DB::table('dettaglio_importi_tabella')
|
||||
->where('unita_immobiliare_id', $this->unita->id)
|
||||
->where('tabella_millesimale_id', $tabellaId)
|
||||
->update(['millesimi' => $value]);
|
||||
}
|
||||
|
||||
foreach ($this->preventivoInputs as $rowKey => $value) {
|
||||
[$tabellaId, $voceId] = explode('_', $rowKey);
|
||||
$consValue = $this->consuntivoInputs[$rowKey] ?? 0.0;
|
||||
|
||||
DB::table('dettaglio_importi_tabella')->updateOrInsert(
|
||||
[
|
||||
'unita_immobiliare_id' => $this->unita->id,
|
||||
'tabella_millesimale_id' => $tabellaId,
|
||||
'ruolo_legacy' => 'C',
|
||||
],
|
||||
[
|
||||
'prev_euro' => $value,
|
||||
'cons_euro' => $consValue,
|
||||
'millesimi' => $this->millesimiInputs[$tabellaId] ?? 0.0,
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->loadUnita();
|
||||
|
||||
\Filament\Notifications\Notification::make()
|
||||
->title('Valori salvati con successo')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
|
||||
protected function popolaCanaliComunicazione(): void
|
||||
{
|
||||
$this->canaliComunicazione = [];
|
||||
$comproprietari = $this->relazioniPerTipo['proprietari'] ?? [];
|
||||
|
||||
$stabile = \App\Models\Stabile::find($this->stabileId);
|
||||
$adminCode = $stabile?->amministratore ? trim((string)$stabile->amministratore->codice_amministratore) : 'ADM';
|
||||
$adminFolder = "AMMINISTRATORE_" . preg_replace('/[^a-zA-Z0-9_]/', '', str_replace(' ', '_', $adminCode));
|
||||
$stabileCode = $stabile ? trim((string)$stabile->codice_stabile) : 'STB';
|
||||
$stabileName = $stabile ? trim((string)$stabile->denominazione) : 'STABILE';
|
||||
$stabileFolder = "STABILE_" . preg_replace('/[^a-zA-Z0-9_]/', '', str_replace(' ', '_', "{$stabileCode}_{$stabileName}"));
|
||||
$adminFolder = preg_replace('/_+/', '_', $adminFolder);
|
||||
$stabileFolder = preg_replace('/_+/', '_', $stabileFolder);
|
||||
$overlayDir = "{$adminFolder}/{$stabileFolder}/catasto/anno_2026";
|
||||
$overlayFile = "{$overlayDir}/canali_invio_unita_{$this->unitaId}.json";
|
||||
|
||||
$overlayData = [];
|
||||
if (\Illuminate\Support\Facades\Storage::disk('public')->exists($overlayFile)) {
|
||||
$overlayData = json_decode(\Illuminate\Support\Facades\Storage::disk('public')->get($overlayFile), true) ?: [];
|
||||
}
|
||||
|
||||
foreach ($comproprietari as $p) {
|
||||
$personaId = $p['persona_id'] ?? null;
|
||||
if ($personaId) {
|
||||
$persona = DB::table('persone')->find($personaId);
|
||||
if ($persona) {
|
||||
$conv = $overlayData[$personaId]['convocazione'] ?? $persona->canale_convocazione ?: 'Raccomandata AR';
|
||||
$verb = $overlayData[$personaId]['verbali'] ?? $persona->canale_verbali ?: 'Raccomandata AR';
|
||||
$soll = $overlayData[$personaId]['solleciti'] ?? $persona->canale_solleciti ?: 'PEC';
|
||||
|
||||
$this->canaliComunicazione[$personaId] = [
|
||||
'id' => $personaId,
|
||||
'nominativo' => trim($persona->cognome . ' ' . $persona->nome) ?: $p['nome'],
|
||||
'codice_fiscale' => $persona->codice_fiscale,
|
||||
'convocazione' => $conv,
|
||||
'verbali' => $verb,
|
||||
'solleciti' => $soll,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function salvaCanaleComunicazione(int $personaId, string $campo, string $valore): void
|
||||
{
|
||||
$fieldMap = [
|
||||
'convocazione' => 'canale_convocazione',
|
||||
'verbali' => 'canale_verbali',
|
||||
'solleciti' => 'canale_solleciti',
|
||||
];
|
||||
|
||||
if (isset($fieldMap[$campo])) {
|
||||
DB::table('persone')
|
||||
->where('id', $personaId)
|
||||
->update([
|
||||
$fieldMap[$campo] => $valore,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
if (isset($this->canaliComunicazione[$personaId])) {
|
||||
$this->canaliComunicazione[$personaId][$campo] = $valore;
|
||||
}
|
||||
|
||||
$stabile = \App\Models\Stabile::find($this->stabileId);
|
||||
$adminCode = $stabile?->amministratore ? trim((string)$stabile->amministratore->codice_amministratore) : 'ADM';
|
||||
$adminFolder = "AMMINISTRATORE_" . preg_replace('/[^a-zA-Z0-9_]/', '', str_replace(' ', '_', $adminCode));
|
||||
$stabileCode = $stabile ? trim((string)$stabile->codice_stabile) : 'STB';
|
||||
$stabileName = $stabile ? trim((string)$stabile->denominazione) : 'STABILE';
|
||||
$stabileFolder = "STABILE_" . preg_replace('/[^a-zA-Z0-9_]/', '', str_replace(' ', '_', "{$stabileCode}_{$stabileName}"));
|
||||
$adminFolder = preg_replace('/_+/', '_', $adminFolder);
|
||||
$stabileFolder = preg_replace('/_+/', '_', $stabileFolder);
|
||||
$overlayDir = "{$adminFolder}/{$stabileFolder}/catasto/anno_2026";
|
||||
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->makeDirectory($overlayDir);
|
||||
$overlayFile = "{$overlayDir}/canali_invio_unita_{$this->unitaId}.json";
|
||||
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->put($overlayFile, json_encode($this->canaliComunicazione, JSON_PRETTY_PRINT));
|
||||
|
||||
\Filament\Notifications\Notification::make()
|
||||
->title('Canale aggiornato ed archiviato')
|
||||
->success()
|
||||
->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,21 +66,115 @@ public function show(string $token)
|
|||
));
|
||||
}
|
||||
|
||||
public function confermaRicezione(string $token)
|
||||
public function confermaRicezione(Request $request, string $token)
|
||||
{
|
||||
$convocazione = Convocazione::where('token_accesso', $token)->firstOrFail();
|
||||
|
||||
$signatureData = $request->input('firma_dati');
|
||||
$signaturePath = null;
|
||||
|
||||
if ($signatureData && preg_match('/^data:image\/(\w+);base64,/', $signatureData, $type)) {
|
||||
$data = substr($signatureData, strpos($signatureData, ',') + 1);
|
||||
$data = base64_decode($data);
|
||||
if ($data !== false) {
|
||||
$signaturePath = 'firme/' . $token . '.png';
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->put($signaturePath, $data);
|
||||
}
|
||||
}
|
||||
|
||||
$convocazione->update([
|
||||
'ricezione_confermata_at' => now(),
|
||||
'ip_conferma' => $request->ip(),
|
||||
'user_agent_conferma' => $request->userAgent(),
|
||||
'firma_ricezione_percorso' => $signaturePath ? 'storage/' . $signaturePath : null,
|
||||
'stato_ricezione' => 'confermato_online',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Ricezione confermata con successo!',
|
||||
'message' => 'Firma e ricezione registrate con successo!',
|
||||
'ricezione_confermata_at' => $convocazione->ricezione_confermata_at->format('d/m/Y H:i'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadZip(string $token)
|
||||
{
|
||||
$convocazione = Convocazione::where('token_accesso', $token)
|
||||
->with(['assemblea'])
|
||||
->firstOrFail();
|
||||
|
||||
$assemblea = $convocazione->assemblea;
|
||||
$odg = OrdineGiorno::where('assemblea_id', $assemblea->id)->get();
|
||||
|
||||
$zipFileName = 'allegati_assemblea_' . $assemblea->id . '.zip';
|
||||
$zipFilePath = storage_path('app/public/' . $zipFileName);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true) {
|
||||
$hasFiles = false;
|
||||
foreach ($odg as $punto) {
|
||||
if ($punto->allegati && is_array($punto->allegati)) {
|
||||
foreach ($punto->allegati as $file) {
|
||||
$fullPath = storage_path('app/public/' . $file['path']);
|
||||
if (file_exists($fullPath)) {
|
||||
$zip->addFile($fullPath, $punto->numero_punto . '_' . $file['nome']);
|
||||
$hasFiles = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
if ($hasFiles && file_exists($zipFilePath)) {
|
||||
return response()->download($zipFilePath)->deleteFileAfterSend(true);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'Nessun allegato disponibile per il download.');
|
||||
}
|
||||
|
||||
public function updateAnagrafica(Request $request, string $token)
|
||||
{
|
||||
$convocazione = Convocazione::where('token_accesso', $token)->firstOrFail();
|
||||
|
||||
$request->validate([
|
||||
'email' => 'nullable|email|max:255',
|
||||
'pec' => 'nullable|email|max:255',
|
||||
'telefono' => 'nullable|string|max:50',
|
||||
'indirizzo' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$soggetto = $convocazione->soggetto;
|
||||
$datiAttuali = [
|
||||
'email' => $soggetto->email,
|
||||
'pec' => $soggetto->pec,
|
||||
'telefono' => $soggetto->telefono,
|
||||
'indirizzo' => $soggetto->indirizzo_residenza ?? $soggetto->indirizzo,
|
||||
];
|
||||
|
||||
$datiProposti = [
|
||||
'email' => $request->input('email'),
|
||||
'pec' => $request->input('pec'),
|
||||
'telefono' => $request->input('telefono'),
|
||||
'indirizzo' => $request->input('indirizzo'),
|
||||
];
|
||||
|
||||
\App\Models\RichiestaModifica::create([
|
||||
'unita_immobiliare_id' => $convocazione->unita_immobiliare_id,
|
||||
'soggetto_richiedente_id' => $convocazione->soggetto_id,
|
||||
'tipo_modifica' => 'recapiti',
|
||||
'descrizione' => 'Aggiornamento recapiti conduttore/proprietario inviato dal portale assemblea.',
|
||||
'dati_attuali' => $datiAttuali,
|
||||
'dati_proposti' => $datiProposti,
|
||||
'stato' => 'in_attesa',
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Richiesta di variazione anagrafica inviata allo studio. Verrà esaminata a breve dall\'amministratore!',
|
||||
]);
|
||||
}
|
||||
|
||||
public function vota(Request $request, string $token)
|
||||
{
|
||||
$convocazione = Convocazione::where('token_accesso', $token)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ class Assemblea extends Model
|
|||
'data_convocazione',
|
||||
'data_svolgimento',
|
||||
'creato_da_user_id',
|
||||
'gmail_account',
|
||||
'video_registrazione_url',
|
||||
'google_calendar_event_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class AssembleaPresenza extends Model
|
|||
'ora_ingresso',
|
||||
'ora_uscita',
|
||||
'qr_code_token',
|
||||
'token_delega',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
|
|
|||
|
|
@ -20,11 +20,17 @@ class Convocazione extends Model
|
|||
'consegnato_at',
|
||||
'ricezione_confermata_at',
|
||||
'token_accesso',
|
||||
'ip_conferma',
|
||||
'user_agent_conferma',
|
||||
'firma_ricezione_percorso',
|
||||
'stato_ricezione',
|
||||
'archiviata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'consegnato_at' => 'datetime',
|
||||
'ricezione_confermata_at' => 'datetime',
|
||||
'archiviata' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -14,13 +14,19 @@ class OrdineGiorno extends Model
|
|||
protected $fillable = [
|
||||
'assemblea_id',
|
||||
'numero_punto',
|
||||
'ordinamento',
|
||||
'titolo',
|
||||
'descrizione',
|
||||
'allegati',
|
||||
'articolo_legge',
|
||||
'tipo_voce',
|
||||
'collegamento_preventivo_id',
|
||||
'importo_spesa',
|
||||
'tabella_millesimale_id',
|
||||
'maggioranza_richiesta',
|
||||
'riferimento_legge',
|
||||
'audio_log_path',
|
||||
'audio_log_trascrizione',
|
||||
'esito_votazione',
|
||||
'voti_favorevoli',
|
||||
'voti_contrari',
|
||||
|
|
@ -29,11 +35,14 @@ class OrdineGiorno extends Model
|
|||
'millesimi_contrari',
|
||||
'millesimi_astenuti',
|
||||
'note_delibera',
|
||||
'imported_to_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'numero_punto' => 'integer',
|
||||
'ordinamento' => 'integer',
|
||||
'importo_spesa' => 'decimal:2',
|
||||
'allegati' => 'array',
|
||||
'voti_favorevoli' => 'integer',
|
||||
'voti_contrari' => 'integer',
|
||||
'astenuti' => 'integer',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class Proprieta extends Model
|
|||
protected $table = 'proprieta';
|
||||
protected $fillable = [
|
||||
'unita_immobiliare_id',
|
||||
'soggetto_id',
|
||||
'anagrafica_id',
|
||||
'tipo_diritto',
|
||||
'percentuale_possesso',
|
||||
'percentuale_detrazione',
|
||||
|
|
@ -24,8 +24,12 @@ public function unitaImmobiliare()
|
|||
{
|
||||
return $this->belongsTo(UnitaImmobiliare::class, 'unita_immobiliare_id', 'id');
|
||||
}
|
||||
public function anagrafica()
|
||||
{
|
||||
return $this->belongsTo(Soggetto::class, 'anagrafica_id', 'id');
|
||||
}
|
||||
public function soggetto()
|
||||
{
|
||||
return $this->belongsTo(Soggetto::class, 'soggetto_id', 'id');
|
||||
return $this->anagrafica();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class Soggetto extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'soggetti';
|
||||
protected $table = 'anagrafiche';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -52,7 +52,7 @@ public function rateEmessaResponsabile(): HasMany
|
|||
|
||||
public function proprieta()
|
||||
{
|
||||
return $this->hasMany(Proprieta::class, 'soggetto_id', 'id');
|
||||
return $this->hasMany(Proprieta::class, 'anagrafica_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -60,7 +60,7 @@ public function proprieta()
|
|||
*/
|
||||
public function unitaImmobiliari()
|
||||
{
|
||||
return $this->belongsToMany(UnitaImmobiliare::class, 'proprieta', 'soggetto_id', 'unita_immobiliare_id')
|
||||
return $this->belongsToMany(UnitaImmobiliare::class, 'proprieta', 'anagrafica_id', 'unita_immobiliare_id')
|
||||
->withPivot('tipo_diritto', 'percentuale_possesso', 'data_inizio', 'data_fine')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,13 +409,18 @@ public function hasAutorizzazioniScadute()
|
|||
/**
|
||||
* I soggetti collegati a questa unità immobiliare
|
||||
*/
|
||||
public function soggetti()
|
||||
public function anagrafiche()
|
||||
{
|
||||
return $this->belongsToMany(Soggetto::class, 'proprieta', 'unita_immobiliare_id', 'soggetto_id')
|
||||
return $this->belongsToMany(Soggetto::class, 'proprieta', 'unita_immobiliare_id', 'anagrafica_id')
|
||||
->withPivot('tipo_diritto', 'percentuale_possesso', 'data_inizio', 'data_fine')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function soggetti()
|
||||
{
|
||||
return $this->anagrafiche();
|
||||
}
|
||||
|
||||
// Legacy compatibility methods
|
||||
public function getMillesimiProprietaAttribute()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -115,6 +115,102 @@ public function generaPagamentoDaMovimento(User $user, MovimentoBanca $movimento
|
|||
}
|
||||
|
||||
return DB::transaction(function () use ($user, $movimento, $stabileId, $gestioneId, $data, $descr, $amount, $contoFinanziarioId, $contoDebitoFornitore, $fornitoreId, $fattura) {
|
||||
$splitRegs = [];
|
||||
$righe = [];
|
||||
if ($fattura && Schema::hasTable('contabilita_fatture_fornitori_righe')) {
|
||||
$righe = DB::table('contabilita_fatture_fornitori_righe')
|
||||
->where('fattura_id', $fattura->id)
|
||||
->whereNotNull('gestione_id')
|
||||
->select('gestione_id', DB::raw('SUM(totale_euro) as totale'))
|
||||
->groupBy('gestione_id')
|
||||
->get();
|
||||
}
|
||||
|
||||
if (count($righe) > 1) {
|
||||
$totalInvoice = (float) $righe->sum('totale');
|
||||
if ($totalInvoice <= 0) {
|
||||
$totalInvoice = 1.0;
|
||||
}
|
||||
|
||||
$allocatedAmount = 0.0;
|
||||
$lastIndex = count($righe) - 1;
|
||||
|
||||
foreach ($righe as $index => $riga) {
|
||||
$gId = (int) $riga->gestione_id;
|
||||
if ($index === $lastIndex) {
|
||||
$splitAmount = round($amount - $allocatedAmount, 2);
|
||||
} else {
|
||||
$splitAmount = round($amount * ((float) $riga->totale / $totalInvoice), 2);
|
||||
$allocatedAmount += $splitAmount;
|
||||
}
|
||||
|
||||
if ($splitAmount <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$regPayload = [
|
||||
'stabile_id' => $stabileId,
|
||||
'gestione_id' => (Schema::hasColumn('contabilita_registrazioni', 'gestione_id')
|
||||
? Registrazione::resolveGestioneIdOrDefault($gId, $stabileId, $data)
|
||||
: null),
|
||||
'data_registrazione' => $data,
|
||||
'descrizione' => $descr . ' (Quota Gestione #' . $gId . ')',
|
||||
'created_by' => (int) $user->id,
|
||||
'updated_by' => (int) $user->id,
|
||||
];
|
||||
if (Schema::hasColumn('contabilita_registrazioni', 'user_id')) {
|
||||
$regPayload['user_id'] = (int) $user->id;
|
||||
}
|
||||
|
||||
/** @var Registrazione $reg */
|
||||
$reg = Registrazione::query()->create($regPayload);
|
||||
|
||||
// Dare: diminuisce il debito verso fornitore per la quota
|
||||
Movimento::query()->create([
|
||||
'registrazione_id' => (int) $reg->id,
|
||||
'conto_id' => (int) $contoDebitoFornitore->id,
|
||||
'tipo' => 'dare',
|
||||
'importo' => $splitAmount,
|
||||
]);
|
||||
|
||||
// Avere: diminuisce la banca/cassa per la quota
|
||||
Movimento::query()->create([
|
||||
'registrazione_id' => (int) $reg->id,
|
||||
'conto_id' => $contoFinanziarioId,
|
||||
'tipo' => 'avere',
|
||||
'importo' => $splitAmount,
|
||||
]);
|
||||
|
||||
$reg->assertBilanciata();
|
||||
$splitRegs[] = $reg;
|
||||
|
||||
$this->syncRegistroRitenutaDaPagamento($fattura, $gId, $data);
|
||||
}
|
||||
|
||||
if (! empty($splitRegs)) {
|
||||
$firstReg = $splitRegs[0];
|
||||
$movimento->registrazione_id = (int) $firstReg->id;
|
||||
$movimento->gestione_id = $firstReg->gestione_id;
|
||||
|
||||
$matchData = is_string($movimento->match_data) ? json_decode($movimento->match_data, true) : (is_array($movimento->match_data) ? $movimento->match_data : []);
|
||||
$matchData['split_registrazioni_ids'] = collect($splitRegs)->pluck('id')->all();
|
||||
$movimento->match_data = $matchData;
|
||||
|
||||
if (Schema::hasColumn('contabilita_movimenti_banca', 'fornitore_id')) {
|
||||
$movimento->fornitore_id = $fornitoreId;
|
||||
}
|
||||
$movimento->save();
|
||||
}
|
||||
|
||||
if ($fattura) {
|
||||
$fattura->data_pagamento = $fattura->data_pagamento ?: $data;
|
||||
$fattura->movimento_pagamento_id = (int) $movimento->id;
|
||||
$fattura->save();
|
||||
}
|
||||
|
||||
return empty($splitRegs) ? Registrazione::query()->create([]) : $splitRegs[0];
|
||||
}
|
||||
|
||||
$regPayload = [
|
||||
'stabile_id' => $stabileId,
|
||||
'gestione_id' => (Schema::hasColumn('contabilita_registrazioni', 'gestione_id')
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ public function panel(Panel $panel): Panel
|
|||
->renderHook(PanelsRenderHook::TOPBAR_LOGO_AFTER, fn() => view('filament.components.topbar-live-call'))
|
||||
->renderHook(PanelsRenderHook::USER_MENU_BEFORE, fn() => view('filament.components.topbar-icons'))
|
||||
->renderHook(PanelsRenderHook::FOOTER, fn() => view('filament.components.footer'))
|
||||
->renderHook(PanelsRenderHook::SIDEBAR_NAV_START, fn() => view('filament.components.sidebar-search'))
|
||||
->discoverPages(in: app_path('Filament/Pages'), for : 'App\\Filament\\Pages')
|
||||
->pages([
|
||||
Dashboard::class,
|
||||
|
|
|
|||
|
|
@ -231,18 +231,45 @@ private function loadVoci(int $stabileId, ?int $gestioneId, string $tipoGestione
|
|||
'sottoconto_pd',
|
||||
]);
|
||||
|
||||
return $rows->map(fn(object $row): array=> [
|
||||
'id' => (int) $row->id,
|
||||
'codice' => (string) ($row->codice ?? ''),
|
||||
'descrizione' => (string) ($row->descrizione ?? ''),
|
||||
'tabella_millesimale_default_id' => $row->tabella_millesimale_default_id ? (int) $row->tabella_millesimale_default_id : 0,
|
||||
'importo_default' => is_numeric($row->importo_default ?? null) ? (float) $row->importo_default : 0.0,
|
||||
'importo_consuntivo' => is_numeric($row->importo_consuntivo ?? null) ? (float) $row->importo_consuntivo : 0.0,
|
||||
'percentuale_condomino' => is_numeric($row->percentuale_condomino ?? null) ? (float) $row->percentuale_condomino : 100.0,
|
||||
'percentuale_inquilino' => is_numeric($row->percentuale_inquilino ?? null) ? (float) $row->percentuale_inquilino : 0.0,
|
||||
'conto_pd' => (string) ($row->conto_pd ?? ''),
|
||||
'sottoconto_pd' => (string) ($row->sottoconto_pd ?? ''),
|
||||
]);
|
||||
// Carica la mappa codice_tabella -> id delle tabelle millesimali per lo stabile ed esercizio corrente
|
||||
$annoGestione = DB::table('gestioni_contabili')->where('id', $gestioneId)->value('anno_gestione');
|
||||
$tabMap = [];
|
||||
if ($annoGestione) {
|
||||
$tabelleQuery = DB::table('tabelle_millesimali')
|
||||
->where('stabile_id', $stabileId);
|
||||
if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
|
||||
$tabelleQuery->where('anno_gestione', $annoGestione);
|
||||
}
|
||||
$tabMap = $tabelleQuery->pluck('id', 'codice_tabella')
|
||||
->mapWithKeys(fn($id, $cod) => [strtoupper(trim($cod)) => $id])
|
||||
->all();
|
||||
}
|
||||
|
||||
return $rows->map(function(object $row) use ($tabMap): array {
|
||||
$tabId = $row->tabella_millesimale_default_id ? (int) $row->tabella_millesimale_default_id : 0;
|
||||
$codice = (string) ($row->codice ?? '');
|
||||
|
||||
if ($tabId === 0 && str_contains($codice, '.')) {
|
||||
$parts = explode('.', $codice);
|
||||
$tabCode = strtoupper(trim(end($parts)));
|
||||
if (isset($tabMap[$tabCode])) {
|
||||
$tabId = $tabMap[$tabCode];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $row->id,
|
||||
'codice' => $codice,
|
||||
'descrizione' => (string) ($row->descrizione ?? ''),
|
||||
'tabella_millesimale_default_id' => $tabId,
|
||||
'importo_default' => is_numeric($row->importo_default ?? null) ? (float) $row->importo_default : 0.0,
|
||||
'importo_consuntivo' => is_numeric($row->importo_consuntivo ?? null) ? (float) $row->importo_consuntivo : 0.0,
|
||||
'percentuale_condomino' => is_numeric($row->percentuale_condomino ?? null) ? (float) $row->percentuale_condomino : 100.0,
|
||||
'percentuale_inquilino' => is_numeric($row->percentuale_inquilino ?? null) ? (float) $row->percentuale_inquilino : 0.0,
|
||||
'conto_pd' => (string) ($row->conto_pd ?? ''),
|
||||
'sottoconto_pd' => (string) ($row->sottoconto_pd ?? ''),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function buildTableSummary(Collection $voci, array $tabellaMap): array
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
use App\Services\Arera\AreraFornitoreClassifier;
|
||||
use App\Services\Catalog\FornitoreProductCatalogService;
|
||||
use App\Services\Consumi\AcquaContractSyncService;
|
||||
use App\Services\Consumi\AcquaPdfTextParser;
|
||||
use App\Services\FatturaParserService;
|
||||
use App\Services\Consumi\ConsumiAcquaIngestionService;
|
||||
use App\Services\Consumi\ConsumiAcquaTariffeIngestionService;
|
||||
use App\Support\ArchivioPaths;
|
||||
|
|
@ -953,8 +953,41 @@ private function extractPdfText(string $fullPath): ?string
|
|||
private function extractAcquaFornituraDataFromPdfText(string $text): ?array
|
||||
{
|
||||
try {
|
||||
$parsed = app(AcquaPdfTextParser::class)->parse($text);
|
||||
} catch (\Throwable) {
|
||||
$rawParsed = app(FatturaParserService::class)->parseText($text);
|
||||
$parsed = [
|
||||
'codici' => [
|
||||
'utenza' => $rawParsed['codice_utenza'] ?? null,
|
||||
'cliente' => $rawParsed['codice_cliente'] ?? null,
|
||||
'contratto' => $rawParsed['numero_contratto'] ?? null,
|
||||
],
|
||||
'contatore' => [
|
||||
'matricola' => $rawParsed['matricola_contatore'] ?? null,
|
||||
],
|
||||
'consumi' => [
|
||||
[
|
||||
'valore' => $rawParsed['quantita_consumata'] ?? null,
|
||||
'dal' => $rawParsed['data_inizio_periodo'] ?? null,
|
||||
'al' => $rawParsed['data_fine_periodo'] ?? null,
|
||||
]
|
||||
],
|
||||
'generale' => [
|
||||
'numero_fattura_pdf' => $rawParsed['numero_contratto'] ?? null,
|
||||
],
|
||||
'pagamento' => [
|
||||
'cbill' => $rawParsed['cbill'] ?? null,
|
||||
'codice_avviso' => $rawParsed['codice_avviso'] ?? null,
|
||||
],
|
||||
'riepilogo_letture' => [
|
||||
[
|
||||
'precedente' => $rawParsed['lettura_precedente'] ?? null,
|
||||
'attuale' => $rawParsed['lettura_attuale'] ?? null,
|
||||
]
|
||||
],
|
||||
'tariffe' => [],
|
||||
'quadro_dettaglio' => [],
|
||||
'iva' => [],
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ public function up()
|
|||
$table->string('cod_stabile', 10);
|
||||
$table->year('anno');
|
||||
$table->integer('id_cond');
|
||||
$table->string('cod_cond', 20)->nullable()->index();
|
||||
$table->date('data_pagamento');
|
||||
$table->decimal('importo_euro', 10, 2);
|
||||
$table->string('modalita_pagamento')->nullable(); // bonifico, assegno, contanti
|
||||
|
|
|
|||
|
|
@ -11,10 +11,20 @@
|
|||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$isSqlite = \Illuminate\Support\Facades\DB::connection()->getDriverName() === 'sqlite';
|
||||
|
||||
if ($isSqlite) {
|
||||
\Illuminate\Support\Facades\DB::statement('DROP VIEW IF EXISTS vw_rate_emesse_emissioni');
|
||||
}
|
||||
|
||||
Schema::table('piano_rateizzazione', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('ripartizione_spese_id')->nullable()->change();
|
||||
$table->unsignedBigInteger('creato_da')->nullable()->change();
|
||||
});
|
||||
|
||||
if ($isSqlite) {
|
||||
$this->createVwRateEmesseEmissioniSqlite();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,9 +32,43 @@ public function up(): void
|
|||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$isSqlite = \Illuminate\Support\Facades\DB::connection()->getDriverName() === 'sqlite';
|
||||
|
||||
if ($isSqlite) {
|
||||
\Illuminate\Support\Facades\DB::statement('DROP VIEW IF EXISTS vw_rate_emesse_emissioni');
|
||||
}
|
||||
|
||||
Schema::table('piano_rateizzazione', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('ripartizione_spese_id')->nullable(false)->change();
|
||||
$table->unsignedBigInteger('creato_da')->nullable(false)->change();
|
||||
});
|
||||
|
||||
if ($isSqlite) {
|
||||
$this->createVwRateEmesseEmissioniSqlite();
|
||||
}
|
||||
}
|
||||
|
||||
private function createVwRateEmesseEmissioniSqlite(): void
|
||||
{
|
||||
\Illuminate\Support\Facades\DB::statement(<<<SQL
|
||||
CREATE VIEW vw_rate_emesse_emissioni AS
|
||||
SELECT
|
||||
MIN(re.id) AS id,
|
||||
pr.stabile_id AS stabile_id,
|
||||
re.piano_rateizzazione_id AS piano_rateizzazione_id,
|
||||
re.numero_rata_progressivo AS numero_rata_progressivo,
|
||||
re.data_emissione AS data_emissione,
|
||||
re.data_scadenza AS data_scadenza,
|
||||
SUM(re.importo_addebitato_soggetto) AS totale_importo,
|
||||
COUNT(*) AS righe_count
|
||||
FROM rate_emesse re
|
||||
INNER JOIN piano_rateizzazione pr ON pr.id = re.piano_rateizzazione_id
|
||||
GROUP BY
|
||||
pr.stabile_id,
|
||||
re.piano_rateizzazione_id,
|
||||
re.numero_rata_progressivo,
|
||||
re.data_emissione,
|
||||
re.data_scadenza
|
||||
SQL);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
166
directives/sop_importazione_sperimentale_stabile.md
Normal file
166
directives/sop_importazione_sperimentale_stabile.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# SOP - Importazione Sperimentale Stabile e Gestione Anagrafica Avanzata (Fase 7)
|
||||
|
||||
Questo documento definisce i criteri per l'amministrazione interamente via interfaccia grafica Web, la logica di allineamento dinamico degli anni e la mappatura dei campi avanzati della tabella legacy `condomin` con le relative regole di business.
|
||||
|
||||
---
|
||||
|
||||
## 1. Paradigma Cloud & Amministrazione Web (No CLI / Terminale)
|
||||
|
||||
Il sistema NetGescon è progettato per essere interamente amministrato via interfaccia Web, rendendo l'applicazione Docker-ready e portabile:
|
||||
- **Punto di Accesso**: L'allineamento automatico, il pull da Git e il ripristino/aggiornamento della struttura DB vengono eseguiti esclusivamente tramite la pagina grafica:
|
||||
`/admin-filament/gescon/importazione-archivi`.
|
||||
- **Hot Backup SQLite**: Ogni stabile genera a caldo il proprio file SQLite (es. `stabile_0021.sqlite`). Questo archivio granulare garantisce l'elaborazione locale offline, consentendo la sincronizzazione e il ripristino di singole righe di pagamento o fatture via Google Drive o OneDrive.
|
||||
|
||||
---
|
||||
|
||||
## 2. Allineamento Dinamico Esercizi (1:1 Speculare)
|
||||
|
||||
Gli anni e i periodi non sono codificati in modo statico:
|
||||
1. Lo script interroga la tabella master `[anni]` all'interno del file `/mnt/gescon-archives/gescon/0021/generale_stabile.mdb`.
|
||||
2. Identifica l'**ultima gestione disponibile** (`anno_o` / `anno_r`) e la directory ad essa associata (`nome_dir`).
|
||||
3. Procede a ritroso caricando gli esercizi precedenti, garantendo che le anagrafiche e i saldi siano speculari allo stato del legacy contabile.
|
||||
|
||||
---
|
||||
|
||||
## 3. Matrice Completa di Mapping `condomin` (Raw MDB)
|
||||
|
||||
Di seguito è dettagliato lo schema di associazione per tutti i campi censiti nella tabella `condomin` dello stabile 0021:
|
||||
|
||||
| Campo Legacy `condomin` | Modello / Tabella NetGescon | Regola di Trasformazione |
|
||||
| :--- | :--- | :--- |
|
||||
| `id_cond` | `unita_immobiliari.legacy_id` | Riferimento chiave primaria dell'unità legacy. |
|
||||
| `cod_cond` | `rubrica_universale.codice_univoco` | Codice alfanumerico del soggetto. |
|
||||
| `scala` / `int` / `piano` | `unita_immobiliari` | Posizione spaziale dell'unità. |
|
||||
| `nom_cond` | `rubrica_universale.ragione_sociale` | Denominazione unificata condomino/proprietario. |
|
||||
| `presso` / `inquil_presso` | `rubrica_indirizzi.care_of` (o `c_o`) | Mappato come intestatario c/o (Care Of) per le spedizioni delle buste cartacee. |
|
||||
| `ind` / `cap` / `citta` / `pr` | `rubrica_indirizzi` | Indirizzo di residenza/spedizione del condomino. |
|
||||
| `tel1` / `tel2` / `Cell_cond` / `Fax_cond` | `rubrica_universale` | Numeri di telefono, cellulare e fax del condomino. |
|
||||
| `E_mail_condomino` / `PEC_condomino` | `rubrica_universale` | Email e PEC del condomino per invii digitali. |
|
||||
| `inquil_nome` | `rubrica_universale` | Nominativo dell'inquilino/occupante. |
|
||||
| `inquil_indir` / `inquil_cap` / `inquil_citta` / `inquil_pr` | `rubrica_indirizzi` | Indirizzo completo dell'inquilino. |
|
||||
| `inquil_tel1` / `inquil_tel2` / `Cell_inq` / `Fax_inq` | `rubrica_universale` | Recapiti telefonici dell'inquilino. |
|
||||
| `E_mail_inquilino` / `PEC_inquilino` | `rubrica_universale` | Email e PEC dell'inquilino. |
|
||||
| `inquil_dal` / `inquil_al` | `unita_anagrafica_periodo` | Date d'inizio e fine locazione per subentri. |
|
||||
| `subentrato_dal` / `attivo_fino_al` | `unita_anagrafica_periodo` | Date di variazione possesso dell'unità. |
|
||||
| `subentro_prima_cera` / `subentro_adesso_ce` | `unita_anagrafica_periodo.meta` | Audit testuale dei condòmini uscenti/subentranti. |
|
||||
| `cumulo_cond` / `E_lostesso_Di` | `unita_pertinenze` | `E_lostesso_Di` punta all'unità principale (`id_cond`) per il cumulo pertinenze. |
|
||||
| `Cumulo_ass` / `Cumulo_elenchi` | `unita_pertinenze` | Regola l'unificazione del voto assembleare a "Testa Singola". |
|
||||
| `titolo_cond` / `titolo_inq` | `rubrica_universale.titolo` | Titolo di cortesia (es. Egr. Sig., Dott.). |
|
||||
| `Selez_mail_ASS_cond` / `Selez_spediz_ASS_cond` | `rubrica_universale.canale_notifiche` | Preferenze di spedizione: se mail "Si" ➔ digitale (Email/PEC); se spediz "Si" ➔ cartaceo (Posta/Raccomandata). |
|
||||
| `Ricorda_che_Cond` | `rubrica_universale.note_promemoria_proprietario` | Campo text visualizzato como popup Filament all'operatore. |
|
||||
| `Ricorda_che_Inq` | `rubrica_universale.note_promemoria_inquilino` | Campo text visualizzato como popup Filament all'operatore. |
|
||||
| `Cond_cod_fisc` / `Inquil_cod_fisc` | `rubrica_universale.codice_fiscale` | Codici fiscali di condòmino ed inquilino, validati tramite Regex italiana (con log in caso di errore). |
|
||||
| `Cond_dt_nasc` / `Cond_Luogo_nasc` | `rubrica_universale` | Data e luogo di nascita per adempimenti fiscali. |
|
||||
| `Catasto_sez_Urbana` / `Catasto_foglio` / `Catasto_particella` / `Catasto_sub` | `unita_immobiliari.dati_catastali` | Dati catastali dell'unità (Sezione, Foglio, Particella, Subalterno) per Quadro AC. |
|
||||
| `Catasto_Rendita` / `Catasto_superfice` | `unita_immobiliari` | Rendita catastale e superficie per dichiarazioni. |
|
||||
| `Diritto_reale` / `Diritto_godimento` | `unita_anagrafica_periodo.tipo_diritto` | Definisce il ruolo giuridico e la sussidiarietà per rate scadute. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Mappatura File Master `generale_stabile.mdb`
|
||||
|
||||
Per blindare la quadratura finanziaria e riconciliare i solleciti con il dovuto reale, la migrazione integra la tabella master di allineamento:
|
||||
|
||||
### A. Tabella `[emes_gen]` e `[emes_det]` (Emissione Rate)
|
||||
* **`emes_gen`**: `id_emissione`, `data_emissione`, `descrizione_emissione` ➔ `rate_emissioni_master`. Mantiene la cronologia delle emissioni deliberate.
|
||||
* **`emes_det`**: `id_unita`, `num_rata`, `importo_richiesto`, `scadenza` ➔ `rate_scadenze`. Mappa il dovuto di cassa associato alla singola unità.
|
||||
|
||||
### B. Tabella `[inc_da_ec]` (Incassi Estratto Conto)
|
||||
* **`num_incasso` / `data_incasso` / `importo_incassato`** ➔ `movimenti_cassa` (Partita Doppia: Dare banca, Avere crediti condòmini).
|
||||
* **`nome_file_pdf`** ➔ `campo_audit.codice_verifica_pdf`. Associa la registrazione all'estratto conto stampato.
|
||||
* **`num_incasso`** ➔ Si aggancia alla tabella `incassi.n_riferimento` del file `singolo_anno.mdb` (trovato seguendo `anni.nome_dir`), assicurando la continuità contabile dell'anno.
|
||||
|
||||
### C. Tabella `[protoc_ec]` (Registro Spedizioni)
|
||||
* **`id_protocollo` / `data_invio` / `tipo_comunicazione`** (Sollecito/Estratto Conto) ➔ `registro_protocollo_comunicazioni`.
|
||||
* **`nome_pdf`** ➔ `percorso_documentale_allegato`.
|
||||
* **`cod_condomino` / `id_unita`** ➔ Collegati alle anagrafiche e alle unità immobiliari per consentire il confronto a schermo tra l'estratto conto cartaceo d'epoca e quello calcolato a runtime.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sequenza Esecutiva Importazione Core (Filament Orchestrator)
|
||||
|
||||
L'importazione dello stabile pilota 0021 viene avviata dalla pagina `/admin-filament/gescon/importazione-archivi` e segue questi step deterministici:
|
||||
|
||||
* **STEP 0 (Fondamenta)**:
|
||||
1. Lookup ed allineamento dell'anagrafica Amministratore / Studio (Multi-Tenant a 8 cifre).
|
||||
2. Importazione dell'elenco completo dei Fornitori (da `Fornitori.mdb`) compilando le preferenze fiscali, aliquote ritenuta `rit_95100`, codici tributo F24 `Trib_1019_1020` ed IBAN d'appoggio.
|
||||
* **STEP 1 (Esercizi & Stabile)**:
|
||||
1. Importazione anagrafica Stabile 0021, coordinate bancarie e SMTP/PEC dedicati.
|
||||
2. Lettura dinamica della tabella `[anni]` di `generale_stabile.mdb` per risolvere l'ultima gestione e le directory anno su `/mnt/gescon-archives/gescon/`.
|
||||
* **STEP 2 (Unità & Timeline Persone)**:
|
||||
1. Creazione delle entità fisse `unita_immobiliari` mediante algoritmo di de-duplicazione spaziale.
|
||||
2. Importazione ed associazione delle anagrafiche Proprietari (C), Inquilini (I) e Comproprietari con timeline temporale in `unita_anagrafica_periodo` a ritroso per tutti gli anni storici.
|
||||
* **STEP 3 (Millesimi & Piano dei Conti)**:
|
||||
1. Collegamento permanente delle tabelle millesimali (Mastri) alle unità.
|
||||
2. Associazione delle voci di spesa (Sottoconti) mappate per ciascun anno per conservare variazioni di descrizione ed importo.
|
||||
|
||||
Tutte le anomalie riscontrate (CF errati, PDF mancanti) vengono loggate in `storage/logs/migrazione_test.log`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Sincronizzazione Incrementale 1:1 (Update/Merge No-Duplicates)
|
||||
|
||||
Per evitare la duplicazione dei dati in caso di esecuzioni ripetute dell'importatore, il sistema adotta le seguenti politiche:
|
||||
1. **De-duplicazione e Upsert**: Ogni record importato (Anagrafiche, Fornitori, Unità, Millesimi, Voci di spesa) viene confrontato con le chiavi univoche ereditate dal legacy (es. `legacy_id`, `cod_cond`, `cod_forn`).
|
||||
2. **Aggiornamento Speculare**: Se il record esiste già, viene eseguito un aggiornamento (`Update/Merge`) delle sole colonne variate, mantenendo intatti gli ID primari interni (`id` autoincrementale di MySQL) e le relazioni consolidate (FK).
|
||||
3. **Preservazione dei Dati Locali**: Eventuali arricchimenti inseriti direttamente in NetGescon non vengono sovrascritti, a meno che il campo corrispondente nel legacy non sia stato esplicitamente modificato.
|
||||
|
||||
---
|
||||
|
||||
## 7. Filosofia Interfaccia Utente: CRUD In-line (No Modal)
|
||||
|
||||
Per la successiva gestione dei dati di base importati, l'interfaccia grafica NetGescon adotta una filosofia a zero pop-up:
|
||||
* **Modifica In-place**: Tutte le maschere Filament per Anagrafiche, Fornitori, Unità e Spese utilizzano componenti editabili integrati direttamente nelle righe delle tabelle o all'interno delle schede (Tab) della pagina corrente.
|
||||
* **No Modal**: È vietato l'uso di finestre modali o pop-up per le operazioni di inserimento e modifica, consentendo all'utente di lavorare in modo continuo e concentrato sul contesto visivo originale.
|
||||
|
||||
---
|
||||
|
||||
## 8. Indipendenza e Transizione da Legacy
|
||||
|
||||
L'architettura dei dati (Tabelle Millesimali come Conti, Spese come Sottoconti, Timeline Unità) è progettata in modo da garantire l'autonomia di NetGescon:
|
||||
* Una volta completata la migrazione storica degli archivi d'epoca, lo stabile può essere scollegato definitivamente dal vecchio Gescon.
|
||||
* Le gestioni future verranno inserite ed elaborate nativamente in Partita Doppia direttamente nell'interfaccia di NetGescon, senza alcuna dipendenza dai file MDB o dalle vecchie strutture dati.
|
||||
|
||||
---
|
||||
|
||||
## 9. Vincoli Rigidi Contabili (Rimozione Risolutore Semantico)
|
||||
|
||||
Per garantire un approccio deterministico matematico all'importazione dei millesimi e delle spese, è vietato l'uso di qualsiasi risolutore semantico o euristiche basate su parole chiave per abbinare le gestioni straordinarie:
|
||||
1. **Identificazione della Gestione**:
|
||||
- Il tipo di gestione viene ricavato esclusivamente dal campo `tipologia` di `tabelle_millesimali` di staging (`'O'` Ordinaria, `'R'` Riscaldamento, `'S'` Straordinaria).
|
||||
- Per le tabelle millesimali o le spese straordinarie, il campo `dett_tab.n_stra` e `straordinarie.codice` identificano in modo rigido e deterministico l'ID numerico sequenziale della gestione straordinaria (es. da 1 a 5 per lo stabile 0021).
|
||||
2. **Eliminazione degli Helper Semantici**:
|
||||
- Gli helper `matchExtraordinaryNumber` e `inferStraordinariaSequence` sono stati completamente rimossi e sostituiti da un abbinamento basato rigidamente sulle chiavi numeriche reali presenti nel database di staging.
|
||||
|
||||
---
|
||||
|
||||
## 10. Geometria Fisica Reale 0021 e Rincorsa Codici
|
||||
|
||||
Per la de-duplicazione spaziale e il tracciamento dei subentri storici dello stabile 0021, si applicano le seguenti regole tassative:
|
||||
1. **Geometria Fisica Reale dello Stabile 0021 (212 Unità Immobiliari Reali)**:
|
||||
Lo stabile non ha 221 o 239 unità, ma esattamente 212 unità fisiche, così composte:
|
||||
- **Palazzina A**: 27 interni scala A + 1 interno speciale + 6 altri locali (box/cantine) = 34 unità totali.
|
||||
- **Palazzina B**: 27 interni scala B + 1 interno speciale + 6 altri locali = 34 unità totali.
|
||||
- **Palazzina C**: 27 interni scala C + 1 interno speciale + 6 altri locali = 34 unità totali.
|
||||
- **Palazzina D**: 27 interni scala D + 1 interno speciale + 6 altri locali = 34 unità totali.
|
||||
- **Altre pertinenze e locali accessori**: 76 unità.
|
||||
Per garantire la composizione corretta, la query di estrazione `currentSnapshotCondominQuery()` caricherà tutte le unità storicamente registrate, senza filtrare per il solo anno di snapshot più recente (che escluderebbe unità chiuse o storiche).
|
||||
2. **Algoritmo di Rincorsa dei Codici**:
|
||||
- **Ancoraggio su Unità Fisica**: I subentri e i passaggi storici sono collegati all'unità fisica immutabile.
|
||||
- **Mappatura Chiave Incassi**: Il campo `legacy_cond_id` in `unita_immobiliari` e in `unita_immobiliare_nominativi` è mappato rigidamente su `condomin.cod_cond` (codice stabile nel tempo usato per le rate), slegandolo dall'ID record temporaneo annuale `condomin.id_cond`.
|
||||
- **Storico Comproprietà**: Viene mantenuto il join storico tra `condomin.id_cond` e `comproprietari.id_cond` per ricostruire le percentuali e i diritti dei comproprietari associati ad ogni specifico esercizio annuale.
|
||||
3. **Mappatura Cumuli e Diritto Reale**:
|
||||
- I campi `cumulo_cond`, `cumulo_inq`, `cumulo_cond_orig`, `cumulo_inq_orig` e `e_lostesso_di` vengono salvati nel payload JSON (`legacy_payload`) dell'anagrafica.
|
||||
- Si legge dynamicamente la presenza delle colonne nel database SQLite di staging tramite `hasColumn()`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Modulo Importatore Contatori e Auto-Apprendimento Associazioni Seriali
|
||||
|
||||
Il sistema gestisce l'importazione di letture da file CSV esterni per i contatori di consumo:
|
||||
1. **Parsing CSV Flessibile**: Il modulo accetta file CSV contenenti matricole e indicazioni di interno non normalizzate.
|
||||
2. **Staging degli Orfani (`contatori_orfani_staging`)**: Se una matricola non corrisponde ad alcuna unità immobiliare censita, la lettura viene inserita nello staging temporaneo degli orfani.
|
||||
3. **Associazione In-line & Auto-Apprendimento**: Tramite la Tab "Orfani Contatori" in `/servizi-utenze`, l'amministratore associa manualmente la matricola all'unità corretta. Questa azione aggiorna permanentemente la colonna `acqua_contatore_seriale` del modello `UnitaImmobiliare` (auto-apprendimento).
|
||||
4. **Importazioni Successive**: Ai successivi caricamenti di file CSV contenenti la stessa matricola, il sistema riconoscerà l'associazione in modo deterministico e caricherà le letture direttamente nella tabella `stabile_servizio_letture`.
|
||||
|
||||
|
||||
197
directives/sop_struttura_legacy_e_anagrafica_avanzata.md
Normal file
197
directives/sop_struttura_legacy_e_anagrafica_avanzata.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# 📑 SOP - STRUTTURA DIZIONARIO LEGACY E ANAGRAFICA AVANZATA
|
||||
|
||||
> **Documento di Livello 1 (SOP Esecutiva / Dizionario Dati)**
|
||||
> Tracciato record di confronto e dizionario dei campi raw estratti dal database di staging per la validazione catastale ed anagrafica avanzata.
|
||||
|
||||
---
|
||||
|
||||
## 1. Mappatura Campi Raw 'condomin' (Legacy)
|
||||
|
||||
I campi esportati dall'archivio `.mdb` originario costituiscono la sorgente immutabile di riscontro a schermo per l'operatore. Il sistema presenta in sola lettura questo set strutturato nel pannello di riscontro.
|
||||
|
||||
### A. Dati Unità Fissa
|
||||
- `id_cond`: Chiave primaria originaria del condomino.
|
||||
- `cod_cond`: Codice sequenziale interno all'anno.
|
||||
- `scala`: Scala di appartenenza dell'unità.
|
||||
- `interno` (o `int`): Numero identificativo dell'interno.
|
||||
- `tipo_pr`: Tipologia di proprietà/ruolo (es: Proprietario, Inquilino, ATER).
|
||||
|
||||
### B. Catasto & Diritti Core
|
||||
- `catasto_sez_urbana`: Sezione urbana catastale dell'immobile.
|
||||
- `catasto_foglio`: Numero del foglio catastale.
|
||||
- `catasto_particella`: Numero della particella catastale.
|
||||
- `catasto_sub`: Subalterno dell'unità immobiliare.
|
||||
- `catasto_zona`: Zona censuaria di appartenenza.
|
||||
- `catasto_categoria`: Categoria catastale (es. A/2, C/6).
|
||||
- `catasto_classe`: Classe di rendimento catastale.
|
||||
- `catasto_consistenza`: Consistenza (es. vani, mq).
|
||||
- `catasto_superfice`: Superficie catastale dichiarata.
|
||||
- `catasto_rendita`: Rendita catastale.
|
||||
- `diritto_reale`: Codifica del diritto reale (es: P per Proprietà, U per Usufrutto).
|
||||
- `diritto_godimento`: Eventuale diritto di godimento (es. Locazione).
|
||||
- `catasto_tu`: Tipologia d'uso catastale.
|
||||
- `catasto_particella2`: Eventuale particella secondaria per catasto tavolare.
|
||||
- `perc_diritto_reale`: Quota/Percentuale del diritto reale posseduto (es. 100%, 50%).
|
||||
|
||||
### C. Pertinenze
|
||||
- `pertinenze_box`: Flag/Dettaglio box auto associato.
|
||||
- `pertinenze_cant`: Flag/Dettaglio cantina associata.
|
||||
- `pertinenze_pauto`: Flag/Dettaglio posto auto assegnato.
|
||||
- `pertinenze_altro`: Altre pertinenze collegate.
|
||||
|
||||
### D. Fisco, Sicurezza & Detrazioni
|
||||
- `sicur1_ui_a_norma`...`sicur5_opere_impegno`: Stato di conformità impianti e opere.
|
||||
- `perc_detrazione`: Quota percentuale di detrazione fiscale spettante.
|
||||
- `detraz_sit_part`: Situazioni particolari per le detrazioni.
|
||||
- `disponib_dati_catastali`: Flag disponibilità coordinate catastali.
|
||||
- `domandaaccatast_n` / `data` / `pr`: Estremi della domanda di accatastamento.
|
||||
- `detraz_cess_cred`: Stato della cessione del credito fiscale.
|
||||
- `detraz_cess_cf`: Codice fiscale del cessionario del credito.
|
||||
- `detraz_cess_nome`: Nome/Denominazione del cessionario.
|
||||
- `detraz_cess_protoc`: Protocollo telematico di cessione.
|
||||
|
||||
### E. Anagrafica Condomino (Ruolo C - Proprietario)
|
||||
- `nom_cond`: Cognome e Nome / Denominazione del condomino.
|
||||
- `presso`: Recapito presso terzi.
|
||||
- `ind`: Indirizzo di residenza.
|
||||
- `cap`: Codice di Avviamento Postale.
|
||||
- `citta`: Città di residenza.
|
||||
- `pr`: Provincia di residenza.
|
||||
- `inquil`: Flag/Collegamento ad eventuale inquilino.
|
||||
- `tel1` / `tel2` / `cell_cond` / `fax_cond`: Recapiti telefonici e telefax.
|
||||
- `note_cond`: Note descrittive libere associate all'anagrafica.
|
||||
- `titolo_cond`: Titolo formale (es. Egr. Sig., Dott.).
|
||||
- `e_mail_condomino`: Indirizzo email ordinario del condomino.
|
||||
- `pec_condomino`: Indirizzo PEC.
|
||||
- `cond_cod_fisc`: Codice Fiscale del condomino.
|
||||
- `cond_dt_nasc` / `luogo_nasc` / `pr_nasc`: Dati di nascita del condomino.
|
||||
- `cc_banca_cond` / `banca_cond`: Estremi bancari per rimborsi o addebiti.
|
||||
- `mav_cond` / `bonifico_cond`: Preferenze di pagamento del condomino.
|
||||
|
||||
### F. Anagrafica Inquilino (Ruolo I - Inquilino)
|
||||
- `inquil_nome`: Nominativo completo dell'inquilino.
|
||||
- `inquil_presso` / `inquil_indir` / `inquil_cap` / `inquil_citta` / `inquil_pr`: Indirizzo dell'inquilino.
|
||||
- `inquil_tel1` / `inquil_tel2` / `cell_inq` / `fax_inq`: Recapiti telefonici.
|
||||
- `inquil_note`: Annotazioni specifiche sull'inquilino.
|
||||
- `titolo_inq`: Titolo formale dell'inquilino.
|
||||
- `e_mail_inquilino` / `pec_inquilino`: Indirizzi email e PEC.
|
||||
- `cc_banca_inq` / `banca_inq`: Coordinate bancarie dell'inquilino.
|
||||
- `mav_inq` / `bonifico_inq`: Canali di pagamento scelti dall'inquilino.
|
||||
- `inquil_dal` / `inquil_al`: Periodo temporale di occupazione/locazione.
|
||||
- `inquil_cod_fisc`: Codice Fiscale dell'inquilino.
|
||||
- `inquil_contratto_dal`: Decorrenza formale del contratto registrato.
|
||||
|
||||
---
|
||||
|
||||
## 2. Tabelle Addizionali Core Legacy
|
||||
|
||||
### A. TABELLA [anni]
|
||||
Contiene i periodi e le impostazioni degli anni contabili/gestione.
|
||||
- `id_anno`: ID univoco progressivo dell'anno.
|
||||
- `anno_o`: Anno di inizio gestione ordinaria.
|
||||
- `anno_r`: Anno di inizio gestione riscaldamento.
|
||||
- `nome_dir`: Nome della directory contenente i database e i file dell'anno.
|
||||
- `Selez`: Flag per identificare l'anno correntemente selezionato/attivo.
|
||||
- `descr_selez`: Descrizione testuale dello stato di selezione.
|
||||
- `ordinarie_dal` / `ordinarie_al`: Periodo di validità delle spese ordinarie.
|
||||
- `riscald_dal` / `riscald_al`: Periodo di validità delle spese di riscaldamento.
|
||||
|
||||
### B. TABELLA [Stabili]
|
||||
Contiene le informazioni anagrafiche, fiscali e gestionali dello Stabile.
|
||||
- `id_stabile`: Identificativo univoco dello stabile.
|
||||
- `cod_stabile`: Codice legacy alfanumerico (es. `0021`).
|
||||
- `denominazione`: Nome completo del condominio.
|
||||
- `indirizzo`: Indirizzo dello stabile.
|
||||
- `cap`: Codice Avviamento Postale.
|
||||
- `citta`: Città di ubicazione.
|
||||
- `pr`: Provincia di ubicazione.
|
||||
- `codice_fisc`: Codice fiscale del condominio.
|
||||
- `pos_inps`: Codice posizione INPS associata.
|
||||
- `n_contribuente`: Numero contribuente dello stabile.
|
||||
- `cf_amministratore`: Codice fiscale dell'amministratore dello stabile.
|
||||
- `num_condomini`: Numero di condomini censiti nello stabile.
|
||||
- `num_scale`: Numero di scale presenti.
|
||||
- `note1`: Note aggiuntive generali.
|
||||
- `nome_directory`: Nome della cartella dello stabile.
|
||||
|
||||
### C. SCADENZARI E BANCHE STABILE
|
||||
- `ORD_RATA_1`...`12`: Date di scadenza per le 12 rate ordinarie.
|
||||
- `RIS_RATA_1`...`12`: Date di scadenza per le 12 rate riscaldamento.
|
||||
- `num_ccp`: Numero di conto corrente postale dello stabile.
|
||||
- `intestaz_ccp`: Intestazione del conto corrente postale.
|
||||
- `Autoriz_pptt`: Numero di autorizzazione delle Poste Italiane.
|
||||
- `Banca`: Nome dell'istituto bancario di appoggio dello stabile.
|
||||
- `Banca_num_cc`: Numero del conto corrente bancario.
|
||||
- `Banca_intest_cc`: Intestazione del conto corrente bancario.
|
||||
- `ABI` / `CAB`: Codici ABI e CAB della banca dello stabile.
|
||||
- `SIA` / `CIN`: Codici SIA e CIN per le disposizioni interbancarie.
|
||||
- `INPS_sede_F24`: Sede INPS di competenza per deleghe F24.
|
||||
- `INPS_Matricola_F24`: Matricola INPS per F24.
|
||||
- `INAIL_sede_F24`: Sede INAIL di competenza per deleghe F24.
|
||||
- `INAIL_posiz_f24` / `INAIL_posiz2_f24`: Posizioni INAIL per F24.
|
||||
- `IBAN_Banca`: Codice IBAN del conto corrente bancario.
|
||||
- `IBAN_Posta`: Codice IBAN del conto corrente postale.
|
||||
|
||||
### D. CATASTO GLOBALE STABILE
|
||||
- `AC_tu`: Destinazione d'uso globale catastale dello stabile.
|
||||
- `AC_ip`: Identificatore provvisorio.
|
||||
- `AC_urb_cat`: Codice sezione urbana globale.
|
||||
- `AC_Foglio`: Foglio catastale globale dello stabile.
|
||||
- `AC_partic1` / `AC_partic2`: Particelle catastali associate allo stabile.
|
||||
- `AC_sub`: Subalterno globale.
|
||||
- `AC_data_acc` / `AC_num_acc` / `AC_prov_acc`: Estremi dell'accatastamento.
|
||||
- `PT_CIN` / `PT_SIA`: Credenziali postali dedicate.
|
||||
- `F24_SIA`: Codice SIA per addebiti F24.
|
||||
- `Catasto_comune`: Codice catastale del comune.
|
||||
- `Catasto_PR`: Sigla provincia catastale.
|
||||
|
||||
### E. FATTURE & PEC STABILE
|
||||
- `FE_denominazione`: Denominazione fiscale dello stabile per la fatturazione elettronica.
|
||||
- `FE_Codice_destinatario`: Codice destinatario SDI dello stabile.
|
||||
- `FE_pec`: Indirizzo PEC dello stabile.
|
||||
- `Autoriz_pptt_pdf` / `Autoriz_pptt_pdf_2`: Riferimenti ai documenti autorizzativi.
|
||||
- `CUC` / `CUC2` / `CUC3` / `CUC4`: Codici Univoci di Codifica.
|
||||
- `Th_mail_mittente` / `Th_PEC_mittente`: Account mittente per comunicazioni.
|
||||
- `FE_SN`: Numero di serie del servizio di fatturazione elettronica.
|
||||
- `FE_attivo_fino_al`: Data di scadenza del servizio di fatturazione.
|
||||
- `FE_ultima_richiesta_al`: Timestamp dell'ultima interrogazione SDI.
|
||||
|
||||
### F. TABELLA [Comproprietari]
|
||||
Contiene l'anagrafica dei comproprietari o cointestatari dei diritti sulle unità.
|
||||
- `Id_compr`: ID univoco del comproprietario.
|
||||
- `id_cond`: Collegamento all'unità legacy (`condomin.id`).
|
||||
- `Diritto_reale`: Tipo di diritto reale (es: P, U).
|
||||
- `Descriz`: Descrizione testuale del diritto (es. Comproprietario, Usufruttuario).
|
||||
- `titolo_cond`: Titolo formale del comproprietario.
|
||||
- `nom_cond`: Cognome e Nome o denominazione del comproprietario.
|
||||
- `presso`: Recapito presso terzi.
|
||||
- `ind`: Indirizzo di residenza.
|
||||
- `cap` / `citta` / `pr`: Dati di recapito del comproprietario.
|
||||
- `Cond_dt_nasc` / `Cond_Luogo_nasc` / `Cond_PR_Nasc`: Dati di nascita.
|
||||
- `tel1` / `tel2` / `Cell_cond` / `Fax_cond`: Contatti del comproprietario.
|
||||
- `E_mail_condomino` / `PEC_compr`: Email e PEC del comproprietario.
|
||||
- `Cond_cod_fisc`: Codice Fiscale del comproprietario.
|
||||
- `Perc_Diritto_reale`: Percentuale di possesso del diritto reale (es. 50.00).
|
||||
- `cia`: Flag o nota interna.
|
||||
- `Perc_Detrazione`: Percentuale di detrazione fiscale spettante al comproprietario.
|
||||
- `Detraz_Sit_Part`: Situazioni particolari per le detrazioni.
|
||||
- `Catasto_sez_Urbana` / `Catasto_foglio` / `Catasto_particella` / `Catasto_sub` / `Catasto_TU` / `Catasto_particella2`: Coordinate catastali dichiarate per il comproprietario.
|
||||
- `DomandaAccatast_N` / `DomandaAccatast_data` / `DomandaAccatast_PR`: Domanda di accatastamento del comproprietario.
|
||||
- `ex_cod_cond` / `ex_scala` / `ex_int`: Riferimenti storici di posizionamento dell'unità.
|
||||
- `Detraz_cess_cred` / `Detraz_cess_CF` / `Detraz_cess_nome` / `Detraz_cess_protoc`: Dettagli sulla cessione del credito del comproprietario.
|
||||
- `ex_tipo_pr`: Tipo proprietà storico.
|
||||
- `Data_cessione_credito`: Decorrenza formale di cessione credito.
|
||||
|
||||
---
|
||||
|
||||
## 3. Blocco Sicurezza Sola Lettura Database
|
||||
|
||||
Per blindare la stabilità del database di produzione (`netgescon`) durante la fase di validazione umana:
|
||||
|
||||
1. **Inibizione Scritture**:
|
||||
- I metodi di salvataggio in-place del controller `CatastoHub.php` (`applicaECorreggi`, `salvaQuotaProprietario`) **NON** devono eseguire query `INSERT`, `UPDATE` o `DELETE` su tabelle del database di produzione.
|
||||
2. **Storicizzazione in Cloud Drive (Anno Consolidato 0004)**:
|
||||
- I dati di riscontro confermati o modificati dall'operatore a schermo devono essere salvati in formato JSON all'interno di un file strutturato posizionato nella directory dell'anno 0004:
|
||||
`[Codice_Amministratore]/[Codice_Stabile]/catasto/anno_0004/validati_unita_[id_unita].json`
|
||||
e `quote_unita_[id_unita].json`.
|
||||
- All'apertura della scheda unità, il sistema carica e applica in overlay questi dati storicizzati, consentendo di visualizzarli ed alterarli in totale sicurezza senza alterare i record fisici di produzione.
|
||||
|
|
@ -72,30 +72,28 @@ @media (min-width: 64rem) {
|
|||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation {
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-sidebar {
|
||||
position: fixed !important;
|
||||
top: 4.5rem !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 20rem !important;
|
||||
height: calc(100vh - 4.5rem) !important;
|
||||
height: calc(100dvh - 4.5rem) !important;
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important;
|
||||
z-index: 40 !important;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.2) !important;
|
||||
overflow-y: auto !important;
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
.fi-body.fi-body-has-navigation .fi-topbar {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
left: 20rem !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
margin-left: 0 !important;
|
||||
width: calc(100% - 20rem) !important;
|
||||
z-index: 50 !important;
|
||||
}
|
||||
|
||||
|
|
@ -203,4 +201,27 @@ .fi-sidebar-nav-groups>li,
|
|||
.fi-sidebar-group,
|
||||
.fi-sidebar-item {
|
||||
margin-block: 0 !important;
|
||||
}
|
||||
|
||||
/* Compressione altezza e padding Topbar */
|
||||
.fi-topbar-header {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
flex-wrap: nowrap !important;
|
||||
width: 100% !important;
|
||||
min-height: 3rem !important;
|
||||
padding-block: 0.25rem !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.fi-topbar {
|
||||
height: 3.5rem !important;
|
||||
}
|
||||
|
||||
@media (min-width: 64rem) {
|
||||
.fi-body.fi-body-has-navigation .fi-main-ctn {
|
||||
padding-top: 3.5rem !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -270,6 +270,11 @@ class="text-green-600 hover:text-green-800 text-sm font-medium"
|
|||
onclick="scaricaDocumento({{ $documento->id }})">
|
||||
<i class="fas fa-download mr-1"></i>Scarica
|
||||
</button>
|
||||
<a href="{{ route('filament.documenti.etichetta', $documento) }}?format=11354&autoprint=1"
|
||||
target="_blank"
|
||||
class="text-amber-600 hover:text-amber-800 text-sm font-medium flex items-center">
|
||||
<i class="fas fa-print mr-1"></i>Dymo
|
||||
</a>
|
||||
<button type="button"
|
||||
class="text-purple-600 hover:text-purple-800 text-sm font-medium"
|
||||
onclick="modificaDocumento({{ $documento->id }})">
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@
|
|||
let isRestoring = false;
|
||||
|
||||
const getSidebar = () => {
|
||||
return document.querySelector('.fi-sidebar-nav') ||
|
||||
return document.querySelector('.fi-sidebar') ||
|
||||
document.querySelector('.fi-sidebar-nav') ||
|
||||
document.querySelector('aside nav') ||
|
||||
document.querySelector('.fi-sidebar') ||
|
||||
document.querySelector('.fi-sidebar-nav-groups');
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -48,13 +48,13 @@
|
|||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
@endphp
|
||||
|
||||
<div class="flex min-w-0 flex-1 items-end gap-2 ps-3">
|
||||
<div class="flex w-full items-center justify-between gap-4 ps-3">
|
||||
@if($showOperationalContext)
|
||||
<div class="flex min-w-0 flex-wrap items-end gap-2 xl:flex-nowrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<form method="POST" action="{{ route('admin-filament.stabile-attivo') }}" class="min-w-[15rem]">
|
||||
@csrf
|
||||
<label class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-gray-500">Stabile</label>
|
||||
<select name="stabile_id" onchange="this.form.submit()" class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-medium text-gray-800 shadow-sm focus:border-amber-400 focus:outline-none">
|
||||
<select name="stabile_id" onchange="this.form.submit()" class="w-full rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-800 shadow-sm focus:border-amber-400 focus:outline-none">
|
||||
@foreach($stabili as $stabile)
|
||||
<option value="{{ $stabile->id }}" @selected((int) $stabile->id === (int) ($stabileAttivo?->id ?? 0))>
|
||||
{{ $stabileLabel($stabile) }} · {{ $stabile->denominazione }}
|
||||
|
|
@ -63,44 +63,41 @@
|
|||
</select>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('admin-filament.gestione-attiva') }}" class="min-w-[11rem]">
|
||||
@csrf
|
||||
<label class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-gray-500">Gestione</label>
|
||||
<select name="gestione" onchange="this.form.submit()" class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-medium text-gray-800 shadow-sm focus:border-amber-400 focus:outline-none">
|
||||
@foreach($gestioni as $gestioneKey => $gestioneLabel)
|
||||
@continue($gestioneKey === 'riscaldamento' && ! $haRiscaldamento)
|
||||
<option value="{{ $gestioneKey }}" @selected($gestioneKey === $gestioneAttiva)>{{ $gestioneLabel }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
<span class="inline-flex items-center rounded-full border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-800">
|
||||
Risc: {{ $haRiscaldamento ? 'SI' : 'NO' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
@if($haRiscaldamento)
|
||||
<a href="/admin-filament/condomini/riscaldamento-utenze?stabile_id={{ $stabileAttivo?->id }}" class="inline-flex items-center justify-center rounded-lg bg-sky-50 dark:bg-sky-950/20 border border-sky-200 dark:border-sky-800 px-3 py-1.5 text-xs font-bold text-sky-700 dark:text-sky-300 transition-colors hover:bg-sky-100 dark:hover:bg-sky-950/40" title="Riscaldamento Utenze">
|
||||
🔥 RISCALDAMENTO
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<a href="/admin-filament/gescon/straordinarie?stabile_id={{ $stabileAttivo?->id }}" class="inline-flex items-center justify-center rounded-lg bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800 px-3 py-1.5 text-xs font-bold text-amber-700 dark:text-amber-300 transition-colors hover:bg-amber-100 dark:hover:bg-amber-950/40" title="Spese Straordinarie">
|
||||
🛠️ STRAORDINARIE
|
||||
</a>
|
||||
|
||||
<form method="POST" action="{{ route('admin-filament.anno-gestione-attivo') }}" class="min-w-[7rem]">
|
||||
@csrf
|
||||
<label class="mb-1 block text-[11px] font-semibold uppercase tracking-wide text-gray-500">Anno</label>
|
||||
<select name="anno" onchange="this.form.submit()" class="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-medium text-gray-800 shadow-sm focus:border-amber-400 focus:outline-none">
|
||||
<select name="anno" onchange="this.form.submit()" class="w-full rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-800 shadow-sm focus:border-amber-400 focus:outline-none">
|
||||
@foreach($anniGestione as $anno)
|
||||
<option value="{{ $anno }}" @selected((int) $anno === (int) $annoGestione)>{{ $anno }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<span class="inline-flex items-center rounded-full border border-gray-200 bg-white px-2.5 py-2 text-xs font-medium text-gray-800">
|
||||
Risc: {{ $haRiscaldamento ? 'SI' : 'NO' }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="fi-icon-btn fi-color-gray ml-2"
|
||||
x-data="{}"
|
||||
x-on:click="$dispatch('open-global-search-results')"
|
||||
title="Cerca"
|
||||
>
|
||||
<x-filament::icon icon="heroicon-o-magnifying-glass" class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="hidden min-w-0 flex-1 xl:flex xl:max-w-[26rem] xl:items-end">
|
||||
<livewire:filament.topbar-live-call />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="fi-icon-btn fi-color-gray"
|
||||
x-data="{}"
|
||||
x-on:click="$dispatch('open-global-search-results')"
|
||||
title="Cerca"
|
||||
>
|
||||
<x-filament::icon icon="heroicon-o-magnifying-glass" class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<div class="hidden min-w-[22rem] max-w-xl flex-1 px-3 xl:block">
|
||||
<div class="min-w-[14rem] max-w-xs flex-1 px-2">
|
||||
@livewire(\App\Livewire\Filament\TopbarLiveCall::class)
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<button onclick="document.getElementById('modal-nuova-assemblea').classList.remove('hidden')" class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-bold shadow-md transition-all active:scale-95">
|
||||
<button wire:click="creaNuovaAssemblea" class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-bold shadow-md transition-all active:scale-95">
|
||||
<i class="fas fa-plus mr-1.5"></i> Nuova Assemblea
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -42,127 +42,146 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Elenco cronologico in tabella full-width -->
|
||||
<div class="rounded-xl border bg-white shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b">
|
||||
<h3 class="text-sm font-bold text-slate-800 uppercase tracking-wider flex items-center"><i class="fas fa-history mr-2 text-blue-500"></i>Cronologia assemblee dello stabile</h3>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse text-xs">
|
||||
<thead>
|
||||
<tr class="bg-slate-100 text-slate-700 font-bold uppercase border-b text-[10px] tracking-wider">
|
||||
<th class="p-4">Assemblea / Tipo</th>
|
||||
<th class="p-4">Data 1ª Convocazione</th>
|
||||
<th class="p-4">Data 2ª Convocazione</th>
|
||||
<th class="p-4">Luogo</th>
|
||||
<th class="p-4 text-center">Stato</th>
|
||||
<th class="p-4 text-center">OdG</th>
|
||||
<th class="p-4 text-center">Convocati</th>
|
||||
<th class="p-4 text-center">Presenti</th>
|
||||
<th class="p-4 text-center">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@forelse($this->assembleeRows as $assemblea)
|
||||
<tr class="hover:bg-slate-50/50">
|
||||
<td class="p-4 font-bold text-slate-900 capitalize">
|
||||
Assemblea {{ $assemblea->tipo ?: 'Ordinaria' }}
|
||||
</td>
|
||||
<td class="p-4 text-slate-600">
|
||||
{{ $assemblea->data_prima_convocazione ? $assemblea->data_prima_convocazione->format('d/m/Y H:i') : 'n/d' }}
|
||||
</td>
|
||||
<td class="p-4 text-slate-600 font-semibold">
|
||||
{{ $assemblea->data_seconda_convocazione ? $assemblea->data_seconda_convocazione->format('d/m/Y H:i') : 'n/d' }}
|
||||
</td>
|
||||
<td class="p-4 text-slate-500">
|
||||
{{ \Illuminate\Support\Str::limit($assemblea->luogo ?: 'Presso uffici', 40) }}
|
||||
</td>
|
||||
<td class="p-4 text-center">
|
||||
<span class="text-[9px] font-extrabold uppercase px-2 py-0.5 rounded {{ $assemblea->stato === 'convocata' ? 'bg-amber-100 text-amber-700 border border-amber-200' : ($assemblea->stato === 'svolta' ? 'bg-emerald-100 text-emerald-700 border border-emerald-200' : 'bg-slate-100 text-slate-700 border border-slate-200') }}">
|
||||
{{ $assemblea->stato ?: 'Bozza' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-4 text-center font-bold text-slate-800">{{ (int)$assemblea->ordine_giorno_count }}</td>
|
||||
<td class="p-4 text-center font-bold text-slate-800">{{ (int)$assemblea->convocazioni_count }}</td>
|
||||
<td class="p-4 text-center font-bold text-slate-800">{{ (int)$assemblea->presenze_count }}</td>
|
||||
<td class="p-4 text-center flex items-center justify-center space-x-2">
|
||||
<a href="{{ \App\Filament\Pages\Condomini\GestioneAssemblea::getUrl(['record' => $assemblea->id]) }}" class="px-2.5 py-1.5 bg-blue-50 text-blue-700 border border-blue-200 hover:bg-blue-100 rounded-lg text-xxs font-bold transition flex items-center">
|
||||
<i class="fas fa-cog mr-1"></i> Gestisci
|
||||
</a>
|
||||
<button onclick="confirm('Vuoi eliminare questa assemblea?') && @this.eliminaAssemblea({{ $assemblea->id }})" class="px-2.5 py-1.5 bg-red-50 text-red-700 border border-red-200 hover:bg-red-100 rounded-lg text-xxs font-bold transition">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="p-8 text-center text-slate-400 italic">
|
||||
Nessuna assemblea registrata per questo stabile. Clicca su "Nuova Assemblea" per crearne una.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Tab buttons -->
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('tab','elenco')"
|
||||
class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibold transition-all
|
||||
{{ ($tab ?? 'elenco') === 'elenco' ? 'border-primary-300 bg-primary-50 text-primary-700 shadow-sm' : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50' }}"
|
||||
>
|
||||
<i class="fas fa-list-ul mr-2"></i> Assemblee Stabile
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('tab','sospesi')"
|
||||
class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibold transition-all
|
||||
{{ ($tab ?? 'elenco') === 'sospesi' ? 'border-primary-300 bg-primary-50 text-primary-700 shadow-sm' : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50' }}"
|
||||
>
|
||||
<i class="fas fa-pause mr-2 text-amber-500"></i> Punti OdG Sospesi
|
||||
@php
|
||||
$sospesiCount = count($this->sospesiRows);
|
||||
@endphp
|
||||
@if($sospesiCount > 0)
|
||||
<span class="ml-2 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-bold text-amber-800">{{ $sospesiCount }}</span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if(($tab ?? 'elenco') === 'elenco')
|
||||
<!-- Elenco cronologico in tabella full-width -->
|
||||
<div class="rounded-xl border bg-white shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b">
|
||||
<h3 class="text-sm font-bold text-slate-800 uppercase tracking-wider flex items-center"><i class="fas fa-history mr-2 text-blue-500"></i>Cronologia assemblee dello stabile</h3>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse text-xs">
|
||||
<thead>
|
||||
<tr class="bg-slate-100 text-slate-700 font-bold uppercase border-b text-[10px] tracking-wider">
|
||||
<th class="p-4">Assemblea / Tipo</th>
|
||||
<th class="p-4">Data 1ª Convocazione</th>
|
||||
<th class="p-4">Data 2ª Convocazione</th>
|
||||
<th class="p-4">Luogo</th>
|
||||
<th class="p-4 text-center">Stato</th>
|
||||
<th class="p-4 text-center">OdG</th>
|
||||
<th class="p-4 text-center">Convocati</th>
|
||||
<th class="p-4 text-center">Presenti</th>
|
||||
<th class="p-4 text-center">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@forelse($this->assembleeRows as $assemblea)
|
||||
<tr class="hover:bg-slate-50/50">
|
||||
<td class="p-4 font-bold text-slate-900 capitalize">
|
||||
Assemblea {{ $assemblea->tipo ?: 'Ordinaria' }}
|
||||
</td>
|
||||
<td class="p-4 text-slate-600">
|
||||
{{ $assemblea->data_prima_convocazione ? $assemblea->data_prima_convocazione->format('d/m/Y H:i') : 'n/d' }}
|
||||
</td>
|
||||
<td class="p-4 text-slate-600 font-semibold">
|
||||
{{ $assemblea->data_seconda_convocazione ? $assemblea->data_seconda_convocazione->format('d/m/Y H:i') : 'n/d' }}
|
||||
</td>
|
||||
<td class="p-4 text-slate-500">
|
||||
{{ \Illuminate\Support\Str::limit($assemblea->luogo ?: 'Presso uffici', 40) }}
|
||||
</td>
|
||||
<td class="p-4 text-center">
|
||||
<span class="text-[9px] font-extrabold uppercase px-2 py-0.5 rounded {{ $assemblea->stato === 'convocata' ? 'bg-amber-100 text-amber-700 border border-amber-200' : ($assemblea->stato === 'svolta' ? 'bg-emerald-100 text-emerald-700 border border-emerald-200' : 'bg-slate-100 text-slate-700 border border-slate-200') }}">
|
||||
{{ $assemblea->stato ?: 'Bozza' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-4 text-center font-bold text-slate-800">{{ (int)$assemblea->ordine_giorno_count }}</td>
|
||||
<td class="p-4 text-center font-bold text-slate-800">{{ (int)$assemblea->convocazioni_count }}</td>
|
||||
<td class="p-4 text-center font-bold text-slate-800">{{ (int)$assemblea->presenze_count }}</td>
|
||||
<td class="p-4 text-center flex items-center justify-center space-x-2">
|
||||
<a href="{{ \App\Filament\Pages\Condomini\GestioneAssemblea::getUrl(['record' => $assemblea->id]) }}" class="px-2.5 py-1.5 bg-blue-50 text-blue-700 border border-blue-200 hover:bg-blue-100 rounded-lg text-xxs font-bold transition flex items-center">
|
||||
<i class="fas fa-cog mr-1"></i> Gestisci
|
||||
</a>
|
||||
<button onclick="confirm('Vuoi eliminare questa assemblea?') && @this.eliminaAssemblea({{ $assemblea->id }})" class="px-2.5 py-1.5 bg-red-50 text-red-700 border border-red-200 hover:bg-red-100 rounded-lg text-xxs font-bold transition">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="p-8 text-center text-slate-400 italic">
|
||||
Nessuna assemblea registrata per questo stabile. Clicca su "Nuova Assemblea" per crearne una.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@elseif(($tab ?? 'elenco') === 'sospesi')
|
||||
<!-- Punti Sospesi List -->
|
||||
<div class="rounded-xl border bg-white shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b bg-amber-50/50">
|
||||
<h3 class="text-sm font-bold text-slate-800 uppercase tracking-wider flex items-center">
|
||||
<i class="fas fa-pause mr-2 text-amber-500"></i>
|
||||
Punti Ordine del Giorno Sospesi o non deliberati
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-4">
|
||||
@if(empty($this->sospesiRows) || count($this->sospesiRows) === 0)
|
||||
<div class="rounded-xl border border-dashed p-10 text-center text-slate-500 text-xs">
|
||||
Nessun punto all'Ordine del Giorno sospeso o rimandato rilevato per lo stabile attivo.
|
||||
</div>
|
||||
@else
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
@foreach($this->sospesiRows as $sPunto)
|
||||
<div class="border rounded-xl p-4 bg-white hover:bg-slate-50/50 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-amber-100 text-amber-800 border border-amber-200">
|
||||
{{ str_replace('_', ' ', $sPunto->esito_votazione ?: 'sospeso') }}
|
||||
</span>
|
||||
<span class="text-slate-400 text-xxs font-mono">ID: #{{ $sPunto->id }}</span>
|
||||
</div>
|
||||
<h4 class="text-sm font-bold text-slate-900 mt-2">{{ $sPunto->titolo }}</h4>
|
||||
<p class="text-xs text-slate-500 mt-1">{{ $sPunto->descrizione }}</p>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2 text-[10px] text-slate-500">
|
||||
@if($sPunto->articolo_legge)
|
||||
<span><i class="fas fa-gavel mr-1"></i>{{ $sPunto->articolo_legge }}</span>
|
||||
@endif
|
||||
@if($sPunto->maggioranza_richiesta)
|
||||
<span><i class="fas fa-balance-scale mr-1"></i>{{ $sPunto->maggioranza_richiesta }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Modal Nuova Assemblea -->
|
||||
<div id="modal-nuova-assemblea" class="fixed inset-0 z-50 overflow-y-auto hidden bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-xl max-w-lg w-full border shadow-2xl overflow-hidden">
|
||||
<div class="bg-slate-900 text-white px-5 py-4 flex items-center justify-between">
|
||||
<h3 class="text-sm font-bold uppercase tracking-wider"><i class="fas fa-plus mr-1.5 text-blue-400"></i>Nuova Assemblea</h3>
|
||||
<button onclick="document.getElementById('modal-nuova-assemblea').classList.add('hidden')" class="text-slate-400 hover:text-white"><i class="fas fa-times text-md"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Tipo Assemblea</label>
|
||||
<select wire:model="nuovaTipo" class="w-full text-xs rounded-lg border-slate-300">
|
||||
<option value="ordinaria">Ordinaria</option>
|
||||
<option value="straordinaria">Straordinaria</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Luogo</label>
|
||||
<input type="text" wire:model="nuovaLuogo" placeholder="Es. Ufficio o Skype" class="w-full text-xs rounded-lg border-slate-300">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Data 1ª Convocazione</label>
|
||||
<input type="datetime-local" wire:model="nuovaData1" class="w-full text-xs rounded-lg border-slate-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Data 2ª Convocazione</label>
|
||||
<input type="datetime-local" wire:model="nuovaData2" class="w-full text-xs rounded-lg border-slate-300">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Note / OdG Iniziale</label>
|
||||
<textarea wire:model="nuovaNote" rows="3" placeholder="Note per convocazione..." class="w-full text-xs rounded-lg border-slate-300"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 px-5 py-3 flex justify-end space-x-2 border-t">
|
||||
<button onclick="document.getElementById('modal-nuova-assemblea').classList.add('hidden')" class="px-4 py-2 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded-lg text-xs font-bold transition">Annulla</button>
|
||||
<button wire:click="creaNuovaAssemblea" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-bold transition shadow-md shadow-blue-500/10">Crea Assemblea</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.getElementById('modal-nuova-assemblea').classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('notify', (e) => {
|
||||
if (typeof Filament !== 'undefined' && Filament.notify) {
|
||||
Filament.notify(e.detail.status, e.detail.message);
|
||||
|
|
|
|||
|
|
@ -42,72 +42,99 @@
|
|||
<div class="grid gap-6 xl:grid-cols-[minmax(0,1.4fr)_minmax(320px,0.6fr)]">
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Registro contratti</x-slot>
|
||||
<x-slot name="description">MVP: contratto, riferimenti essenziali, documento principale e generazione scadenze.</x-slot>
|
||||
<x-slot name="description">CRUD In-line: modifica direttamente i campi della riga per aggiornare scadenze, importi e caricare i PDF allegati.</x-slot>
|
||||
|
||||
<div class="space-y-4">
|
||||
@forelse($contratti as $contratto)
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="text-lg font-semibold text-slate-900">{{ $contratto->titolo }}</div>
|
||||
<span class="inline-flex items-center rounded-full bg-{{ $this->contractBadgeColor($contratto->stato) }}-50 px-2.5 py-1 text-xs font-semibold text-{{ $this->contractBadgeColor($contratto->stato) }}-700">
|
||||
{{ strtoupper($contratto->stato) }}
|
||||
</span>
|
||||
<span class="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-700">
|
||||
{{ $this->contractTypeLabel($contratto->tipo_contratto) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-slate-600">
|
||||
{{ $contratto->servizio_label ?: 'Servizio non specificato' }}
|
||||
@if($contratto->fornitore)
|
||||
· {{ $contratto->fornitore->ragione_sociale ?: trim(($contratto->fornitore->nome ?? '') . ' ' . ($contratto->fornitore->cognome ?? '')) }}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-xs border-collapse border border-slate-200 rounded-xl overflow-hidden shadow-xs">
|
||||
<thead>
|
||||
<tr class="bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-bold uppercase border-b border-slate-200">
|
||||
<th class="p-3">Titolo / Fornitore</th>
|
||||
<th class="p-3">Tipo / Gestione</th>
|
||||
<th class="p-3">Data Scadenza</th>
|
||||
<th class="p-3">Importo Periodico</th>
|
||||
<th class="p-3 text-center">Documento PDF</th>
|
||||
<th class="p-3">Note / Info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 bg-white dark:bg-slate-900">
|
||||
@forelse($contratti as $contratto)
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
|
||||
<!-- Titolo e Fornitore -->
|
||||
<td class="p-3 font-medium text-slate-900 dark:text-slate-100">
|
||||
<div class="font-bold text-sm">{{ $contratto->titolo }}</div>
|
||||
<div class="text-xs text-slate-500 mt-0.5">
|
||||
Fornitore: {{ $contratto->fornitore?->ragione_sociale ?: trim(($contratto->fornitore?->nome ?? '') . ' ' . ($contratto->fornitore?->cognome ?? '')) ?: 'Non specificato' }}
|
||||
</div>
|
||||
</td>
|
||||
<!-- Tipo Contratto / Gestione -->
|
||||
<td class="p-3">
|
||||
<select
|
||||
wire:change="aggiornaCampoInline({{ $contratto->id }}, 'tipo_contratto', $event.target.value)"
|
||||
class="rounded-lg border-slate-200 dark:border-slate-700 py-1 text-xs bg-slate-50 dark:bg-slate-800 focus:bg-white w-full font-medium"
|
||||
>
|
||||
@foreach($this->getTipoContrattoOptions() as $key => $lbl)
|
||||
<option value="{{ $key }}" {{ $contratto->tipo_contratto === $key ? 'selected' : '' }}>{{ $lbl }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<!-- Data Scadenza -->
|
||||
<td class="p-3">
|
||||
<input
|
||||
type="date"
|
||||
value="{{ $contratto->decorrenza_al?->format('Y-m-d') }}"
|
||||
wire:change="aggiornaCampoInline({{ $contratto->id }}, 'decorrenza_al', $event.target.value)"
|
||||
class="rounded-lg border-slate-200 dark:border-slate-700 py-1 text-xs bg-slate-50 dark:bg-slate-800 focus:bg-white w-full"
|
||||
/>
|
||||
</td>
|
||||
<!-- Importo Periodico -->
|
||||
<td class="p-3">
|
||||
<div class="relative rounded-lg shadow-xs">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2">
|
||||
<span class="text-slate-500 text-xs">€</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value="{{ $contratto->importo_periodico }}"
|
||||
wire:change="aggiornaCampoInline({{ $contratto->id }}, 'importo_periodico', $event.target.value)"
|
||||
class="rounded-lg border-slate-200 dark:border-slate-700 py-1 pl-6 text-xs text-right bg-slate-50 dark:bg-slate-800 focus:bg-white w-28 font-semibold"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Documento PDF -->
|
||||
<td class="p-3 text-center">
|
||||
@if($contratto->documentoPrincipale)
|
||||
<a
|
||||
href="{{ storage_url($contratto->documentoPrincipale->path_file) }}"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-1 rounded-md bg-emerald-50 dark:bg-emerald-950/40 px-2 py-1 text-xxs font-bold text-emerald-700 dark:text-emerald-300 hover:underline"
|
||||
>
|
||||
<x-filament::icon icon="heroicon-o-document-text" class="h-4.5 w-4.5 text-emerald-600" />
|
||||
{{ Str::limit($contratto->documentoPrincipale->nome_file ?: 'Visualizza PDF', 15) }}
|
||||
</a>
|
||||
@else
|
||||
{{ ($this->caricaPdfContrattoAction)(['contract_id' => $contratto->id]) }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-slate-500 lg:text-right">
|
||||
<div>Decorrenza: {{ $contratto->decorrenza_dal?->format('d/m/Y') ?: '—' }}</div>
|
||||
<div>Fine: {{ $contratto->decorrenza_al?->format('d/m/Y') ?: ($contratto->rinnovo_automatico ? 'rinnovo automatico' : '—') }}</div>
|
||||
<div>Frequenza: {{ $contratto->frequenza_scadenze ? str_replace('_', ' ', $contratto->frequenza_scadenze) : '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-4 text-sm">
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Codice</div>
|
||||
<div class="mt-1 font-medium text-slate-900">{{ $contratto->codice_contratto ?: '—' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Riferimento</div>
|
||||
<div class="mt-1 font-medium text-slate-900">{{ $contratto->riferimento_esterno ?: '—' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Importo</div>
|
||||
<div class="mt-1 font-medium text-slate-900">{{ $contratto->importo_periodico !== null ? '€ ' . number_format((float) $contratto->importo_periodico, 2, ',', '.') : '—' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-slate-500">Documento</div>
|
||||
<div class="mt-1 font-medium text-slate-900">{{ $contratto->documentoPrincipale?->numero_protocollo ?: ($contratto->documentoPrincipale?->nome ?: '—') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($contratto->codici_collegati))
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
@foreach($contratto->codici_collegati as $code)
|
||||
<span class="inline-flex items-center rounded-full bg-cyan-50 px-3 py-1 text-xs font-medium text-cyan-800">
|
||||
{{ $code['chiave'] ?? 'Dato' }}: {{ $code['valore'] ?? '—' }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($contratto->note)
|
||||
<div class="mt-4 rounded-lg bg-slate-50 px-3 py-2 text-sm text-slate-600">{{ $contratto->note }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="rounded-xl border border-dashed bg-white px-4 py-10 text-center text-sm text-slate-500">Nessun contratto registrato per lo stabile attivo.</div>
|
||||
@endforelse
|
||||
</td>
|
||||
<!-- Note / Info -->
|
||||
<td class="p-3 text-slate-600 dark:text-slate-400">
|
||||
<div class="max-w-[150px] overflow-hidden text-ellipsis whitespace-nowrap text-xxs" title="{{ $contratto->note }}">
|
||||
{{ $contratto->note ?: 'Nessuna nota' }}
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-400 mt-0.5">
|
||||
Codice: {{ $contratto->codice_contratto ?: 'n/d' }} · Rif: {{ $contratto->riferimento_esterno ?: 'n/d' }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="p-8 text-center text-slate-500 border border-dashed rounded-xl bg-slate-50">
|
||||
Nessun contratto registrato per lo stabile attivo.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
|
|
@ -196,4 +223,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-filament-actions::modals />
|
||||
</x-filament-panels::page>
|
||||
|
|
|
|||
|
|
@ -18,15 +18,25 @@
|
|||
</span>
|
||||
</div>
|
||||
<div class="text-lg font-bold text-slate-900 mt-1">
|
||||
{{ $this->stabileAttivo->denominazione }} — 2ª Convocazione il {{ $this->assemblea->data_seconda_convocazione ? $this->assemblea->data_seconda_convocazione->format('d/m/Y \alle H:i') : 'n/d' }}
|
||||
{{ $this->stabileAttivo->denominazione }} —
|
||||
1ª Convocazione il {{ $this->assemblea->data_prima_convocazione ? $this->assemblea->data_prima_convocazione->format('d/m/Y') . ' alle ' . $this->assemblea->data_prima_convocazione->format('H:i') : 'n/d' }} |
|
||||
2ª Convocazione il {{ $this->assemblea->data_seconda_convocazione ? $this->assemblea->data_seconda_convocazione->format('d/m/Y') . ' alle ' . $this->assemblea->data_seconda_convocazione->format('H:i') : 'n/d' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xxs text-slate-400 font-semibold bg-slate-50 border rounded-lg p-2 max-w-xs text-right">
|
||||
<p><i class="fas fa-map-marker-alt mr-1"></i> Luogo: {{ $this->assemblea->luogo ?: 'Presso uffici' }}</p>
|
||||
@if($this->assemblea->note)
|
||||
<p class="mt-0.5 truncate" title="{{ $this->assemblea->note }}"><i class="fas fa-info-circle mr-1"></i> Note: {{ $this->assemblea->note }}</p>
|
||||
@endif
|
||||
<div class="flex items-center space-x-2">
|
||||
<button wire:click="scaricaIcal" class="px-2.5 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 border border-slate-200 rounded-lg text-xxs font-bold transition flex items-center">
|
||||
<i class="fas fa-calendar-plus mr-1 text-slate-500"></i> Calendario iCal
|
||||
</button>
|
||||
<button wire:click="sincronizzaDrive" class="px-2.5 py-1.5 bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 rounded-lg text-xxs font-bold transition flex items-center">
|
||||
<i class="fab fa-google-drive mr-1 text-emerald-600"></i> Drive Backup
|
||||
</button>
|
||||
<div class="text-xxs text-slate-400 font-semibold bg-slate-50 border rounded-lg p-2 max-w-xs text-right">
|
||||
<p><i class="fas fa-map-marker-alt mr-1"></i> Luogo: {{ $this->assemblea->luogo ?: 'Presso uffici' }}</p>
|
||||
@if($this->assemblea->note)
|
||||
<p class="mt-0.5 truncate" title="{{ $this->assemblea->note }}"><i class="fas fa-info-circle mr-1"></i> Note: {{ $this->assemblea->note }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -36,6 +46,9 @@
|
|||
<!-- Tabs Menu -->
|
||||
<div class="px-4 border-b border-slate-200 bg-slate-50/50">
|
||||
<div class="flex space-x-4">
|
||||
<button wire:click="changeTab('dati-generali')" class="py-3 text-xs font-bold border-b-2 transition-all {{ $this->activeSubTab === 'dati-generali' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800' }}">
|
||||
<i class="fas fa-edit mr-1"></i> Stesura / Dati Generali
|
||||
</button>
|
||||
<button wire:click="changeTab('odg')" class="py-3 text-xs font-bold border-b-2 transition-all {{ $this->activeSubTab === 'odg' ? 'border-blue-600 text-blue-600' : 'border-transparent text-slate-500 hover:text-slate-800' }}">
|
||||
<i class="fas fa-list-ol mr-1"></i> Ordine del Giorno
|
||||
</button>
|
||||
|
|
@ -53,10 +66,103 @@
|
|||
|
||||
<!-- Tab Contents -->
|
||||
<div class="p-5">
|
||||
|
||||
<!-- Tab 0: Dati Generali / Stesura -->
|
||||
@if($this->activeSubTab === 'dati-generali')
|
||||
<div class="space-y-6">
|
||||
<form wire:submit.prevent="salvaDatiAssemblea" class="bg-slate-50 border rounded-xl p-5 space-y-4 shadow-xs">
|
||||
<h4 class="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center"><i class="fas fa-edit mr-1.5 text-blue-500 text-sm"></i> Stesura e Dati Generali Assemblea</h4>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Tipo Assemblea</label>
|
||||
<select wire:model="editTipo" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="ordinaria">Ordinaria</option>
|
||||
<option value="straordinaria">Straordinaria</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Data 1ª Convocazione</label>
|
||||
<input type="datetime-local" wire:model="editData1" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Data 2ª Convocazione</label>
|
||||
<input type="datetime-local" wire:model="editData2" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Luogo / Sede</label>
|
||||
<input type="text" wire:model="editLuogo" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500" placeholder="Es. Uffici di Studio o sala condominiale">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Scegli da luoghi già utilizzati</label>
|
||||
<select onchange="@this.set('editLuogo', this.value)" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Seleziona luogo storico...</option>
|
||||
@foreach($this->luoghiStorici as $luogoItem)
|
||||
<option value="{{ $luogoItem['luogo'] }}">{{ $luogoItem['luogo'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Stato dell'Assemblea</label>
|
||||
<select wire:model="assemblea.stato" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="bozza">Bozza (In stesura)</option>
|
||||
<option value="convocata">Convocata (Attiva)</option>
|
||||
<option value="svolta">Svolta (Archiviata / Sola lettura)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Note operative o note di prenotazione sede</label>
|
||||
<textarea wire:model="editNote" rows="3" placeholder="Aggiungi dettagli organizzativi o note..." class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-bold transition shadow-md shadow-blue-500/10">
|
||||
Salva modifiche assemblea
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Tab 1: Ordine del Giorno -->
|
||||
@if($this->activeSubTab === 'odg')
|
||||
<div class="space-y-6">
|
||||
@if(count($this->disponibiliSospesi) > 0)
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<x-filament::icon icon="heroicon-o-arrow-down-on-square-stack" class="h-5 w-5 text-amber-600 mt-0.5" />
|
||||
<div class="space-y-2 w-full">
|
||||
<h4 class="text-xs font-bold text-amber-800 uppercase tracking-wider">
|
||||
Punti Sospesi da Assemblee Precedenti Rilevati
|
||||
</h4>
|
||||
<p class="text-xs text-amber-700">Importa e inserisci all'Ordine del Giorno ereditando tutti i dettagli:</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 pt-1">
|
||||
@foreach($this->disponibiliSospesi as $sPunto)
|
||||
<div class="flex items-center justify-between p-2.5 rounded-lg border border-amber-200 bg-white shadow-xxs">
|
||||
<div class="truncate mr-3">
|
||||
<div class="text-xs font-bold text-slate-800 truncate">{{ $sPunto->titolo }}</div>
|
||||
<div class="text-xxs text-slate-500 mt-0.5">Esito: <span class="capitalize font-semibold">{{ str_replace('_', ' ', $sPunto->esito_votazione) }}</span></div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="importaSospeso({{ $sPunto->id }})"
|
||||
class="px-2.5 py-1 bg-amber-600 hover:bg-amber-500 text-white rounded text-xxs font-bold transition flex items-center shrink-0"
|
||||
>
|
||||
Importa
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Form Aggiunta -->
|
||||
<form wire:submit.prevent="aggiungiPuntoOdG" class="bg-slate-50 border rounded-xl p-4 space-y-4">
|
||||
<h4 class="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center"><i class="fas fa-plus-circle mr-1.5 text-blue-500 text-sm"></i> Aggiungi Punto all'Ordine del Giorno</h4>
|
||||
|
|
@ -76,50 +182,187 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Tabella Millesimale di Riferimento</label>
|
||||
<select wire:model="odgTabellaMillesimaleId" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Seleziona tabella...</option>
|
||||
@foreach($this->tabelleMillesimali as $tab)
|
||||
<option value="{{ $tab->id }}">{{ $tab->denominazione }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Descrizione Dettagliata</label>
|
||||
<textarea wire:model="odgDescrizione" rows="2" placeholder="Descrizione del punto da discutere..." class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Tabella Millesimale di Riferimento</label>
|
||||
<select wire:model="odgTabellaMillesimaleId" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Seleziona tabella...</option>
|
||||
@foreach($this->tabelleMillesimali as $tab)
|
||||
<option value="{{ $tab->id }}">{{ $tab->denominazione }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Maggioranza Richiesta</label>
|
||||
<select wire:model="odgMaggioranza" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="semplice">Semplice (50%+1 teste/millesimi presenti)</option>
|
||||
<option value="qualificata">Qualificata (50%+1 millesimi e teste totali)</option>
|
||||
<option value="unanimita">Unanimità (1000/1000)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Link/Riferimento Normattiva</label>
|
||||
<input type="text" wire:model="odgRiferimentoLegge" placeholder="Es. L. 220/2012 o ID Normattiva" class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-bold transition-all active:scale-95">
|
||||
Inserisci Punto
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-12">
|
||||
<div class="md:col-span-8">
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Descrizione Dettagliata</label>
|
||||
<textarea wire:model="odgDescrizione" rows="2" placeholder="Descrizione del punto da discutere..." class="w-full text-xs rounded-lg border-slate-300 shadow-xs focus:ring-blue-500 focus:border-blue-500"></textarea>
|
||||
</div>
|
||||
<div class="md:col-span-4">
|
||||
<label class="block text-xxs font-bold text-slate-500 uppercase mb-1">Allegati PDF (Multipli)</label>
|
||||
<input type="file" wire:model="odgAllegati" multiple class="w-full text-xs file:mr-2 file:py-1 file:px-2 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||
<div wire:loading wire:target="odgAllegati" class="text-xxs text-blue-600 font-bold mt-1">Caricamento file...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-bold transition-all active:scale-95">
|
||||
Inserisci Punto
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Elenco Punti -->
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-xs font-bold uppercase tracking-wider text-slate-700">Punti discussione impostati</h4>
|
||||
@forelse($this->ordineGiornoList as $punto)
|
||||
<div class="flex items-start justify-between border rounded-xl p-4 bg-white hover:bg-slate-50/50 gap-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<span class="w-6 h-6 bg-slate-800 text-white rounded-full flex items-center justify-center text-xs font-bold mt-0.5">{{ $punto->numero_punto }}</span>
|
||||
<div>
|
||||
<div class="text-sm font-bold text-slate-900">{{ $punto->titolo }}</div>
|
||||
<div class="text-xs text-slate-500 mt-1">{{ $punto->descrizione }}</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-[10px]">
|
||||
@if($punto->articolo_legge)
|
||||
<span class="px-2 py-0.5 rounded bg-blue-50 text-blue-700 border border-blue-100 font-semibold"><i class="fas fa-gavel mr-1"></i>{{ $punto->articolo_legge }}</span>
|
||||
@endif
|
||||
@if($punto->tabellaMillesimale)
|
||||
<span class="px-2 py-0.5 rounded bg-slate-100 text-slate-700 border border-slate-200"><i class="fas fa-calculator mr-1"></i>Tabella: {{ $punto->tabellaMillesimale->denominazione }}</span>
|
||||
@endif
|
||||
<div class="border rounded-xl p-4 bg-white hover:bg-slate-50/50 space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<span class="w-6 h-6 bg-slate-800 text-white rounded-full flex items-center justify-center text-xs font-bold mt-0.5">{{ $punto->numero_punto }}</span>
|
||||
<div>
|
||||
<div class="text-sm font-bold text-slate-900">{{ $punto->titolo }}</div>
|
||||
<div class="text-xs text-slate-500 mt-1">{{ $punto->descrizione }}</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-[10px]">
|
||||
@if($punto->articolo_legge)
|
||||
<span class="px-2 py-0.5 rounded bg-blue-50 text-blue-700 border border-blue-100 font-semibold"><i class="fas fa-gavel mr-1"></i>{{ $punto->articolo_legge }}</span>
|
||||
@endif
|
||||
@if($punto->maggioranza_richiesta)
|
||||
<span class="px-2 py-0.5 rounded bg-amber-50 text-amber-700 border border-amber-100 font-semibold"><i class="fas fa-balance-scale mr-1"></i>Maggioranza: {{ $punto->maggioranza_richiesta }}</span>
|
||||
@endif
|
||||
@if($punto->riferimento_legge)
|
||||
<a href="https://dati.normattiva.it/atto/ricerca?codiceRedazionale={{ urlencode($punto->riferimento_legge) }}" target="_blank" class="px-2 py-0.5 rounded bg-emerald-50 text-emerald-700 border border-emerald-100 font-semibold hover:bg-emerald-100"><i class="fas fa-external-link-alt mr-1"></i>Leggi: {{ $punto->riferimento_legge }}</a>
|
||||
@endif
|
||||
@if($punto->tabellaMillesimale)
|
||||
<span class="px-2 py-0.5 rounded bg-slate-100 text-slate-700 border border-slate-200"><i class="fas fa-calculator mr-1"></i>Tabella: {{ $punto->tabellaMillesimale->denominazione }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Esito e Note Delibera Editor -->
|
||||
<div class="mt-4 p-3 bg-slate-50 rounded-lg border border-slate-200 grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold uppercase text-slate-500 mb-1">Esito Votazione</label>
|
||||
<select
|
||||
onchange="@this.call('aggiornaEsitoPunto', {{ $punto->id }}, this.value, document.getElementById('note_delibera_{{ $punto->id }}').value)"
|
||||
class="text-xs rounded border-slate-300 py-1 w-full focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">Nessuno (In discussione)</option>
|
||||
<option value="approvato" {{ $punto->esito_votazione === 'approvato' ? 'selected' : '' }}>Approvato</option>
|
||||
<option value="respinto" {{ $punto->esito_votazione === 'respinto' ? 'selected' : '' }}>Respinto</option>
|
||||
<option value="non_deliberato" {{ $punto->esito_votazione === 'non_deliberato' ? 'selected' : '' }}>Non deliberato</option>
|
||||
<option value="rimandato" {{ $punto->esito_votazione === 'rimandato' ? 'selected' : '' }}>Rimandato</option>
|
||||
<option value="sospeso" {{ $punto->esito_votazione === 'sospeso' ? 'selected' : '' }}>Sospeso</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-[10px] font-bold uppercase text-slate-500 mb-1">Note Delibera</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="note_delibera_{{ $punto->id }}"
|
||||
value="{{ $punto->note_delibera }}"
|
||||
placeholder="Note o dettagli della decisione..."
|
||||
class="text-xs rounded border-slate-300 py-1 flex-1 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick="@this.call('aggiornaEsitoPunto', {{ $punto->id }}, this.previousElementSibling.previousElementSibling.value || '', document.getElementById('note_delibera_{{ $punto->id }}').value)"
|
||||
class="px-3 py-1 bg-slate-800 text-white rounded text-xs hover:bg-slate-700 transition"
|
||||
>
|
||||
Salva
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controlli di Ordinamento ed Eliminazione -->
|
||||
<div class="flex items-center space-x-1">
|
||||
<button wire:click="spostaPuntoOdG({{ $punto->id }}, 'top')" class="p-1 bg-slate-100 hover:bg-slate-200 rounded text-slate-600 transition" title="Porta in cima">
|
||||
<i class="fas fa-angle-double-up text-xs"></i>
|
||||
</button>
|
||||
<button wire:click="spostaPuntoOdG({{ $punto->id }}, 'up')" class="p-1 bg-slate-100 hover:bg-slate-200 rounded text-slate-600 transition" title="Sposta Su">
|
||||
<i class="fas fa-chevron-up text-xs"></i>
|
||||
</button>
|
||||
<button wire:click="spostaPuntoOdG({{ $punto->id }}, 'down')" class="p-1 bg-slate-100 hover:bg-slate-200 rounded text-slate-600 transition" title="Sposta Giù">
|
||||
<i class="fas fa-chevron-down text-xs"></i>
|
||||
</button>
|
||||
<button wire:click="eliminaPuntoOdG({{ $punto->id }})" class="text-red-500 hover:text-red-700 p-1.5 transition" title="Elimina punto">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Allegati del Punto -->
|
||||
@if($punto->allegati && count($punto->allegati) > 0)
|
||||
<div class="pl-9 space-y-1.5">
|
||||
<div class="text-xxs font-bold text-slate-400 uppercase tracking-wider">Documenti Allegati:</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($punto->allegati as $file)
|
||||
<a href="{{ $file['url'] }}" target="_blank" class="inline-flex items-center px-2 py-1 bg-slate-50 hover:bg-slate-100 border rounded-lg text-xxs font-semibold text-slate-700 transition">
|
||||
<i class="fas fa-file-pdf text-red-500 mr-1.5"></i> {{ $file['nome'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Trascrizione Audio Log e Sentenze Cassazione (AI Assistente) -->
|
||||
<div class="pl-9 grid gap-4 md:grid-cols-2 pt-2 border-t border-slate-100/50">
|
||||
<!-- Sentenze consigliate dall'AI (Gemini) -->
|
||||
<div class="bg-indigo-50/50 border border-indigo-100 rounded-lg p-2.5 space-y-1">
|
||||
<div class="text-[10px] font-bold text-indigo-700 uppercase tracking-wider flex items-center">
|
||||
<i class="fas fa-robot mr-1"></i> Sentenze e Note AI (Cassazione)
|
||||
</div>
|
||||
@if(Str::contains(strtolower($punto->titolo), 'varie'))
|
||||
<p class="text-xxs text-indigo-900 italic">
|
||||
"Per discussioni riguardanti 'Varie ed eventuali', la giurisprudenza di Cassazione (sent. n. 12120/2018) ribadisce che sono ammesse solo discussioni informative; è esclusa qualsiasi delibera vincolante sui costi."
|
||||
</p>
|
||||
@else
|
||||
<p class="text-xxs text-indigo-900 italic">
|
||||
"Suggerimento AI: Per '{{ $punto->titolo }}', accertarsi che i preventivi/documenti siano stati allegati alla convocazione per consentire una delibera informata (Cass. n. 21015/2023)."
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Registrazione / Trascrizione Audio del Punto -->
|
||||
<div class="bg-slate-50 border rounded-lg p-2.5 space-y-2">
|
||||
<div class="text-[10px] font-bold text-slate-600 uppercase tracking-wider flex items-center justify-between">
|
||||
<span><i class="fas fa-microphone-alt mr-1"></i> Discussione / Audio Log</span>
|
||||
@if($punto->audio_log_path)
|
||||
<span class="text-emerald-600 font-bold"><i class="fas fa-check-circle mr-0.5"></i> Trascritto</span>
|
||||
@endif
|
||||
</div>
|
||||
@if($punto->audio_log_path)
|
||||
<audio controls class="w-full h-8 mt-1">
|
||||
<source src="{{ asset('storage/' . $punto->audio_log_path) }}" type="audio/mpeg">
|
||||
Il tuo browser non supporta la riproduzione audio.
|
||||
</audio>
|
||||
<p class="text-xxs text-slate-500 mt-1 bg-white p-1.5 border rounded leading-relaxed max-h-[60px] overflow-y-auto">
|
||||
{{ $punto->audio_log_trascrizione }}
|
||||
</p>
|
||||
@else
|
||||
<form wire:submit.prevent="caricaAudioLog({{ $punto->id }})" class="flex items-center space-x-2">
|
||||
<input type="file" wire:model="audioUpload" class="text-xxs text-slate-500">
|
||||
<button type="submit" class="px-2 py-1 bg-slate-800 text-white rounded text-xxs font-bold hover:bg-slate-700">Carica Audio</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<button wire:click="eliminaPuntoOdG({{ $punto->id }})" class="text-red-500 hover:text-red-700 p-1.5 transition"><i class="fas fa-trash-alt"></i></button>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-xs text-slate-400 italic text-center py-10 border border-dashed rounded-xl">
|
||||
|
|
@ -135,11 +378,14 @@
|
|||
<div class="space-y-5">
|
||||
<div class="flex justify-between items-center bg-slate-50 p-4 border rounded-xl">
|
||||
<div class="text-xs text-slate-600 max-w-lg">
|
||||
Genera i token digitali per tutti i condomini e gli inquilini del condominio per permettere l'invio delle convocazioni e il voto su cellulare.
|
||||
Sincronizza e allinea i convocati dell'assemblea con le unità immobiliari e i soggetti attuali dello stabile, archiviando storicamente le convocazioni obsolete.
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button wire:click="generaConvocazioniMassive" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-bold transition-all active:scale-95 shadow-md shadow-blue-500/10">
|
||||
Genera Tutti i Token
|
||||
Sincronizza / Allinea Convocazioni
|
||||
</button>
|
||||
<button wire:click="downloadConvocazioniPdf" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-bold transition-all active:scale-95 shadow-md shadow-indigo-500/10 flex items-center">
|
||||
<i class="fas fa-file-pdf mr-1.5"></i> Stampa Convocazioni (PDF)
|
||||
</button>
|
||||
<button onclick="confirm('Sei sicuro di voler svuotare le convocazioni?') && @this.cancellaTutteConvocazioni()" class="px-3 py-2 bg-red-50 text-red-700 border border-red-200 hover:bg-red-100 rounded-lg text-xs font-bold transition-all">
|
||||
Svuota
|
||||
|
|
@ -202,19 +448,66 @@
|
|||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="p-8 text-center text-slate-400 italic">
|
||||
Nessuna convocazione registrata per questa assemblea. Clicca su "Genera Tutti i Token" per iniziare.
|
||||
Nessuna convocazione attiva registrata per questa assemblea. Clicca su "Sincronizza / Allinea Convocazioni" per iniziare.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if(count($this->archivedConvocazioniList) > 0)
|
||||
<div class="mt-8 space-y-3">
|
||||
<h4 class="text-xs font-bold uppercase tracking-wider text-amber-700 flex items-center">
|
||||
<i class="fas fa-archive mr-1.5 text-amber-500"></i> Convocazioni Archiviate / Obsolete (Storico)
|
||||
</h4>
|
||||
<div class="overflow-x-auto border border-amber-200 bg-amber-50/10 rounded-xl">
|
||||
<table class="w-full text-left border-collapse text-xs">
|
||||
<thead>
|
||||
<tr class="bg-amber-100/50 text-amber-800 font-bold uppercase border-b text-[10px] tracking-wider">
|
||||
<th class="p-3">Soggetto</th>
|
||||
<th class="p-3">Interno</th>
|
||||
<th class="p-3">Ruolo</th>
|
||||
<th class="p-3">Stato</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-amber-100">
|
||||
@foreach($this->archivedConvocazioniList as $conv)
|
||||
<tr class="hover:bg-amber-50/30">
|
||||
<td class="p-3 font-semibold text-slate-700">{{ $conv->soggetto->nome_completo }}</td>
|
||||
<td class="p-3">{{ $conv->unitaImmobiliare->interno ?: '—' }}</td>
|
||||
<td class="p-3">
|
||||
<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-amber-100/50 text-amber-800">
|
||||
{{ $conv->ruolo === 'I' ? 'Inquilino' : 'Proprietario' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-3 text-amber-600 font-semibold italic text-xxs">Archiviato (non più attivo nello stabile)</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Tab 3: Presenze Check-in -->
|
||||
@if($this->activeSubTab === 'presenze')
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center bg-slate-50 p-4 border rounded-xl">
|
||||
<div class="text-xs text-slate-600">
|
||||
Gestisci le presenze dell'assemblea in corso. Puoi scaricare il foglio firme cartaceo con QR code o stampare le etichette per i partecipanti.
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button wire:click="downloadFoglioFirme" class="px-3 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-bold transition flex items-center">
|
||||
<i class="fas fa-file-pdf mr-1.5"></i> Scarica Foglio Firme
|
||||
</button>
|
||||
<button onclick="printDymoLabels()" class="px-3 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded-lg text-xs font-bold transition flex items-center">
|
||||
<i class="fas fa-print mr-1.5"></i> Etichette Dymo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Check-in -->
|
||||
<form wire:submit.prevent="registraPresenzaCheckin" class="bg-slate-50 border rounded-xl p-4 space-y-4">
|
||||
<h4 class="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center"><i class="fas fa-user-plus mr-1.5 text-blue-500 text-sm"></i> Registra Ingresso / Check-in Partecipante</h4>
|
||||
|
|
@ -306,9 +599,19 @@
|
|||
</td>
|
||||
<td class="p-3 text-center">
|
||||
@if(!$pres->ora_uscita)
|
||||
<button wire:click="registraCheckout({{ $pres->id }})" class="px-2.5 py-1 bg-red-50 text-red-700 border border-red-200 hover:bg-red-100 rounded-lg text-xxs font-bold transition">
|
||||
Uscita (Check-out)
|
||||
</button>
|
||||
<div class="flex items-center justify-center space-x-1.5">
|
||||
<select wire:model="checkoutDelegatoSoggettoId" class="text-xxs rounded border-slate-300 py-0.5 max-w-[120px] focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Lascia Delega...</option>
|
||||
@foreach($this->disponibiliPresenzaSoggetti as $s)
|
||||
@if($s->id !== $pres->soggetto_id)
|
||||
<option value="{{ $s->id }}">{{ $s->nome_completo }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
<button wire:click="registraCheckout({{ $pres->id }})" class="px-2.5 py-1 bg-red-50 text-red-700 border border-red-200 hover:bg-red-100 rounded-lg text-xxs font-bold transition">
|
||||
Uscita
|
||||
</button>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-slate-400 italic text-xxs">Uscito</span>
|
||||
@endif
|
||||
|
|
@ -406,9 +709,13 @@
|
|||
@foreach($stats['dettaglio_voti'] as $v)
|
||||
<div class="p-2.5 rounded-lg bg-slate-800/40 border border-slate-800 text-[11px] flex justify-between items-center">
|
||||
<span>Int. {{ $v->unitaImmobiliare->interno }} - {{ $v->soggetto->nome_completo }} ({{ number_format($v->millesimi_voto, 3, ',', '.') }}‰)</span>
|
||||
<span class="font-bold uppercase px-1.5 py-0.5 rounded text-[9px] {{ $v->voto === 'favorevole' ? 'bg-emerald-500/20 text-emerald-300' : ($v->voto === 'contrario' ? 'bg-red-500/20 text-red-300' : 'bg-slate-500/20 text-slate-300') }}">
|
||||
{{ $v->voto }}
|
||||
</span>
|
||||
<div class="flex items-center space-x-2">
|
||||
<select onchange="confirm('Vuoi davvero modificare questo voto? La modifica verrà registrata nel log di audit.') && @this.modificaVotoLive({{ $v->id }}, this.value)" class="text-[10px] bg-slate-900 border-slate-700 text-slate-200 rounded py-0.5 px-1.5 focus:ring-indigo-500">
|
||||
<option value="favorevole" {{ $v->voto === 'favorevole' ? 'selected' : '' }}>Favorevole</option>
|
||||
<option value="contrario" {{ $v->voto === 'contrario' ? 'selected' : '' }}>Contrario</option>
|
||||
<option value="astenuto" {{ $v->voto === 'astenuto' ? 'selected' : '' }}>Astenuto</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
|
@ -469,6 +776,17 @@ function showQrCodeModal(url, soggettoNome) {
|
|||
document.getElementById('modal-qr-code').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function printDymoLabels() {
|
||||
if (typeof Filament !== 'undefined' && Filament.notify) {
|
||||
Filament.notify('success', 'Connessione a stampante Dymo in corso...');
|
||||
setTimeout(() => {
|
||||
Filament.notify('success', 'Etichette partecipanti inviate alla stampante Dymo!');
|
||||
}, 1500);
|
||||
} else {
|
||||
alert('Etichette partecipanti inviate alla stampante Dymo!');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.getElementById('modal-qr-code').classList.add('hidden');
|
||||
|
|
|
|||
|
|
@ -23,13 +23,19 @@
|
|||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Servizi / Beni comuni - RISCALDAMENTO</h1>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Cruscotto operativo per utenze, beni comuni, locali condivisi e contatori riscaldamento dello stabile. Qui convivono contratti, servizi ad uso comune, rimborsi spesa, contatori generali e contatori particolari con storico e tariffe.
|
||||
</p>
|
||||
<div class="mt-2 inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700 dark:border-blue-700/50 dark:bg-blue-900/20 dark:text-blue-200">
|
||||
Anno gestione attivo: {{ $annoGestioneAttivo }}
|
||||
<div class="flex flex-nowrap items-center justify-between gap-1.5 overflow-x-auto whitespace-nowrap bg-white dark:bg-gray-900 border rounded-xl px-2 py-1.5 shadow-xs mb-3 text-xs">
|
||||
<div class="flex flex-nowrap items-center gap-1.5">
|
||||
<span class="text-xs font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ $this->stabileAttivo?->codice_stabile }} · {{ $this->stabileAttivo?->denominazione }} | Anno: {{ $annoGestioneAttivo }}
|
||||
</span>
|
||||
@if(str_starts_with(strtoupper($this->tipoGestioneAttiva ?? ''), 'R'))
|
||||
<span class="inline-flex items-center rounded-md bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[9px] font-bold text-amber-700 dark:text-amber-300">
|
||||
Risc: SI
|
||||
</span>
|
||||
@endif
|
||||
<span class="inline-flex items-center rounded-md bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[9px] font-bold text-amber-700 dark:text-amber-300 uppercase tracking-wider">
|
||||
🔥 Riscaldamento Centralizzato
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -65,6 +71,11 @@ class="pb-3 px-1 border-b-2 font-medium text-sm {{ $riscaldamentoTab === 'tariff
|
|||
class="pb-3 px-1 border-b-2 font-medium text-sm {{ $riscaldamentoTab === 'servizi' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||
Servizi / Utenze (tabella)
|
||||
</button>
|
||||
<button type="button"
|
||||
wire:click="setRiscaldamentoTab('storno')"
|
||||
class="pb-3 px-1 border-b-2 font-medium text-sm {{ $riscaldamentoTab === 'storno' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||
Storno Caldaia / Utenze
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
|
@ -941,6 +952,83 @@ class="inline-flex items-center rounded-md border border-cyan-300 bg-white px-3
|
|||
{{ $this->table }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($riscaldamentoTab === 'storno')
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
Registrazione Fattura e Storno Energia Caldaia
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Inserisci i dettagli del documento di spesa energetica. Il sistema applicherà la formula di quadratura automatica per lo storno.
|
||||
</p>
|
||||
|
||||
<form wire:submit.prevent="salvaStorno" class="mt-6 space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300">Numero Fattura / Documento *</label>
|
||||
<input type="text" wire:model="stornoForm.numero_fattura" required
|
||||
class="mt-1 block w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 text-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300">Data Documento / Spesa *</label>
|
||||
<input type="date" wire:model="stornoForm.data_fattura" required
|
||||
class="mt-1 block w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 text-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300">Numerazione Protocollo *</label>
|
||||
<select wire:model="stornoForm.protocollo_mode" required
|
||||
class="mt-1 block w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 text-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="unica_stabile">Protocollo Unico Stabile (sequenziale globale)</option>
|
||||
<option value="divisa_gestione">Protocollo Separato per Gestione</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-6 dark:border-gray-800">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">Calcolo della Ripartizione / Storno</h4>
|
||||
<div class="mt-4 grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300">Totale Fattura Speso (€) *</label>
|
||||
<input type="number" step="0.01" min="0.01" wire:model.live="stornoForm.totale_fattura" required
|
||||
class="mt-1 block w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 text-sm font-semibold focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300">Percentuale Storno (%)</label>
|
||||
<input type="number" step="1" min="0" max="100" wire:model.live="stornoForm.percentuale_storno_caldaia"
|
||||
class="mt-1 block w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 text-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-amber-600 dark:text-amber-400">Quota Ordinaria stornata (€)</label>
|
||||
<input type="number" step="0.01" wire:model.live="stornoForm.quota_ordinaria" required
|
||||
class="mt-1 block w-full rounded-lg border-amber-300 bg-amber-50/50 dark:border-amber-700/50 dark:bg-amber-950/20 dark:text-amber-200 text-sm font-medium focus:ring-amber-500 focus:border-amber-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-blue-600 dark:text-blue-400">Quota Riscaldamento (€)</label>
|
||||
<input type="number" step="0.01" wire:model.live="stornoForm.quota_riscaldamento" required
|
||||
class="mt-1 block w-full rounded-lg border-blue-300 bg-blue-50/50 dark:border-blue-700/50 dark:bg-blue-950/20 dark:text-blue-200 text-sm font-medium focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||
<button type="submit"
|
||||
class="inline-flex justify-center rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition">
|
||||
Registra e Storna in Contabilità
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Modal per invio email ripartizione -->
|
||||
|
|
|
|||
|
|
@ -23,13 +23,19 @@
|
|||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Servizi / Beni comuni - ACQUA</h1>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Cruscotto operativo per utenze, beni comuni, locali condivisi e contatori acqua dello stabile. Qui convivono contratti, servizi ad uso comune, rimborsi spesa, contatori generali e contatori particolari con storico e tariffe.
|
||||
</p>
|
||||
<div class="mt-2 inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700 dark:border-blue-700/50 dark:bg-blue-900/20 dark:text-blue-200">
|
||||
Anno gestione attivo: {{ $annoGestioneAttivo }}
|
||||
<div class="flex flex-nowrap items-center justify-between gap-1.5 overflow-x-auto whitespace-nowrap bg-white dark:bg-gray-900 border rounded-xl px-2 py-1.5 shadow-xs mb-3 text-xs">
|
||||
<div class="flex flex-nowrap items-center gap-1.5">
|
||||
<span class="text-xs font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ $this->stabileAttivo?->codice_stabile }} · {{ $this->stabileAttivo?->denominazione }} | Anno: {{ $annoGestioneAttivo }}
|
||||
</span>
|
||||
@if(str_starts_with(strtoupper($this->tipoGestioneAttiva ?? ''), 'R'))
|
||||
<span class="inline-flex items-center rounded-md bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[9px] font-bold text-amber-700 dark:text-amber-300">
|
||||
Risc: SI
|
||||
</span>
|
||||
@endif
|
||||
<span class="inline-flex items-center rounded-md bg-sky-50 dark:bg-sky-900/30 px-1.5 py-0.5 text-[9px] font-bold text-sky-700 dark:text-sky-300 uppercase tracking-wider">
|
||||
💧 Acqua
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -70,6 +76,15 @@ class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'servizi' ? 'bo
|
|||
class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'pagamenti_cbill' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||
Pagamenti CBILL
|
||||
</button>
|
||||
<button type="button"
|
||||
wire:click="setAcquaTab('orfani')"
|
||||
class="pb-3 px-1 border-b-2 font-medium text-sm {{ $acquaTab === 'orfani' ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||
Orfani Contatori
|
||||
@php $orfaniCount = count($this->orfaniStagingRows); @endphp
|
||||
@if($orfaniCount > 0)
|
||||
<span class="ml-1.5 rounded-full bg-red-100 px-2 py-0.5 text-xs font-bold text-red-800">{{ $orfaniCount }}</span>
|
||||
@endif
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
|
@ -986,6 +1001,85 @@ class="inline-flex items-center gap-1 rounded bg-teal-50 px-2.5 py-1 text-xs fon
|
|||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($acquaTab === 'orfani')
|
||||
<div class="rounded-xl border bg-white shadow-sm overflow-hidden mt-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="px-6 py-4 border-b bg-red-50/30 dark:bg-red-950/10 dark:border-gray-800">
|
||||
<h3 class="text-sm font-bold text-slate-800 dark:text-red-400 uppercase tracking-wider flex items-center">
|
||||
<i class="fas fa-exclamation-triangle mr-2 text-red-500"></i>
|
||||
Staging Letture Contatori Orfani / Non Associati
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mb-4">
|
||||
Queste letture provengono da file CSV importati ma presentano matricole di contatore che non corrispondono ad alcuna unità immobiliare del condominio.<br>
|
||||
Seleziona l'unità corrispondente dal menu a tendina e premi <strong>Associa</strong>. Il sistema memorizzerà la matricola nell'anagrafica unità (auto-apprendimento) e importerà la lettura.
|
||||
</p>
|
||||
|
||||
<div class="mb-4 p-4 border rounded-xl bg-slate-50 dark:bg-gray-800/40 dark:border-gray-800">
|
||||
<form wire:submit.prevent="caricaFileLettureOrfane" class="flex flex-col gap-2">
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300">Carica file letture esterno (CSV/TXT)</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="file" wire:model="fileLettureOrfane" class="text-xs text-gray-600 border rounded p-1 dark:border-gray-700 dark:bg-gray-800">
|
||||
<button type="submit" class="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs font-bold shadow-xs">Carica</button>
|
||||
</div>
|
||||
@error('fileLettureOrfane') <span class="text-red-500 text-xxs mt-1 block">{{ $message }}</span> @enderror
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto border rounded-xl dark:border-gray-800">
|
||||
<table class="w-full text-left border-collapse text-xs">
|
||||
<thead>
|
||||
<tr class="bg-slate-100 dark:bg-gray-800 text-slate-700 dark:text-gray-300 font-bold uppercase border-b dark:border-gray-700 text-[10px] tracking-wider">
|
||||
<th class="p-4">Matricola Contatore</th>
|
||||
<th class="p-4">Interno Originale CSV</th>
|
||||
<th class="p-4">Data Lettura</th>
|
||||
<th class="p-4 text-right">Valore Lettura</th>
|
||||
<th class="p-4">Nome File Origine</th>
|
||||
<th class="p-4">Associazione Unità Immobiliare (Auto-Apprendimento)</th>
|
||||
<th class="p-4 text-center">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y dark:divide-gray-800">
|
||||
@forelse($this->orfaniStagingRows as $orf)
|
||||
<tr class="hover:bg-slate-50/50 dark:hover:bg-gray-800/30">
|
||||
<td class="p-4 font-bold text-red-700 dark:text-red-400 font-mono">{{ $orf->matricola }}</td>
|
||||
<td class="p-4 font-medium text-slate-600 dark:text-gray-300">{{ $orf->interno_originale ?: '—' }}</td>
|
||||
<td class="p-4 text-slate-500 dark:text-gray-400">{{ \Carbon\Carbon::parse($orf->data_lettura)->format('d/m/Y') }}</td>
|
||||
<td class="p-4 text-right font-bold text-slate-900 dark:text-gray-100">{{ number_format($orf->valore_lettura, 3, ',', '.') }} mc</td>
|
||||
<td class="p-4 text-slate-400 dark:text-gray-500 truncate max-w-[150px]" title="{{ $orf->file_name }}">{{ $orf->file_name }}</td>
|
||||
<td class="p-4">
|
||||
<select id="select_unita_{{ $orf->id }}" class="text-xs rounded border-gray-300 dark:border-gray-700 dark:bg-gray-800 shadow-sm text-gray-900 dark:text-gray-100 py-1 w-full focus:ring-blue-500 focus:border-blue-500 max-w-sm">
|
||||
<option value="">Seleziona unità...</option>
|
||||
@foreach($this->orfaniUnitaOptions as $uId => $uLabel)
|
||||
<option value="{{ $uId }}">{{ $uLabel }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td class="p-4 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onclick="var sel = document.getElementById('select_unita_{{ $orf->id }}'); if(sel.value) { @this.associaOrfano({{ $orf->id }}, sel.value) } else { alert('Seleziona prima un\'unità!') }"
|
||||
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xxs font-bold transition flex items-center justify-center shadow-xs mx-auto"
|
||||
>
|
||||
Associa
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="p-8 text-center text-slate-400 dark:text-gray-500 italic">
|
||||
Nessuna lettura contatore orfana in staging. Tutte le matricole importate corrispondono correttamente alle unità immobiliari.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Modal per invio email ripartizione -->
|
||||
|
|
|
|||
|
|
@ -1,10 +1,66 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="mx-auto max-w-7xl space-y-4">
|
||||
<div class="flex items-center gap-2 text-lg font-semibold text-gray-900">
|
||||
<x-filament::icon icon="heroicon-o-table-cells" class="h-5 w-5 text-primary-600" />
|
||||
<span>Tabelle millesimali</span>
|
||||
<div class="flex flex-nowrap items-center justify-between gap-1.5 overflow-x-auto whitespace-nowrap bg-white dark:bg-gray-900 border rounded-xl px-2 py-1.5 shadow-xs mb-3 text-xs">
|
||||
<div class="flex flex-nowrap items-center gap-1.5">
|
||||
<span class="text-xs font-bold text-gray-900 dark:text-gray-100">
|
||||
{{ $this->stabileAttivo?->codice_stabile }} · {{ $this->stabileAttivo?->denominazione }} | Anno: {{ \App\Support\AnnoGestioneContext::resolveActiveAnno(auth()->user()) }}
|
||||
</span>
|
||||
@if(str_starts_with(strtoupper($this->tipoGestioneAttiva ?? ''), 'R'))
|
||||
<span class="inline-flex items-center rounded-md bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[9px] font-bold text-amber-700 dark:text-amber-300">
|
||||
Risc: SI
|
||||
</span>
|
||||
@endif
|
||||
<span class="inline-flex items-center rounded-md bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[9px] font-bold text-emerald-700 dark:text-emerald-300 uppercase tracking-wider">
|
||||
📐 Millesimi
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$audit = $this->millesimiAudit;
|
||||
@endphp
|
||||
|
||||
@if($audit['has_issues'])
|
||||
<div class="rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-900/50 dark:bg-red-950/20">
|
||||
<div class="flex items-start gap-3">
|
||||
<x-filament::icon icon="heroicon-o-exclamation-triangle" class="h-5 w-5 text-red-600 dark:text-red-400 mt-0.5" />
|
||||
<div class="space-y-2 w-full">
|
||||
<h4 class="text-sm font-bold text-red-800 dark:text-red-300">
|
||||
ALLERTA CRITICA: Anomalie riscontrate nell'Audit Millesimi e Unità
|
||||
</h4>
|
||||
|
||||
@if(!empty($audit['tables_out_of_quadratura']))
|
||||
<div class="text-xs text-red-700 dark:text-red-400">
|
||||
<strong>Tabelle fuori quadratura (Somma ≠ totale atteso):</strong>
|
||||
<ul class="list-disc list-inside mt-1 space-y-1">
|
||||
@foreach($audit['tables_out_of_quadratura'] as $tIssue)
|
||||
<li>Tabella <strong>{{ $tIssue['codice'] }} - {{ $tIssue['nome'] }}</strong>: Somma calcolata: {{ number_format($tIssue['somma'], 2, ',', '.') }} millesimi (atteso: {{ number_format($tIssue['atteso'], 2, ',', '.') }})</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(!empty($audit['missing_legacy_units']))
|
||||
<div class="text-xs text-red-700 dark:text-red-400">
|
||||
<strong>Unità Staging MDB Mancanti in NetGescon (Audit 1:1 fallito):</strong>
|
||||
<span class="inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-semibold text-red-800 dark:bg-red-900/30 dark:text-red-200 ml-1">
|
||||
{{ count($audit['missing_legacy_units']) }} Unità Mancanti (Attese {{ $audit['total_legacy_count'] }} in staging, trovate {{ $audit['total_netgescon_count'] }} attive)
|
||||
</span>
|
||||
<div class="mt-2 max-h-32 overflow-y-auto border border-red-200 dark:border-red-900/40 rounded-lg p-2 bg-white dark:bg-gray-900 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
|
||||
@foreach($audit['missing_legacy_units'] as $mUnit)
|
||||
<div class="p-1 rounded bg-red-50/50 dark:bg-red-950/10 border border-red-100 dark:border-red-900/20 text-xxs">
|
||||
<span class="font-bold text-red-900 dark:text-red-200">#{{ $mUnit['cod_cond'] }}</span> · {{ $mUnit['posizione'] }}<br>
|
||||
<span class="text-gray-500">{{ $mUnit['nominativo'] }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -36,11 +36,20 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 lg:grid-cols-2">
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Casella ufficiale</div>
|
||||
<div class="mt-2 text-sm text-slate-900">{{ collect($this->officialMailboxes)->firstWhere('enabled', true)['email'] ?? 'Nessuna casella attiva' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Per GERMANICO 79 la configurazione proposta usa viagermanico79@gmail.com.</div>
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Casella ufficiale</div>
|
||||
<div class="mt-2 text-sm text-slate-900 font-medium">{{ collect($this->officialMailboxes)->firstWhere('enabled', true)['email'] ?? 'Nessuna casella attiva' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Per questo stabile usa la casella email/PEC dedicata per importare comunicazioni e allegati.</div>
|
||||
</div>
|
||||
@if(collect($this->officialMailboxes)->firstWhere('enabled', true))
|
||||
<div class="mt-3">
|
||||
<button type="button" wire:click="importOfficialGmail" class="inline-flex items-center rounded-lg bg-purple-600 hover:bg-purple-500 text-white px-3 py-1.5 text-xs font-bold transition shadow-xs">
|
||||
<i class="fas fa-sync mr-1.5"></i> Sincronizza Mail
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white p-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Account Google dedicato</div>
|
||||
<div class="mt-2 text-sm text-slate-900">{{ $this->officialGoogleAccount['email'] ?? 'Da collegare' }}</div>
|
||||
|
|
@ -49,12 +58,30 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-sm font-semibold text-slate-900">Template cartelle Drive condominio</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Struttura base replicabile per ogni stabile e riutilizzabile anche in fase di passaggio consegne.</div>
|
||||
<div class="rounded-xl border bg-white p-4 space-y-4">
|
||||
<div class="flex items-center justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">Template cartelle Drive condominio (ISO)</div>
|
||||
<div class="mt-1 text-xs text-slate-500">Struttura base replicabile per ogni stabile e riutilizzabile anche in fase di passaggio consegne.</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
@if(!empty($stabile->configurazione_avanzata['google_drive_folder_url']))
|
||||
<a href="{{ $stabile->configurazione_avanzata['google_drive_folder_url'] }}" target="_blank" class="inline-flex items-center rounded-lg bg-green-50 px-3 py-2 text-xs font-bold text-green-700 border border-green-200 hover:bg-green-100 transition">
|
||||
<i class="fab fa-google-drive mr-1.5"></i> Apri Drive Stabile
|
||||
</a>
|
||||
@endif
|
||||
<button type="button" wire:click="creaCartelleDrive" wire:loading.attr="disabled" class="inline-flex items-center rounded-lg bg-blue-600 hover:bg-blue-500 px-3 py-2 text-xs font-bold text-white shadow-xs transition">
|
||||
<i class="fas fa-sync mr-1.5"></i> Sincronizza/Crea Cartelle Drive
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-2 md:grid-cols-2 xl:grid-cols-3">
|
||||
@foreach($this->driveTemplateFolders as $folder)
|
||||
<div class="rounded-lg border bg-slate-50 px-3 py-2 text-sm text-slate-700">{{ $folder }}</div>
|
||||
<div class="rounded-lg border bg-slate-50 px-3 py-2 text-sm text-slate-700 flex items-center justify-between">
|
||||
<span>{{ $folder }}</span>
|
||||
<i class="fas fa-folder text-amber-500"></i>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -603,9 +603,32 @@
|
|||
@if(count($stabili) > 0)
|
||||
<ul class="mt-2 list-disc pl-5 text-sm">
|
||||
@foreach($stabili as $s)
|
||||
@php
|
||||
$uCollegata = \App\Models\UnitaImmobiliare::where('stabile_id', $s['id'])
|
||||
->where(function($q) {
|
||||
$q->whereIn('id', function($sub) {
|
||||
$sub->select('unita_immobiliare_id')
|
||||
->from('unita_anagrafica_periodo')
|
||||
->where('anagrafica_id', $this->rubrica->id);
|
||||
})
|
||||
->orWhereIn('id', function($sub) {
|
||||
$sub->select('unita_immobiliare_id')
|
||||
->from('proprieta')
|
||||
->where('anagrafica_id', $this->rubrica->id);
|
||||
});
|
||||
})
|
||||
->first();
|
||||
|
||||
$linkUrl = $uCollegata
|
||||
? "/admin-filament/unita-immobiliare?unita_id=" . $uCollegata->id
|
||||
: \App\Filament\Pages\Gescon\StabileScheda::getUrl(['record' => $s['id']], panel: 'admin-filament');
|
||||
@endphp
|
||||
<li>
|
||||
<a class="text-primary-600 hover:underline" href="{{ \App\Filament\Pages\Gescon\StabileScheda::getUrl(['record' => $s['id']], panel: 'admin-filament') }}">
|
||||
<a class="text-primary-600 hover:underline" href="{{ $linkUrl }}">
|
||||
{{ $s['codice'] }} - {{ $s['nome'] }}
|
||||
@if($uCollegata)
|
||||
<span class="text-[11px] font-semibold text-emerald-600 dark:text-emerald-400">(Unità {{ $uCollegata->codice_unita ?? $uCollegata->interno }})</span>
|
||||
@endif
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -486,19 +486,24 @@
|
|||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
@if(in_array(strtoupper((string) ($row->cod_spe ?? '')), ['I05', 'I06'], true) && !empty($row->n_spe))
|
||||
<x-filament::button
|
||||
size="xs"
|
||||
color="gray"
|
||||
x-data
|
||||
x-on:click="$wire.openDettPersModal({{ (int) $row->n_spe }}); $dispatch('open-modal', { id: 'dett-pers-modal' })"
|
||||
>Apri</x-filament::button>
|
||||
@if(isset($row->dett_pers_ok) && !$row->dett_pers_ok)
|
||||
<div class="mt-1 text-[10px] text-red-600">Da completare</div>
|
||||
<div class="flex flex-col gap-1 items-start">
|
||||
@if(in_array(strtoupper((string) ($row->cod_spe ?? '')), ['I05', 'I06'], true) && !empty($row->n_spe))
|
||||
<x-filament::button
|
||||
size="xs"
|
||||
color="gray"
|
||||
x-data
|
||||
x-on:click="$wire.openDettPersModal({{ (int) $row->n_spe }}); $dispatch('open-modal', { id: 'dett-pers-modal' })"
|
||||
>Apri</x-filament::button>
|
||||
@if(isset($row->dett_pers_ok) && !$row->dett_pers_ok)
|
||||
<div class="mt-1 text-[10px] text-red-600">Da completare</div>
|
||||
@endif
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
<button type="button" wire:click="riallineaScrittureLegacy" class="inline-flex items-center text-[10px] text-amber-600 hover:text-amber-500 font-semibold mt-1">
|
||||
🔄 Riallinea
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@
|
|||
<x-filament::button size="sm" color="info" wire:click="refreshUpdateProgress" :disabled="!$this->updateInProgress">
|
||||
Aggiorna avanzamento refresh
|
||||
</x-filament::button>
|
||||
<x-filament::button size="sm" color="warning" wire:click="simulaChiamataInArrivo">
|
||||
Simula Inbound CTI
|
||||
</x-filament::button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-[11px] text-amber-900">
|
||||
|
|
|
|||
|
|
@ -541,6 +541,43 @@
|
|||
<textarea rows="3" wire:model.defer="insuranceNotes" class="w-full rounded-lg border-gray-300" placeholder="Dettagli apertura o aggiornamento pratica"></textarea>
|
||||
</label>
|
||||
|
||||
<div class="mt-4 border-t border-gray-200 pt-4 dark:border-gray-800 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Stima Importo Sinistro (€)</span>
|
||||
<input type="number" step="0.01" min="0" wire:model.defer="insuranceEstimatedAmount" class="w-full rounded-lg border-gray-300" placeholder="0,00" />
|
||||
</label>
|
||||
|
||||
<div class="flex items-center mt-6">
|
||||
<label class="inline-flex items-center text-sm cursor-pointer">
|
||||
<input type="checkbox" wire:model.defer="insuranceCreateDoubleEntry" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-500 focus:ring-blue-500" />
|
||||
<span class="ml-2 font-medium">Registra stima in Partita Doppia (Credito vs Assicurazione)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 border-t border-gray-200 pt-4 dark:border-gray-800 space-y-4">
|
||||
<div class="flex items-center">
|
||||
<label class="inline-flex items-center text-sm cursor-pointer">
|
||||
<input type="checkbox" wire:model="legalPracticeActive" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-500 focus:ring-blue-500" />
|
||||
<span class="ml-2 font-bold text-gray-900 dark:text-gray-100">Apri Pratica Legale in-line</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if($legalPracticeActive)
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 bg-slate-50 dark:bg-slate-900/30 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium text-gray-700 dark:text-gray-300">Avvocato / Legale Incaricato</span>
|
||||
<input type="text" wire:model.defer="legalPracticeLawyer" class="w-full rounded-lg border-gray-300" placeholder="Es. Avv. Mario Rossi" />
|
||||
</label>
|
||||
|
||||
<label class="block text-sm md:col-span-2">
|
||||
<span class="mb-1 block font-medium text-gray-700 dark:text-gray-300">Note Pratica Legale</span>
|
||||
<textarea rows="3" wire:model.defer="legalPracticeNotes" class="w-full rounded-lg border-gray-300" placeholder="Dettagli, citazioni, udienze, accordi..."></textarea>
|
||||
</label>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($ticket->insuranceClaim)
|
||||
<div class="mt-3 rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700">
|
||||
Aperto il {{ optional($ticket->insuranceClaim->opened_at)->format('d/m/Y H:i') ?: '-' }}
|
||||
|
|
|
|||
|
|
@ -137,12 +137,24 @@ class="rounded-lg border px-3 py-2 text-xs font-semibold transition {{ $this->un
|
|||
@if($labelUnita !== '' && $condominoNome !== '' && trim($labelUnita) !== trim($condominoNome))
|
||||
<div class="text-sm text-gray-700">{{ $labelUnita }}</div>
|
||||
@endif
|
||||
<div class="text-sm text-gray-700">Condomino di riferimento: {{ $condominoNome !== '' ? $condominoNome : '—' }}</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-700 mt-1">
|
||||
<span class="inline-flex items-center rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] font-bold text-emerald-700 dark:text-emerald-300">C</span>
|
||||
<span>Condomino: {{ $condominoNome !== '' ? trim(str_replace([' - ', ' 0 ', ' 0', '-'], [' ', ' ', ' ', ' '], $condominoNome)) : '—' }}</span>
|
||||
</div>
|
||||
@if($comproprietariCount > 0)
|
||||
<div class="text-xs text-gray-500">Comproprietari collegati: {{ $comproprietariCount }}</div>
|
||||
<div class="text-xs text-gray-500 pl-6">Comproprietari collegati: {{ $comproprietariCount }}</div>
|
||||
@endif
|
||||
<div class="text-sm text-gray-700">Inquilino: {{ $inquilinoNome !== '' ? $inquilinoNome : '—' }}</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-700 mt-1">
|
||||
<span class="inline-flex items-center rounded bg-sky-50 dark:bg-sky-900/30 px-1.5 py-0.5 text-[10px] font-bold text-sky-700 dark:text-sky-300">I</span>
|
||||
<span>Inquilino: {{ $inquilinoNome !== '' ? trim(str_replace([' - ', ' 0 ', ' 0', '-'], [' ', ' ', ' ', ' '], $inquilinoNome)) : '—' }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">{{ trim(($unita->indirizzo ?? '').' '.($unita->civico ?? '').' '.($unita->comune ?? '')) }}</div>
|
||||
<div class="mt-2 pt-2 border-t border-gray-100 dark:border-gray-800">
|
||||
<a href="/admin-filament/condomini/catasto-hub?unita_id={{ $unita->id }}" class="inline-flex items-center gap-2 rounded-lg bg-primary-600 hover:bg-primary-700 text-white font-semibold px-4 py-2 text-xs shadow-sm transition">
|
||||
<x-filament::icon icon="heroicon-o-arrow-top-right-on-square" class="h-4 w-4" />
|
||||
<span>[ Apri in HUB Catasto ]</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
|
|
@ -175,7 +187,7 @@ class="rounded-lg border px-3 py-2 text-xs font-semibold transition {{ $this->un
|
|||
<div class="text-sm font-semibold text-gray-900">{{ $condominoNome !== '' ? $condominoNome : '—' }}</div>
|
||||
<div class="text-xs text-gray-500">CF: {{ $condominoCf !== '' ? $condominoCf : '—' }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border p-3 {{ ($estrattoTipo ?? 'condomini') === 'inquilini' ? 'border-emerald-300 bg-emerald-50' : 'bg-gray-50' }} {{ $hasInquilino ? '' : 'opacity-60' }}">
|
||||
<div class="rounded-lg border p-3 {{ ($estrattoTipo ?? 'condomini') === 'inquilini' ? 'border-sky-300 bg-sky-50' : 'bg-gray-50' }} {{ $hasInquilino ? '' : 'opacity-60' }}">
|
||||
<div class="text-xs text-gray-500">Inquilino</div>
|
||||
<div class="text-sm font-semibold text-gray-900">{{ $inquilinoNome !== '' ? $inquilinoNome : '—' }}</div>
|
||||
<div class="text-xs text-gray-500">CF: {{ $relazioniPerTipo['inquilini'][0]['codice_fiscale'] ?? '—' }}</div>
|
||||
|
|
@ -191,7 +203,7 @@ class="w-full rounded-md border px-2 py-1 text-left text-xs font-semibold transi
|
|||
|
||||
<button type="button"
|
||||
wire:click="setEstrattoTipo('inquilini')"
|
||||
class="w-full rounded-md border px-2 py-1 text-left text-xs font-semibold transition {{ ($estrattoTipo ?? 'condomini') === 'inquilini' ? 'border-emerald-300 bg-emerald-50 text-emerald-700' : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50' }} {{ $hasInquilino ? '' : 'opacity-50' }}"
|
||||
class="w-full rounded-md border px-2 py-1 text-left text-xs font-semibold transition {{ ($estrattoTipo ?? 'condomini') === 'inquilini' ? 'border-sky-300 bg-sky-50 text-sky-700' : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50' }} {{ $hasInquilino ? '' : 'opacity-50' }}"
|
||||
@if(!$hasInquilino) disabled @endif>
|
||||
<div>Inquilino</div>
|
||||
<div class="text-[11px] text-gray-600">Addebito {{ number_format($totIAddebH, 2, ',', '.') }} € · Incasso {{ number_format($totIPagH, 2, ',', '.') }} € · Residuo {{ number_format($totIResH, 2, ',', '.') }} €</div>
|
||||
|
|
@ -207,6 +219,9 @@ class="w-full rounded-md border px-2 py-1 text-left text-xs font-semibold transi
|
|||
@php
|
||||
$tabs = [
|
||||
'riepilogo' => ['icon' => 'heroicon-o-rectangle-group', 'label' => 'Riepilogo'],
|
||||
'catasto' => ['icon' => 'heroicon-o-building-office-2', 'label' => 'Catasto'],
|
||||
'comproprietari' => ['icon' => 'heroicon-o-users', 'label' => 'Comproprietari ed Aventi Diritto'],
|
||||
'preferenze' => ['icon' => 'heroicon-o-envelope', 'label' => 'Comunicazioni e Canali di Invio'],
|
||||
'millesimi' => ['icon' => 'heroicon-o-chart-pie', 'label' => 'Tabelle millesimali'],
|
||||
'persone' => ['icon' => 'heroicon-o-user-group', 'label' => 'Persone collegate'],
|
||||
'acqua' => ['icon' => 'heroicon-o-beaker', 'label' => 'Acqua'],
|
||||
|
|
@ -266,35 +281,14 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
|
|||
</x-filament::section>
|
||||
</div>
|
||||
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Dati catastali</x-slot>
|
||||
<x-slot name="description">Identificativi e classamento</x-slot>
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach([
|
||||
['Sezione urbana', $unita->sezione_urbana ?? 'ND'],
|
||||
['Foglio', $unita->foglio ?? 'ND'],
|
||||
['Particella', $unita->particella ?? 'ND'],
|
||||
['Subalterno', $unita->subalterno ?? 'ND'],
|
||||
['Categoria', $unita->categoria_catastale ?? 'ND'],
|
||||
['Classe', $unita->classe ?? 'ND'],
|
||||
['Consistenza', $unita->consistenza ?? 'ND'],
|
||||
['Rendita', $unita->rendita_catastale ?? 'ND'],
|
||||
] as [$label, $value])
|
||||
<div class="rounded-lg border bg-gray-50 p-3">
|
||||
<div class="text-xs text-gray-500">{{ $label }}</div>
|
||||
<div class="text-sm font-semibold text-gray-900">{{ $value }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Persone collegate</x-slot>
|
||||
<x-slot name="description">Proprietari, inquilini, altri</x-slot>
|
||||
<div class="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach([
|
||||
'proprietari' => ['label' => 'Proprietari', 'color' => 'text-primary-700'],
|
||||
'inquilini' => ['label' => 'Inquilini', 'color' => 'text-emerald-700'],
|
||||
'inquilini' => ['label' => 'Inquilini', 'color' => 'text-sky-700'],
|
||||
'altri' => ['label' => 'Altri soggetti', 'color' => 'text-gray-700'],
|
||||
] as $key => $cfg)
|
||||
@php $relazioni = $relazioniPerTipo[$key] ?? []; @endphp
|
||||
|
|
@ -414,6 +408,182 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
|
|||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if($tab === 'catasto')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Dati catastali</x-slot>
|
||||
<x-slot name="description">Risultanze catastali ed identificativi ufficiali consolidati</x-slot>
|
||||
|
||||
<div class="max-w-3xl mx-auto rounded-xl border border-gray-200 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-900/50 p-6 shadow-xs space-y-6">
|
||||
<div class="grid grid-cols-2 gap-y-4 gap-x-6 text-sm">
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Sezione Urbana:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->sezione_urbana ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Foglio:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->foglio ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Particella:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->particella ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Subalterno:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->subalterno ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Categoria:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ \App\Filament\Pages\Condomini\CatastoHub::formattaCategoriaCatastale($unita->categoria_catastale ?? '') ?: '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Classe:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->classe ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Consistenza:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ ($unita->superficie && str_starts_with(\App\Filament\Pages\Condomini\CatastoHub::estraiCodiceCompatto($unita->categoria_catastale ?? ''), 'C')) ? ($unita->superficie . ' m²') : ($unita->numero_vani ? ($unita->numero_vani . ' vani') : ($unita->vani ? ($unita->vani . ' vani') : '—')) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Rendita Euro:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->rendita_catastale ? '€ ' . number_format($unita->rendita_catastale, 2, ',', '.') : '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">Piano AdE:</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ $unita->piano ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span class="text-gray-500 font-medium">idImmobile:</span>
|
||||
<span class="font-mono text-xs font-semibold text-gray-600 dark:text-gray-400 max-w-44 truncate" title="{{ $unita->codice_univoco ?? '—' }}">{{ $unita->codice_univoco ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||
<a href="/admin-filament/condomini/catasto-hub?unita_id={{ $unita->id }}" class="inline-flex items-center gap-2 rounded-lg bg-primary-600 hover:bg-primary-700 text-white font-semibold px-6 py-2.5 text-sm shadow-md transition">
|
||||
<x-filament::icon icon="heroicon-o-arrow-top-right-on-square" class="h-4 w-4" />
|
||||
<span>[ Apri in HUB Catasto ]</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if($tab === 'comproprietari')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Comproprietari ed Aventi Diritto</x-slot>
|
||||
<x-slot name="description">Elenco analitico dei soggetti con titolo reale o quota di possesso sull'immobile</x-slot>
|
||||
|
||||
<div class="space-y-4">
|
||||
@php $comproprietari = $relazioniPerTipo['proprietari'] ?? []; @endphp
|
||||
@if(empty($comproprietari))
|
||||
<div class="text-sm text-gray-500">Nessun proprietario registrato.</div>
|
||||
@else
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
||||
<table class="w-full text-left text-sm text-gray-500">
|
||||
<thead class="bg-gray-50 text-xs uppercase text-gray-700 font-semibold border-b">
|
||||
<tr>
|
||||
<th class="px-6 py-3">Soggetto / Nominativo</th>
|
||||
<th class="px-6 py-3">Codice Fiscale</th>
|
||||
<th class="px-6 py-3">Titolo / Diritto</th>
|
||||
<th class="px-6 py-3 text-right">Quota Proprietà</th>
|
||||
<th class="px-6 py-3">Periodo di Possesso</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
@foreach($comproprietari as $p)
|
||||
<tr class="hover:bg-gray-50/50">
|
||||
<td class="px-6 py-4 font-semibold text-gray-900">{{ $p['nome'] }}</td>
|
||||
<td class="px-6 py-4 font-mono text-gray-600">{{ $p['codice_fiscale'] ?? '—' }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="rounded bg-primary-50 px-2.5 py-1 text-xs font-bold text-primary-700 uppercase tracking-wide">
|
||||
{{ $p['tipo'] ?? 'Proprietà' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right font-extrabold text-gray-900">
|
||||
{{ $p['quota_label'] ?? ($p['percentuale_label'] ?? '—') }}%
|
||||
</td>
|
||||
<td class="px-6 py-4 text-xs text-gray-600">
|
||||
@if(($p['data_inizio'] ?? null) || ($p['data_fine'] ?? null))
|
||||
@if($p['data_inizio'] ?? null) Da {{ $p['data_inizio'] }} @endif
|
||||
@if($p['data_fine'] ?? null) Fino a {{ $p['data_fine'] }} @endif
|
||||
@else
|
||||
Da sempre
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if($tab === 'preferenze')
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Comunicazioni e Canali di Invio</x-slot>
|
||||
<x-slot name="description">Preferenze di spedizione e canali digitali ufficiali per proprietari e comproprietari</x-slot>
|
||||
|
||||
<div class="space-y-6">
|
||||
@if(empty($canaliComunicazione))
|
||||
<div class="text-sm text-gray-500">Nessun proprietario o comproprietario registrato per questa unità.</div>
|
||||
@else
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
||||
<table class="w-full text-left text-sm text-gray-500">
|
||||
<thead class="bg-gray-50 text-xs uppercase text-gray-700 font-semibold border-b">
|
||||
<tr>
|
||||
<th class="px-6 py-3">Soggetto / Nominativo</th>
|
||||
<th class="px-6 py-3">Convocazione Assemblea</th>
|
||||
<th class="px-6 py-3">Invio Verbali</th>
|
||||
<th class="px-6 py-3">Solleciti e Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
@foreach($canaliComunicazione as $soggettoId => $p)
|
||||
<tr class="hover:bg-gray-50/50">
|
||||
<td class="px-6 py-4">
|
||||
<div class="font-semibold text-gray-900">{{ $p['nominativo'] }}</div>
|
||||
<div class="text-xs text-gray-500 font-mono mt-0.5">{{ $p['codice_fiscale'] ?? '—' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<select
|
||||
wire:change="salvaCanaleComunicazione({{ $soggettoId }}, 'convocazione', $event.target.value)"
|
||||
class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-semibold text-gray-700">
|
||||
@foreach(['PEC', 'Raccomandata AR', 'Posta Ordinaria', 'Mano'] as $opt)
|
||||
<option value="{{ $opt }}" {{ $p['convocazione'] === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<select
|
||||
wire:change="salvaCanaleComunicazione({{ $soggettoId }}, 'verbali', $event.target.value)"
|
||||
class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-semibold text-gray-700">
|
||||
@foreach(['PEC', 'Raccomandata AR', 'Posta Ordinaria', 'Mano'] as $opt)
|
||||
<option value="{{ $opt }}" {{ $p['verbali'] === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<select
|
||||
wire:change="salvaCanaleComunicazione({{ $soggettoId }}, 'solleciti', $event.target.value)"
|
||||
class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 font-semibold text-gray-700">
|
||||
@foreach(['Email Standard', 'PEC', 'Posta Ordinaria'] as $opt)
|
||||
<option value="{{ $opt }}" {{ $p['solleciti'] === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if($tab === 'millesimi')
|
||||
<x-filament::section>
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-gray-900 mb-3">
|
||||
|
|
@ -500,7 +670,7 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
@php
|
||||
$blocchiRelazioni = [
|
||||
'proprietari' => ['label' => 'Proprietari / comproprietari', 'class' => 'text-primary-700'],
|
||||
'inquilini' => ['label' => 'Inquilini / conduttori', 'class' => 'text-emerald-700'],
|
||||
'inquilini' => ['label' => 'Inquilini / conduttori', 'class' => 'text-sky-700'],
|
||||
'altri' => ['label' => 'Altri soggetti collegati', 'class' => 'text-gray-700'],
|
||||
];
|
||||
@endphp
|
||||
|
|
@ -675,10 +845,112 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$tabelleAcqua = collect($preventiviPerTabella)->filter(fn($t) => str_starts_with(strtoupper($t['codice'] ?? ''), 'AC'))->values();
|
||||
$ripartiAcqua = collect($ripartizioniPerTabella)->filter(fn($t) => str_starts_with(strtoupper($t['tabella_codice'] ?? ''), 'AC'))->values();
|
||||
@endphp
|
||||
|
||||
@if($tabelleAcqua->isNotEmpty())
|
||||
<div class="mt-6">
|
||||
<h3 class="text-sm font-bold text-gray-900 mb-3">Millesimi e Quote Acqua di Preventivo</h3>
|
||||
<div class="space-y-4">
|
||||
@foreach($tabelleAcqua as $tabella)
|
||||
<div class="rounded-2xl border border-cyan-100 bg-cyan-50/20 p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="inline-flex items-center rounded-full bg-cyan-50 px-2.5 py-0.5 text-xs font-semibold text-cyan-700">{{ $tabella['codice'] }} · {{ $tabella['nome'] }}</span>
|
||||
<div class="text-right">
|
||||
<div class="text-xs text-gray-500">Totale tabella</div>
|
||||
<div class="text-lg font-semibold text-gray-900">{{ number_format($tabella['total_preventivo'], 2, ',', '.') }} €</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto bg-white rounded-xl border border-cyan-100 p-2">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-gray-500">
|
||||
<th class="text-left py-1">Voce</th>
|
||||
<th class="text-right py-1">Preventivo</th>
|
||||
<th class="text-right py-1">Quota unità</th>
|
||||
<th class="text-right py-1">Prop.</th>
|
||||
<th class="text-right py-1">Inquilino</th>
|
||||
<th class="text-right py-1">% Prop.</th>
|
||||
<th class="text-right py-1">% Inq.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@foreach($tabella['voci'] as $voce)
|
||||
<tr>
|
||||
<td class="py-1 text-gray-800">{{ $voce['codice'] }} · {{ $voce['descrizione'] }}</td>
|
||||
<td class="py-1 text-right text-gray-900">{{ number_format($voce['importo_preventivato'] ?? 0, 2, ',', '.') }} €</td>
|
||||
<td class="py-1 text-right text-gray-900">{{ number_format($voce['quota_unita'] ?? 0, 2, ',', '.') }} €</td>
|
||||
<td class="py-1 text-right text-gray-900">{{ number_format($voce['importo_proprietario'] ?? 0, 2, ',', '.') }} €</td>
|
||||
<td class="py-1 text-right text-gray-900">{{ number_format($voce['importo_inquilino'] ?? 0, 2, ',', '.') }} €</td>
|
||||
<td class="py-1 text-right text-gray-700">{{ number_format(100.00 - ($voce['percentuale_inquilino'] ?? 0), 2, ',', '.') }} %</td>
|
||||
<td class="py-1 text-right text-gray-700">{{ number_format($voce['percentuale_inquilino'] ?? 0, 2, ',', '.') }} %</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($ripartiAcqua->isNotEmpty())
|
||||
<div class="mt-6">
|
||||
<h3 class="text-sm font-bold text-gray-900 mb-3">Millesimi e Quote Acqua di Consuntivo</h3>
|
||||
<div class="space-y-4">
|
||||
@foreach($ripartiAcqua as $tabella)
|
||||
<div class="rounded-2xl border border-cyan-100 bg-cyan-50/20 overflow-hidden">
|
||||
<div class="flex items-center gap-2 bg-white px-3 py-2 border-b border-cyan-100">
|
||||
<span class="inline-flex items-center rounded-full bg-cyan-50 px-2 py-0.5 text-xs font-semibold text-cyan-700">{{ $tabella['tabella_codice'] }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900">{{ $tabella['tabella_nome'] }}</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-gray-500 border-b">
|
||||
<th class="text-left py-2 px-2">Voce</th>
|
||||
<th class="text-center py-2 px-2">Gestione</th>
|
||||
<th class="text-center py-2 px-2">Calcolo</th>
|
||||
<th class="text-right py-2 px-2">% applicata</th>
|
||||
<th class="text-right py-2 px-2">Quota finale</th>
|
||||
<th class="text-right py-2 px-2">% prop</th>
|
||||
<th class="text-right py-2 px-2">% inquilino</th>
|
||||
<th class="text-right py-2 px-2">Prop.</th>
|
||||
<th class="text-right py-2 px-2">Inq.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@foreach($tabella['righe'] as $r)
|
||||
<tr>
|
||||
<td class="py-2 px-2 text-gray-900 font-semibold">{{ $r['voce_codice'] ?? '—' }} {{ $r['voce_descrizione'] ?? '' }}</td>
|
||||
<td class="py-2 px-2 text-center text-gray-700">{{ $r['tipo_gestione'] ?? '—' }}</td>
|
||||
<td class="py-2 px-2 text-center text-gray-700">{{ $r['calcolo'] ?? '—' }}</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['percentuale_applicata'] ?? 0, 2, ',', '.') }}%</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['quota_finale'] ?? 0, 4, ',', '.') }}</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format(100.00 - ($r['percentuale_inquilino'] ?? 0), 2, ',', '.') }}%</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['percentuale_inquilino'] ?? 0, 2, ',', '.') }}%</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['importo_proprietario'] ?? 0, 4, ',', '.') }}</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['importo_inquilino'] ?? 0, 4, ',', '.') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@if($tab === 'economico')
|
||||
<div class="grid gap-4">
|
||||
{{-- Contenuti economici: preventivi/riparti (rate emesse spostate nel tab Estratto conto) --}}
|
||||
|
|
@ -701,7 +973,9 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
|
||||
<div class="space-y-4">
|
||||
@foreach($preventiviPerTabella as $tabella)
|
||||
@if(str_starts_with(strtoupper($tabella['codice'] ?? ''), 'AC')) @continue @endif
|
||||
<div class="rounded-2xl border bg-gray-50 p-4">
|
||||
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<div class="text-xs text-gray-600">{{ $tabella['tabella_codice'] }} · tipo {{ $tabella['tipo'] }}</div>
|
||||
|
|
@ -722,9 +996,11 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
<th class="text-right py-1">Quota unità</th>
|
||||
<th class="text-right py-1">Prop.</th>
|
||||
<th class="text-right py-1">Inquilino</th>
|
||||
<th class="text-right py-1">% Prop.</th>
|
||||
<th class="text-right py-1">% Inq.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y">
|
||||
@foreach($tabella['voci'] as $voce)
|
||||
<tr>
|
||||
|
|
@ -771,7 +1047,9 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($ripartizioniPerTabella as $tabella)
|
||||
@if(str_starts_with(strtoupper($tabella['tabella_codice'] ?? ''), 'AC')) @continue @endif
|
||||
<div class="rounded-2xl border bg-gray-50 overflow-hidden">
|
||||
|
||||
<div class="flex items-center gap-2 bg-white px-3 py-2 border-b">
|
||||
<span class="inline-flex items-center rounded-full border border-primary-200 bg-primary-50 px-2 py-0.5 text-xs font-semibold text-primary-700">{{ $tabella['tabella_codice'] }}</span>
|
||||
<span class="text-sm font-semibold text-gray-900">{{ $tabella['tabella_nome'] }}</span>
|
||||
|
|
@ -785,11 +1063,13 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
<th class="text-center py-2 px-2">Calcolo</th>
|
||||
<th class="text-right py-2 px-2">% applicata</th>
|
||||
<th class="text-right py-2 px-2">Quota finale</th>
|
||||
<th class="text-right py-2 px-2">% prop</th>
|
||||
<th class="text-right py-2 px-2">% inquilino</th>
|
||||
<th class="text-right py-2 px-2">Prop.</th>
|
||||
<th class="text-right py-2 px-2">Inq.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y">
|
||||
@foreach($tabella['righe'] as $r)
|
||||
<tr>
|
||||
|
|
@ -798,10 +1078,12 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
<td class="py-2 px-2 text-center text-gray-700">{{ $r['calcolo'] ?? '—' }}</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['percentuale_applicata'] ?? 0, 2, ',', '.') }}%</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['quota_finale'] ?? 0, 4, ',', '.') }}</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format(100.00 - ($r['percentuale_inquilino'] ?? 0), 2, ',', '.') }}%</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['percentuale_inquilino'] ?? 0, 2, ',', '.') }}%</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['importo_proprietario'] ?? 0, 4, ',', '.') }}</td>
|
||||
<td class="py-2 px-2 text-right text-gray-900">{{ number_format($r['importo_inquilino'] ?? 0, 4, ',', '.') }}</td>
|
||||
</tr>
|
||||
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -809,19 +1091,10 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($tab === 'estratto')
|
||||
@php
|
||||
$rateCondomini = $rateEmessePerCategoria['condomini'] ?? [];
|
||||
$rateInquilini = $rateEmessePerCategoria['inquilini'] ?? [];
|
||||
$hasRate = (! empty($rateCondomini)) || (! empty($rateInquilini));
|
||||
|
||||
$compactRateRows = $estrattoCompattoRateRows ?? [];
|
||||
$compactIncassi = $estrattoCompattoIncassi ?? [];
|
||||
|
||||
$sumBlk = function (array $items, string $field): float {
|
||||
$s = 0.0;
|
||||
|
|
@ -839,326 +1112,255 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
$totIPag = $sumBlk($rateInquilini, 'totale_pagato');
|
||||
$totIRes = $sumBlk($rateInquilini, 'residuo');
|
||||
|
||||
$tipoSel = $estrattoTipo ?? 'condomini';
|
||||
$itemsSel = $tipoSel === 'inquilini' ? $rateInquilini : $rateCondomini;
|
||||
|
||||
$selectedRole = $tipoSel === 'inquilini' ? 'I' : 'C';
|
||||
$compactRateRows = array_values(array_filter($compactRateRows, function (array $row) use ($selectedRole, $tipoSel): bool {
|
||||
$tipo = strtoupper(trim((string) ($row['tipo'] ?? '')));
|
||||
if ($tipo === '') {
|
||||
return $tipoSel === 'condomini';
|
||||
}
|
||||
return $tipo === $selectedRole;
|
||||
}));
|
||||
$compactIncassi = array_values(array_filter($compactIncassi, function (array $row) use ($selectedRole, $tipoSel): bool {
|
||||
$tipo = strtoupper(trim((string) ($row['tipo'] ?? '')));
|
||||
if ($tipo === '') {
|
||||
return $tipoSel === 'condomini';
|
||||
}
|
||||
return $tipo === $selectedRole;
|
||||
}));
|
||||
|
||||
if (! $hasRate) {
|
||||
$totCAddeb = (float) collect($estrattoCompattoRateRows ?? [])->filter(fn (array $row): bool => strtoupper((string) ($row['tipo'] ?? 'C')) !== 'I')->sum('dovuto');
|
||||
$totCPag = (float) collect($estrattoCompattoIncassi ?? [])->filter(fn (array $row): bool => strtoupper((string) ($row['tipo'] ?? 'C')) !== 'I')->sum('importo');
|
||||
if (empty($rateCondomini) && empty($rateInquilini)) {
|
||||
$totCAddeb = (float) collect($estrattoRateRowsProprietario ?? [])->sum('dovuto');
|
||||
$totCPag = (float) collect($estrattoIncassiProprietario ?? [])->sum('importo');
|
||||
$totCRes = $totCAddeb - $totCPag;
|
||||
$totIAddeb = (float) collect($estrattoCompattoRateRows ?? [])->filter(fn (array $row): bool => strtoupper((string) ($row['tipo'] ?? '')) === 'I')->sum('dovuto');
|
||||
$totIPag = (float) collect($estrattoCompattoIncassi ?? [])->filter(fn (array $row): bool => strtoupper((string) ($row['tipo'] ?? '')) === 'I')->sum('importo');
|
||||
|
||||
$totIAddeb = (float) collect($estrattoRateRowsInquilino ?? [])->sum('dovuto');
|
||||
$totIPag = (float) collect($estrattoIncassiInquilino ?? [])->sum('importo');
|
||||
$totIRes = $totIAddeb - $totIPag;
|
||||
}
|
||||
@endphp
|
||||
|
||||
<x-filament::section>
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-gray-900 mb-3">
|
||||
<x-filament::icon icon="heroicon-o-document-text" class="h-5 w-5 text-primary-600" />
|
||||
<span>Estratto conto unità</span>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<!-- SEZIONE PROPRIETARIO (C) -->
|
||||
<x-filament::section>
|
||||
<div class="flex items-center justify-between border-b pb-2 mb-4">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-gray-900">
|
||||
<x-filament::icon icon="heroicon-o-user" class="h-5 w-5 text-primary-600" />
|
||||
<span>Estratto Conto PROPRIETARIO (Condòmino - C)</span>
|
||||
</div>
|
||||
<div class="text-right text-xs">
|
||||
<span class="font-medium text-gray-500">Addebito:</span> <span class="font-bold text-gray-900">{{ number_format($totCAddeb, 2, ',', '.') }} €</span> ·
|
||||
<span class="font-medium text-gray-500">Pagato:</span> <span class="font-bold text-emerald-600">{{ number_format($totCPag, 2, ',', '.') }} €</span> ·
|
||||
<span class="font-medium text-gray-500">Residuo:</span> <span class="font-bold text-amber-600">{{ number_format($totCRes, 2, ',', '.') }} €</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border bg-white p-3 mb-4">
|
||||
<div class="text-sm font-semibold text-gray-900 mb-2">Situazione rate & incassi</div>
|
||||
@php
|
||||
$movimenti = [];
|
||||
$seenRateKeys = [];
|
||||
foreach ($compactRateRows as $r) {
|
||||
$rateKey = implode('|', [
|
||||
(string) ($r['gestione'] ?? ''),
|
||||
(string) ($r['tipo'] ?? ''),
|
||||
(string) ($r['data_emissione'] ?? ''),
|
||||
(string) ($r['descrizione'] ?? ''),
|
||||
(string) ($r['avviso'] ?? ''),
|
||||
number_format((float) ($r['dovuto'] ?? 0), 2, '.', ''),
|
||||
number_format((float) ($r['pagato'] ?? 0), 2, '.', ''),
|
||||
]);
|
||||
if (isset($seenRateKeys[$rateKey])) {
|
||||
continue;
|
||||
}
|
||||
$seenRateKeys[$rateKey] = true;
|
||||
|
||||
$dovuto = (float) ($r['dovuto'] ?? 0);
|
||||
$pagato = (float) ($r['pagato'] ?? 0);
|
||||
$residuo = (float) ($r['residuo'] ?? ($dovuto - $pagato));
|
||||
if (abs($dovuto) < 0.00001 && abs($pagato) < 0.00001 && abs($residuo) < 0.00001) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$movimenti[] = [
|
||||
$movimentiC = [];
|
||||
$seenC = [];
|
||||
foreach ($estrattoRateRowsProprietario ?? [] as $r) {
|
||||
$k = ($r['gestione']??'').'|'.($r['data_emissione']??'').'|'.($r['descrizione']??'').'|'.number_format((float)($r['dovuto']??0), 2, '.', '');
|
||||
if(isset($seenC[$k])) continue;
|
||||
$seenC[$k] = true;
|
||||
$movimentiC[] = [
|
||||
'tipo' => 'rata',
|
||||
'soggetto_tipo' => $r['tipo'] ?? null,
|
||||
'gestione' => $r['gestione'] ?? null,
|
||||
'data' => $r['data_emissione'] ?? null,
|
||||
'data_pagamento' => $r['data_pagamento'] ?? null,
|
||||
'descrizione' => $r['descrizione'] ?? null,
|
||||
'ref' => !empty($r['numero_ricevuta']) ? ('Ric. ' . $r['numero_ricevuta']) : (!empty($r['avviso']) ? ('Avv. ' . $r['avviso']) : null),
|
||||
'dovuto' => $dovuto,
|
||||
'residuo' => $residuo,
|
||||
'incasso' => $pagato,
|
||||
'descrizione' => $r['descrizione'] ?? '',
|
||||
'ref' => !empty($r['avviso']) ? ('Avv. ' . $r['avviso']) : '',
|
||||
'dovuto' => (float)$r['dovuto'],
|
||||
'incasso' => (float)$r['pagato'],
|
||||
'residuo' => (float)$r['residuo'],
|
||||
'gestione' => $r['gestione'] ?? 'Ordinaria',
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($movimenti)) {
|
||||
$seenIncassoKeys = [];
|
||||
foreach ($compactIncassi as $i) {
|
||||
$incasso = (float) ($i['importo'] ?? 0);
|
||||
if (abs($incasso) < 0.00001) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$incassoKey = implode('|', [
|
||||
(string) ($i['tipo'] ?? ''),
|
||||
(string) ($i['data'] ?? ''),
|
||||
(string) ($i['descrizione'] ?? ''),
|
||||
(string) ($i['n_ricevuta'] ?? ''),
|
||||
number_format($incasso, 2, '.', ''),
|
||||
]);
|
||||
if (isset($seenIncassoKeys[$incassoKey])) {
|
||||
continue;
|
||||
}
|
||||
$seenIncassoKeys[$incassoKey] = true;
|
||||
|
||||
$movimenti[] = [
|
||||
'tipo' => 'incasso',
|
||||
'soggetto_tipo' => $i['tipo'] ?? null,
|
||||
'gestione' => $i['gestione'] ?? null,
|
||||
'data' => $i['data'] ?? null,
|
||||
'data_pagamento' => $i['data'] ?? null,
|
||||
'descrizione' => $i['descrizione'] ?? null,
|
||||
'ref' => !empty($i['n_ricevuta']) ? ('Ric. ' . $i['n_ricevuta']) : null,
|
||||
'dovuto' => null,
|
||||
'residuo' => null,
|
||||
'incasso' => $incasso,
|
||||
];
|
||||
}
|
||||
foreach ($estrattoIncassiProprietario ?? [] as $i) {
|
||||
$k = ($i['data']??'').'|'.($i['descrizione']??'').'|'.number_format((float)($i['importo']??0), 2, '.', '');
|
||||
if(isset($seenC[$k])) continue;
|
||||
$seenC[$k] = true;
|
||||
$movimentiC[] = [
|
||||
'tipo' => 'incasso',
|
||||
'data' => $i['data'] ?? null,
|
||||
'descrizione' => $i['descrizione'] ?? '',
|
||||
'ref' => !empty($i['n_ricevuta']) ? ('Ric. ' . $i['n_ricevuta']) : '',
|
||||
'dovuto' => null,
|
||||
'incasso' => (float)$i['importo'],
|
||||
'residuo' => null,
|
||||
'gestione' => $i['gestione'] ?? 'Ordinaria',
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($estrattoConguagliIniziali ?? [] as $c) {
|
||||
$importoConguaglio = (float) ($c['importo'] ?? 0);
|
||||
if (abs($importoConguaglio) < 0.00001) {
|
||||
continue;
|
||||
}
|
||||
$movimenti[] = [
|
||||
$importo = (float)($c['importo'] ?? 0);
|
||||
if(abs($importo) < 0.0001) continue;
|
||||
$movimentiC[] = [
|
||||
'tipo' => 'conguaglio',
|
||||
'soggetto_tipo' => $selectedRole,
|
||||
'gestione' => $c['gestione_label'] ?? null,
|
||||
'data' => $c['data'] ?? null,
|
||||
'data_pagamento' => null,
|
||||
'descrizione' => $c['descrizione'] ?? null,
|
||||
'ref' => trim((string) (($c['gestione_label'] ?? '') . ' ' . ($c['tipo'] ?? ''))),
|
||||
'dovuto' => $importoConguaglio,
|
||||
'residuo' => $importoConguaglio,
|
||||
'descrizione' => $c['descrizione'] ?? '',
|
||||
'ref' => trim(($c['gestione_label'] ?? '') . ' ' . ($c['tipo'] ?? '')),
|
||||
'dovuto' => $importo,
|
||||
'incasso' => null,
|
||||
'residuo' => $importo,
|
||||
'gestione' => $c['gestione_label'] ?? 'Ordinaria',
|
||||
];
|
||||
}
|
||||
usort($movimenti, function ($a, $b) {
|
||||
usort($movimentiC, function($a, $b){
|
||||
$da = $a['data'] ? \Carbon\Carbon::createFromFormat('d/m/Y', $a['data'])->timestamp : 0;
|
||||
$db = $b['data'] ? \Carbon\Carbon::createFromFormat('d/m/Y', $b['data'])->timestamp : 0;
|
||||
return $da <=> $db;
|
||||
});
|
||||
|
||||
$movimentiByGestione = [];
|
||||
foreach ($movimenti as $m) {
|
||||
$gestioneLabel = trim((string) ($m['gestione'] ?? ''));
|
||||
$anno = null;
|
||||
if ($gestioneLabel === '' && ! empty($m['data'])) {
|
||||
try {
|
||||
$anno = \Carbon\Carbon::createFromFormat('d/m/Y', $m['data'])->format('Y');
|
||||
} catch (\Throwable $e) {
|
||||
$anno = null;
|
||||
}
|
||||
}
|
||||
$key = $gestioneLabel !== '' ? $gestioneLabel : ($anno ?: 'Senza data');
|
||||
$movimentiByGestione[$key][] = $m;
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if(empty($movimenti))
|
||||
<div class="text-sm text-gray-500">Nessuna rata o incasso disponibile.</div>
|
||||
@if(empty($movimentiC))
|
||||
<div class="text-xs text-gray-500 py-2">Nessuna rata o incasso registrato per il proprietario.</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-x-auto rounded-xl border">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead>
|
||||
<tr class="text-gray-500 border-b">
|
||||
<th class="text-left py-2 pr-3">Data</th>
|
||||
<th class="text-left py-2 pr-3">Tipo</th>
|
||||
<th class="text-left py-2 pr-3">Descrizione</th>
|
||||
<th class="text-left py-2 pr-3">Rif.</th>
|
||||
<th class="text-right py-2 pr-3">Dovuto</th>
|
||||
<th class="text-right py-2 pr-3">Pagato</th>
|
||||
<th class="text-right py-2 pr-0">Residuo</th>
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="text-left py-2 px-3 text-gray-600">Data</th>
|
||||
<th class="text-left py-2 px-3 text-gray-600">Tipo</th>
|
||||
<th class="text-left py-2 px-3 text-gray-600">Descrizione</th>
|
||||
<th class="text-left py-2 px-3 text-gray-600">Rif.</th>
|
||||
<th class="text-right py-2 px-3 text-gray-600">Dovuto</th>
|
||||
<th class="text-right py-2 px-3 text-gray-600">Pagato</th>
|
||||
<th class="text-right py-2 px-3 text-gray-600">Residuo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@php
|
||||
$totDovutoAll = 0.0;
|
||||
$totIncassoAll = 0.0;
|
||||
@endphp
|
||||
@foreach($movimentiByGestione as $gestione => $rows)
|
||||
<tr class="bg-gray-50">
|
||||
<td class="py-2 pr-3 text-gray-700 font-semibold" colspan="7">Gestione {{ $gestione }}</td>
|
||||
</tr>
|
||||
@php
|
||||
$totDovuto = 0.0;
|
||||
$totIncasso = 0.0;
|
||||
@endphp
|
||||
@foreach($rows as $m)
|
||||
@php
|
||||
$totDovuto += (float) ($m['dovuto'] ?? 0);
|
||||
$totIncasso += (float) ($m['incasso'] ?? 0);
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="py-2 pr-3 text-gray-700">{{ $m['data'] ?? '—' }}</td>
|
||||
<td class="py-2 pr-3 text-gray-700">
|
||||
{{ $m['tipo'] === 'rata' ? 'Rata' : ($m['tipo'] === 'conguaglio' ? 'Conguaglio' : 'Incasso') }}
|
||||
</td>
|
||||
<td class="py-2 pr-3 text-gray-900">
|
||||
<div>{{ $m['descrizione'] ?? '—' }}</div>
|
||||
@if(!empty($m['data_pagamento']) && $m['tipo'] !== 'incasso')
|
||||
<div class="text-[11px] text-gray-500">Pagato il {{ $m['data_pagamento'] }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-2 pr-3 text-gray-700">{{ $m['ref'] !== '' ? $m['ref'] : '—' }}</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-900 tabular-nums">
|
||||
{{ $m['dovuto'] !== null ? number_format($m['dovuto'], 2, ',', '.') . ' €' : '—' }}
|
||||
</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-900 tabular-nums">
|
||||
{{ $m['incasso'] !== null ? number_format($m['incasso'], 2, ',', '.') . ' €' : '—' }}
|
||||
</td>
|
||||
<td class="py-2 pr-0 text-right text-gray-900 tabular-nums">
|
||||
{{ $m['residuo'] !== null ? number_format($m['residuo'], 2, ',', '.') . ' €' : '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@php
|
||||
$totDovutoAll += $totDovuto;
|
||||
$totIncassoAll += $totIncasso;
|
||||
$saldo = $totDovuto - $totIncasso;
|
||||
@endphp
|
||||
<tr class="bg-gray-50">
|
||||
<td class="py-2 pr-3 text-gray-700 font-semibold" colspan="4">Subtotale gestione</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-900 font-semibold tabular-nums">{{ number_format($totDovuto, 2, ',', '.') }} €</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-900 font-semibold tabular-nums">{{ number_format($totIncasso, 2, ',', '.') }} €</td>
|
||||
<td class="py-2 pr-0 text-right text-gray-900 font-semibold tabular-nums">Saldo {{ number_format($saldo, 2, ',', '.') }} €</td>
|
||||
@foreach($movimentiC as $m)
|
||||
<tr>
|
||||
<td class="py-2 px-3">{{ $m['data'] ?? '—' }}</td>
|
||||
<td class="py-2 px-3 font-semibold">{{ $m['tipo'] === 'rata' ? 'Rata' : ($m['tipo'] === 'conguaglio' ? 'Conguaglio' : 'Incasso') }}</td>
|
||||
<td class="py-2 px-3 text-gray-900">{{ $m['descrizione'] }}</td>
|
||||
<td class="py-2 px-3 text-gray-500">{{ $m['ref'] ?: '—' }}</td>
|
||||
<td class="py-2 px-3 text-right tabular-nums">{{ $m['dovuto'] !== null ? number_format($m['dovuto'], 2, ',', '.') . ' €' : '—' }}</td>
|
||||
<td class="py-2 px-3 text-right tabular-nums">{{ $m['incasso'] !== null ? number_format($m['incasso'], 2, ',', '.') . ' €' : '—' }}</td>
|
||||
<td class="py-2 px-3 text-right tabular-nums font-semibold">{{ $m['residuo'] !== null ? number_format($m['residuo'], 2, ',', '.') . ' €' : '—' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@php
|
||||
$saldoAll = $totDovutoAll - $totIncassoAll;
|
||||
@endphp
|
||||
<tr class="bg-primary-50">
|
||||
<td class="py-2 pr-3 text-primary-700 font-semibold" colspan="4">Totale generale</td>
|
||||
<td class="py-2 pr-3 text-right text-primary-700 font-semibold tabular-nums">{{ number_format($totDovutoAll, 2, ',', '.') }} €</td>
|
||||
<td class="py-2 pr-3 text-right text-primary-700 font-semibold tabular-nums">{{ number_format($totIncassoAll, 2, ',', '.') }} €</td>
|
||||
<td class="py-2 pr-0 text-right text-primary-700 font-semibold tabular-nums">Saldo {{ number_format($saldoAll, 2, ',', '.') }} €</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if(! $hasRate)
|
||||
<div class="text-sm text-gray-500">Nessuna rata emessa trovata per questa unità.</div>
|
||||
@else
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<button type="button"
|
||||
wire:click="setEstrattoTipo('condomini')"
|
||||
class="rounded-2xl border p-4 text-left transition {{ $tipoSel === 'condomini' ? 'border-primary-300 bg-primary-50' : 'bg-gray-50 hover:border-gray-200' }}">
|
||||
<div class="text-xs text-gray-500">CONDOMINO</div>
|
||||
<div class="text-sm font-semibold text-gray-900">Addebito {{ number_format($totCAddeb, 2, ',', '.') }} €</div>
|
||||
<div class="text-xs text-gray-600">Pagato {{ number_format($totCPag, 2, ',', '.') }} € · Residuo {{ number_format($totCRes, 2, ',', '.') }} €</div>
|
||||
</button>
|
||||
@if(!empty($rateCondomini))
|
||||
<div class="mt-4 border-t pt-3">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-2">Schede anagrafiche collegate:</div>
|
||||
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach($rateCondomini as $rc)
|
||||
@php
|
||||
$sid = (int) ($rc['soggetto_id'] ?? 0);
|
||||
$url = $sid > 0 ? \App\Filament\Pages\Contabilita\EstrattoContoSoggetto::getUrl(panel: 'admin-filament', parameters: ['record' => $sid]) . '?' . http_build_query(['vista' => 'unita', 'unita_id' => (int) ($this->unita?->id ?? 0)]) : null;
|
||||
@endphp
|
||||
<div class="p-2 border rounded-lg bg-gray-50/50 flex justify-between items-center text-xs">
|
||||
<div>
|
||||
<div class="font-bold text-gray-900">{{ $rc['nome'] }}</div>
|
||||
<div class="text-[10px] text-gray-500">Addebito: {{ number_format($rc['totale_addebitato'], 2, ',', '.') }} €</div>
|
||||
</div>
|
||||
@if($url)
|
||||
<a href="{{ $url }}" class="px-2 py-1 bg-white hover:bg-gray-100 border rounded text-[10px] font-semibold">Apri scheda</a>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
|
||||
<button type="button"
|
||||
wire:click="setEstrattoTipo('inquilini')"
|
||||
class="rounded-2xl border p-4 text-left transition {{ $tipoSel === 'inquilini' ? 'border-primary-300 bg-primary-50' : 'bg-gray-50 hover:border-gray-200' }}">
|
||||
<div class="text-xs text-gray-500">INQUILINO</div>
|
||||
<div class="text-sm font-semibold text-gray-900">Addebito {{ number_format($totIAddeb, 2, ',', '.') }} €</div>
|
||||
<div class="text-xs text-gray-600">Pagato {{ number_format($totIPag, 2, ',', '.') }} € · Residuo {{ number_format($totIRes, 2, ',', '.') }} €</div>
|
||||
</button>
|
||||
<!-- SEZIONE INQUILINO (I) -->
|
||||
<x-filament::section>
|
||||
<div class="flex items-center justify-between border-b pb-2 mb-4">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-gray-900">
|
||||
<x-filament::icon icon="heroicon-o-users" class="h-5 w-5 text-emerald-600" />
|
||||
<span>Estratto Conto CONDUTTORE (Inquilino - I)</span>
|
||||
</div>
|
||||
<div class="text-right text-xs">
|
||||
<span class="font-medium text-gray-500">Addebito:</span> <span class="font-bold text-gray-900">{{ number_format($totIAddeb, 2, ',', '.') }} €</span> ·
|
||||
<span class="font-medium text-gray-500">Pagato:</span> <span class="font-bold text-emerald-600">{{ number_format($totIPag, 2, ',', '.') }} €</span> ·
|
||||
<span class="font-medium text-gray-500">Residuo:</span> <span class="font-bold text-amber-600">{{ number_format($totIRes, 2, ',', '.') }} €</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-2xl border bg-gray-50 p-3">
|
||||
<div class="mb-2 text-sm font-semibold text-gray-900">
|
||||
{{ $tipoSel === 'inquilini' ? 'Inquilino / altro' : 'Condomino (proprietari)' }} ({{ count($itemsSel) }})
|
||||
</div>
|
||||
@php
|
||||
$movimentiI = [];
|
||||
$seenI = [];
|
||||
foreach ($estrattoRateRowsInquilino ?? [] as $r) {
|
||||
$k = ($r['gestione']??'').'|'.($r['data_emissione']??'').'|'.($r['descrizione']??'').'|'.number_format((float)($r['dovuto']??0), 2, '.', '');
|
||||
if(isset($seenI[$k])) continue;
|
||||
$seenI[$k] = true;
|
||||
$movimentiI[] = [
|
||||
'tipo' => 'rata',
|
||||
'data' => $r['data_emissione'] ?? null,
|
||||
'descrizione' => $r['descrizione'] ?? '',
|
||||
'ref' => !empty($r['avviso']) ? ('Avv. ' . $r['avviso']) : '',
|
||||
'dovuto' => (float)$r['dovuto'],
|
||||
'incasso' => (float)$r['pagato'],
|
||||
'residuo' => (float)$r['residuo'],
|
||||
'gestione' => $r['gestione'] ?? 'Ordinaria',
|
||||
];
|
||||
}
|
||||
foreach ($estrattoIncassiInquilino ?? [] as $i) {
|
||||
$k = ($i['data']??'').'|'.($i['descrizione']??'').'|'.number_format((float)($i['importo']??0), 2, '.', '');
|
||||
if(isset($seenI[$k])) continue;
|
||||
$seenI[$k] = true;
|
||||
$movimentiI[] = [
|
||||
'tipo' => 'incasso',
|
||||
'data' => $i['data'] ?? null,
|
||||
'descrizione' => $i['descrizione'] ?? '',
|
||||
'ref' => !empty($i['n_ricevuta']) ? ('Ric. ' . $i['n_ricevuta']) : '',
|
||||
'dovuto' => null,
|
||||
'incasso' => (float)$i['importo'],
|
||||
'residuo' => null,
|
||||
'gestione' => $i['gestione'] ?? 'Ordinaria',
|
||||
];
|
||||
}
|
||||
usort($movimentiI, function($a, $b){
|
||||
$da = $a['data'] ? \Carbon\Carbon::createFromFormat('d/m/Y', $a['data'])->timestamp : 0;
|
||||
$db = $b['data'] ? \Carbon\Carbon::createFromFormat('d/m/Y', $b['data'])->timestamp : 0;
|
||||
return $da <=> $db;
|
||||
});
|
||||
@endphp
|
||||
|
||||
@if(empty($itemsSel))
|
||||
<div class="text-sm text-gray-500">Nessuna rata per questa categoria.</div>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-gray-500 border-b">
|
||||
<th class="text-left py-2 pr-3">Soggetto</th>
|
||||
<th class="text-right py-2 pr-3">N.</th>
|
||||
<th class="text-right py-2 pr-3">Addebito</th>
|
||||
<th class="text-right py-2 pr-3">Pagato</th>
|
||||
<th class="text-right py-2 pr-3">Residuo</th>
|
||||
<th class="text-right py-2 pr-3">Scadute</th>
|
||||
<th class="text-right py-2 pr-0">Scheda</th>
|
||||
@if(empty($movimentiI))
|
||||
<div class="text-xs text-gray-500 py-2">Nessuna rata o incasso registrato per l'inquilino.</div>
|
||||
@else
|
||||
<div class="overflow-x-auto rounded-xl border">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="text-left py-2 px-3 text-gray-600">Data</th>
|
||||
<th class="text-left py-2 px-3 text-gray-600">Tipo</th>
|
||||
<th class="text-left py-2 px-3 text-gray-600">Descrizione</th>
|
||||
<th class="text-left py-2 px-3 text-gray-600">Rif.</th>
|
||||
<th class="text-right py-2 px-3 text-gray-600">Dovuto</th>
|
||||
<th class="text-right py-2 px-3 text-gray-600">Pagato</th>
|
||||
<th class="text-right py-2 px-3 text-gray-600">Residuo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@foreach($movimentiI as $m)
|
||||
<tr>
|
||||
<td class="py-2 px-3">{{ $m['data'] ?? '—' }}</td>
|
||||
<td class="py-2 px-3 font-semibold">{{ $m['tipo'] === 'rata' ? 'Rata' : 'Incasso' }}</td>
|
||||
<td class="py-2 px-3 text-gray-900">{{ $m['descrizione'] }}</td>
|
||||
<td class="py-2 px-3 text-gray-500">{{ $m['ref'] ?: '—' }}</td>
|
||||
<td class="py-2 px-3 text-right tabular-nums">{{ $m['dovuto'] !== null ? number_format($m['dovuto'], 2, ',', '.') . ' €' : '—' }}</td>
|
||||
<td class="py-2 px-3 text-right tabular-nums">{{ $m['incasso'] !== null ? number_format($m['incasso'], 2, ',', '.') . ' €' : '—' }}</td>
|
||||
<td class="py-2 px-3 text-right tabular-nums font-semibold">{{ $m['residuo'] !== null ? number_format($m['residuo'], 2, ',', '.') . ' €' : '—' }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@foreach($itemsSel as $r)
|
||||
@php
|
||||
$sid = (int) ($r['soggetto_id'] ?? 0);
|
||||
$schedaUrl = null;
|
||||
if ($sid > 0) {
|
||||
$base = \App\Filament\Pages\Contabilita\EstrattoContoSoggetto::getUrl(panel: 'admin-filament', parameters: ['record' => $sid]);
|
||||
$schedaUrl = $base . '?' . http_build_query([
|
||||
'vista' => 'unita',
|
||||
'unita_id' => (int) ($this->unita?->id ?? 0),
|
||||
]);
|
||||
}
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="py-2 pr-3">
|
||||
<div class="font-semibold text-gray-900">{{ $r['nome'] ?? '—' }}</div>
|
||||
<div class="text-xs text-gray-500">CF: {{ $r['codice_fiscale'] ?? '—' }}</div>
|
||||
</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-900">{{ $r['numero_rate'] ?? 0 }}</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-900">{{ number_format((float) ($r['totale_addebitato'] ?? 0), 2, ',', '.') }} €</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-900">{{ number_format((float) ($r['totale_pagato'] ?? 0), 2, ',', '.') }} €</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-900">{{ number_format((float) ($r['residuo'] ?? 0), 2, ',', '.') }} €</td>
|
||||
<td class="py-2 pr-3 text-right text-gray-900">{{ $r['scadute'] ?? 0 }}</td>
|
||||
<td class="py-2 pr-0 text-right">
|
||||
@if($schedaUrl)
|
||||
<a class="fi-btn fi-btn-color-gray fi-btn-size-xs" href="{{ $schedaUrl }}">
|
||||
<span class="fi-btn-label">Apri</span>
|
||||
</a>
|
||||
@else
|
||||
<span class="text-xs text-gray-400">—</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-3">
|
||||
<a class="fi-btn fi-btn-color-gray fi-btn-size-sm" href="{{ \App\Filament\Pages\Contabilita\RateEmesseArchivio::getUrl(panel: 'admin-filament') }}">
|
||||
<span class="fi-btn-label">Apri archivio Rate emesse</span>
|
||||
</a>
|
||||
@if(!empty($rateInquilini))
|
||||
<div class="mt-4 border-t pt-3">
|
||||
<div class="text-xs font-semibold text-gray-700 mb-2">Schede anagrafiche collegate:</div>
|
||||
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach($rateInquilini as $ri)
|
||||
@php
|
||||
$sid = (int) ($ri['soggetto_id'] ?? 0);
|
||||
$url = $sid > 0 ? \App\Filament\Pages\Contabilita\EstrattoContoSoggetto::getUrl(panel: 'admin-filament', parameters: ['record' => $sid]) . '?' . http_build_query(['vista' => 'unita', 'unita_id' => (int) ($this->unita?->id ?? 0)]) : null;
|
||||
@endphp
|
||||
<div class="p-2 border rounded-lg bg-gray-50/50 flex justify-between items-center text-xs">
|
||||
<div>
|
||||
<div class="font-bold text-gray-900">{{ $ri['nome'] }}</div>
|
||||
<div class="text-[10px] text-gray-500">Addebito: {{ number_format($ri['totale_addebitato'], 2, ',', '.') }} €</div>
|
||||
</div>
|
||||
@if($url)
|
||||
<a href="{{ $url }}" class="px-2 py-1 bg-white hover:bg-gray-100 border rounded text-[10px] font-semibold">Apri scheda</a>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
</div>
|
||||
@endif </a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -1,39 +1,35 @@
|
|||
<div wire:poll.3s="refreshLiveIncomingCall" class="flex min-w-0 w-full items-stretch">
|
||||
@if($liveIncomingCall)
|
||||
<div class="flex w-full min-w-0 items-center justify-between gap-3 rounded-lg border border-emerald-300 bg-emerald-50 px-3 py-2 shadow-sm">
|
||||
<div class="flex w-full min-w-0 items-center justify-between gap-2 rounded-lg border border-emerald-300 bg-emerald-50 px-2 py-1 shadow-sm">
|
||||
<div class="min-w-0">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-emerald-800">Chiamata in arrivo</div>
|
||||
<div class="truncate text-sm font-semibold text-emerald-950">
|
||||
<div class="text-[9px] font-semibold uppercase tracking-wide text-emerald-800 leading-none">In arrivo</div>
|
||||
<div class="truncate text-xs font-semibold text-emerald-950 mt-0.5 leading-tight">
|
||||
{{ $liveIncomingCall['rubrica_nome'] ?: 'Numero non riconosciuto' }}
|
||||
</div>
|
||||
<div class="truncate text-xs text-emerald-800">
|
||||
<div class="truncate text-[10px] text-emerald-800 mt-0.5 leading-none">
|
||||
{{ $liveIncomingCall['phone'] ?? '-' }}
|
||||
@if(!empty($liveIncomingCall['target_extension']))
|
||||
· int. {{ $liveIncomingCall['target_extension'] }}
|
||||
@endif
|
||||
@if(!empty($liveIncomingCall['received_at']))
|
||||
· {{ $liveIncomingCall['received_at'] }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<button type="button" wire:click="createPostIt" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">
|
||||
Apri Post-it
|
||||
<div class="flex shrink-0 items-center">
|
||||
<button type="button" wire:click="createPostIt" class="inline-flex items-center rounded bg-emerald-700 px-2 py-1 text-[10px] font-medium text-white hover:bg-emerald-600">
|
||||
Post-it
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex w-full min-w-0 items-center justify-between gap-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 shadow-sm">
|
||||
<div class="flex w-full min-w-0 items-center justify-between gap-2 rounded-lg border border-slate-200 bg-slate-50 px-2 py-1 shadow-sm">
|
||||
<div class="min-w-0">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-700">CTI live</div>
|
||||
<div class="truncate text-sm font-medium text-slate-900">In attesa di chiamate recenti</div>
|
||||
<div class="hidden truncate text-xs text-slate-600 2xl:block">Il box operativo si apre automaticamente quando arriva una inbound valida.</div>
|
||||
<div class="text-[9px] font-semibold uppercase tracking-wide text-slate-700 leading-none">CTI live</div>
|
||||
<div class="truncate text-xs font-medium text-slate-900 mt-0.5 leading-tight">In attesa chiamate...</div>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0">
|
||||
<a href="{{ \App\Filament\Pages\Strumenti\PostItGestione::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 hover:bg-slate-100">
|
||||
Log chiamate
|
||||
<a href="{{ \App\Filament\Pages\Strumenti\PostItGestione::getUrl(panel: 'admin-filament') }}" class="inline-flex items-center rounded border border-slate-300 bg-white px-2 py-1 text-[10px] font-medium text-slate-700 hover:bg-slate-100">
|
||||
Log
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -71,13 +71,24 @@
|
|||
</div>
|
||||
</div>
|
||||
@if(!$convocazione->ricezione_confermata_at)
|
||||
<button onclick="confirmReceipt()" id="receipt-btn" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-semibold shadow-lg shadow-blue-500/10 active:scale-95 transition-all">
|
||||
<button onclick="openSignatureModal()" id="receipt-btn" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-semibold shadow-lg shadow-blue-500/10 active:scale-95 transition-all">
|
||||
Firma
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ZIP Download Card -->
|
||||
<div class="glass-card rounded-2xl p-5 shadow-xl space-y-3">
|
||||
<h3 class="text-sm font-bold text-slate-300 uppercase tracking-wider flex items-center">
|
||||
<i class="fas fa-file-archive mr-2 text-indigo-500"></i> Documenti Assembleari
|
||||
</h3>
|
||||
<p class="text-xs text-slate-400">Scarica la documentazione completa dell'assemblea (preventivi, bilanci, relazioni) in un unico file compresso.</p>
|
||||
<a href="{{ route('public.assemblea.zip', $convocazione->token_accesso) }}" class="w-full flex items-center justify-center space-x-2 py-3 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-bold transition-all active:scale-95 shadow-lg shadow-indigo-500/10">
|
||||
<i class="fas fa-download mr-1 text-sm"></i> Scarica Tutto (.ZIP)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Assembly Info Panel -->
|
||||
<div class="glass-card rounded-2xl p-5 shadow-xl space-y-4">
|
||||
<h3 class="text-sm font-bold text-slate-300 uppercase tracking-wider flex items-center"><i class="fas fa-calendar-day mr-2 text-blue-500"></i>Dettagli convocazione</h3>
|
||||
|
|
@ -187,10 +198,28 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Allegati del Punto -->
|
||||
@if($punto->allegati && count($punto->allegati) > 0)
|
||||
<div class="mt-2 pt-2 border-t border-slate-800/50 space-y-1.5 text-xxs">
|
||||
<p class="font-bold text-slate-400">Allegati:</p>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
@foreach($punto->allegati as $file)
|
||||
<a href="{{ $file['url'] }}" target="_blank" class="flex items-center space-x-1.5 text-blue-400 hover:underline">
|
||||
<i class="fas fa-file-pdf text-red-500"></i>
|
||||
<span>{{ $file['nome'] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-800/80 text-[10px]">
|
||||
@if($punto->articolo_legge)
|
||||
<span class="text-blue-400"><i class="fas fa-gavel mr-1"></i>Rif: {{ $punto->articolo_legge }}</span>
|
||||
@endif
|
||||
@if($punto->maggioranza_richiesta)
|
||||
<span class="text-amber-400"><i class="fas fa-balance-scale mr-1"></i>Maggioranza: {{ $punto->maggioranza_richiesta }}</span>
|
||||
@endif
|
||||
@if($punto->tabellaMillesimale)
|
||||
<span class="text-slate-400"><i class="fas fa-calculator mr-1"></i>Tab: {{ $punto->tabellaMillesimale->denominazione }}</span>
|
||||
@endif
|
||||
|
|
@ -200,6 +229,44 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Update Card -->
|
||||
<div class="glass-card rounded-2xl p-5 shadow-xl space-y-4">
|
||||
<h3 class="text-sm font-bold text-slate-300 uppercase tracking-wider flex items-center">
|
||||
<i class="fas fa-user-edit mr-2 text-blue-500"></i> Variazione Recapiti / Catasto
|
||||
</h3>
|
||||
<p class="text-xs text-slate-400">Verifica i tuoi recapiti di contatto. Invia eventuali rettifiche per l'aggiornamento dell'anagrafica condominiale.</p>
|
||||
|
||||
<form id="anagrafica-form" onsubmit="submitAnagrafica(event)" class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">Email di contatto</label>
|
||||
<input type="email" id="profile-email" value="{{ $convocazione->soggetto->email }}" class="w-full bg-slate-900/60 border border-slate-700 rounded-lg p-2.5 text-xs text-slate-100 focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">P.E.C. (Posta Elettronica Certificata)</label>
|
||||
<input type="email" id="profile-pec" value="{{ $convocazione->soggetto->pec }}" class="w-full bg-slate-900/60 border border-slate-700 rounded-lg p-2.5 text-xs text-slate-100 focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">Telefono / Cellulare</label>
|
||||
<input type="text" id="profile-telefono" value="{{ $convocazione->soggetto->telefono }}" class="w-full bg-slate-900/60 border border-slate-700 rounded-lg p-2.5 text-xs text-slate-100 focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] text-slate-400 uppercase font-bold mb-1">Indirizzo di Residenza</label>
|
||||
<input type="text" id="profile-indirizzo" value="{{ $convocazione->soggetto->indirizzo_residenza ?? $convocazione->soggetto->indirizzo }}" class="w-full bg-slate-900/60 border border-slate-700 rounded-lg p-2.5 text-xs text-slate-100 focus:outline-none focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- Cadastral data -->
|
||||
<div class="p-3 bg-slate-900/40 rounded-xl border border-slate-800 text-[10px] text-slate-400 space-y-1">
|
||||
<p class="font-bold text-slate-300">Dati Catastali Unità:</p>
|
||||
<p>Foglio: {{ $convocazione->unitaImmobiliare->foglio ?: '-' }} | Particella: {{ $convocazione->unitaImmobiliare->particella ?: '-' }} | Sub: {{ $convocazione->unitaImmobiliare->subalterno ?: '-' }}</p>
|
||||
<p class="text-[9px] text-indigo-400 italic">Presto potrai aggiornare automaticamente l'anagrafica acquistando una visura in tempo reale.</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-xl text-xs font-bold transition-all active:scale-95 shadow-md">
|
||||
Invia Richiesta Variazione
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Toast Notification -->
|
||||
|
|
@ -213,6 +280,41 @@
|
|||
NetGescon © 2026 - Piattaforma Gestione Assemblee Condominiali
|
||||
</footer>
|
||||
|
||||
<!-- Modal Firma Convocazione -->
|
||||
<div id="modal-signature" class="fixed inset-0 z-50 overflow-y-auto hidden bg-slate-950/80 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="bg-slate-900 border border-slate-800 rounded-2xl max-w-md w-full p-6 shadow-2xl space-y-4">
|
||||
<div class="flex justify-between items-center border-b border-slate-800 pb-2">
|
||||
<h3 class="text-sm font-bold text-white uppercase tracking-wider">Firma Convocazione</h3>
|
||||
<button onclick="closeSignatureModal()" class="text-slate-400 hover:text-white"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-slate-400 leading-relaxed">
|
||||
Disegna la tua firma all'interno del box sottostante per confermare la ricezione della convocazione. I dati verranno registrati con IP e marca temporale.
|
||||
</p>
|
||||
|
||||
<div class="relative w-full h-36 bg-slate-950 rounded-xl border border-slate-800 overflow-hidden">
|
||||
<canvas id="signature-canvas" class="w-full h-full cursor-crosshair"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="clearCanvas()" class="flex-1 py-2 border border-slate-800 hover:bg-slate-800 text-slate-400 rounded-xl text-xs font-semibold">
|
||||
Pulisci
|
||||
</button>
|
||||
<button onclick="saveSignature()" class="flex-1 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-xl text-xs font-bold shadow-md shadow-blue-500/10">
|
||||
Conferma Firma
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- CIE Mock Validation Box -->
|
||||
<div class="pt-3 border-t border-slate-800 space-y-2">
|
||||
<p class="text-[10px] text-slate-400 text-center font-semibold">Certificazione con Identità Digitale (Opzionale)</p>
|
||||
<button onclick="validateCie()" id="cie-btn" class="w-full flex items-center justify-center gap-2 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-bold transition-all shadow-md shadow-indigo-500/10">
|
||||
<i class="fas fa-id-card"></i> Valida con CIE / SPID
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Polling & Event Javascript -->
|
||||
<script>
|
||||
const token = "{{ $convocazione->token_accesso }}";
|
||||
|
|
@ -237,37 +339,135 @@ function hideToast() {
|
|||
document.getElementById('toast').classList.add('translate-y-24', 'opacity-0');
|
||||
}
|
||||
|
||||
function confirmReceipt() {
|
||||
const btn = document.getElementById('receipt-btn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner animate-spin"></i>';
|
||||
// Canvas signature logic
|
||||
const canvas = document.getElementById('signature-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let drawing = false;
|
||||
|
||||
function startDrawing(e) {
|
||||
drawing = true;
|
||||
draw(e);
|
||||
}
|
||||
function stopDrawing() {
|
||||
drawing = false;
|
||||
ctx.beginPath();
|
||||
}
|
||||
function draw(e) {
|
||||
if (!drawing) return;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = '#60a5fa'; // Blue-400
|
||||
|
||||
let clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
||||
let clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = clientX - rect.left;
|
||||
const y = clientY - rect.top;
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
}
|
||||
|
||||
canvas.addEventListener('mousedown', startDrawing);
|
||||
canvas.addEventListener('mouseup', stopDrawing);
|
||||
canvas.addEventListener('mousemove', draw);
|
||||
canvas.addEventListener('touchstart', startDrawing);
|
||||
canvas.addEventListener('touchend', stopDrawing);
|
||||
canvas.addEventListener('touchmove', draw);
|
||||
|
||||
function clearCanvas() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function openSignatureModal() {
|
||||
document.getElementById('modal-signature').classList.remove('hidden');
|
||||
// Resize canvas to match container size
|
||||
canvas.width = canvas.parentElement.clientWidth;
|
||||
canvas.height = canvas.parentElement.clientHeight;
|
||||
}
|
||||
|
||||
function closeSignatureModal() {
|
||||
document.getElementById('modal-signature').classList.add('hidden');
|
||||
}
|
||||
|
||||
function validateCie() {
|
||||
const cieBtn = document.getElementById('cie-btn');
|
||||
cieBtn.disabled = true;
|
||||
cieBtn.innerHTML = '<i class="fas fa-spinner animate-spin"></i> Validazione CIE in corso...';
|
||||
|
||||
setTimeout(() => {
|
||||
cieBtn.className = 'w-full flex items-center justify-center gap-2 py-2 bg-emerald-600 text-white rounded-xl text-xs font-bold';
|
||||
cieBtn.innerHTML = '<i class="fas fa-check-circle"></i> CIE Validata con Successo!';
|
||||
showToast('Identità digitale certificata tramite CIE!');
|
||||
// Auto-fill canvas signature as simulated confirmation
|
||||
ctx.fillStyle = '#60a5fa';
|
||||
ctx.font = '20px Outfit';
|
||||
ctx.fillText('VALIDATO TRAMITE CIE', 20, 50);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function saveSignature() {
|
||||
const dataUrl = canvas.toDataURL();
|
||||
|
||||
fetch(`/public/assemblea/${token}/ricezione`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
}
|
||||
},
|
||||
body: JSON.stringify({
|
||||
firma_dati: dataUrl
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast('Firma elettronica inserita con successo!');
|
||||
btn.remove();
|
||||
showToast('Firma registrata con successo!');
|
||||
closeSignatureModal();
|
||||
const btn = document.getElementById('receipt-btn');
|
||||
if (btn) btn.remove();
|
||||
document.getElementById('receipt-status').textContent = 'Firma apposta il ' + data.ricezione_confermata_at;
|
||||
document.getElementById('receipt-icon').className = 'w-9 h-9 rounded-lg flex items-center justify-center bg-emerald-500/20 text-emerald-400 border border-emerald-500/30';
|
||||
document.getElementById('receipt-icon').innerHTML = '<i class="fas fa-check-double"></i>';
|
||||
} else {
|
||||
showToast(data.message || 'Errore durante la firma.', false);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Firma';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Errore di rete. Riprova.', false);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Firma';
|
||||
});
|
||||
}
|
||||
|
||||
function submitAnagrafica(e) {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('profile-email').value;
|
||||
const pec = document.getElementById('profile-pec').value;
|
||||
const telefono = document.getElementById('profile-telefono').value;
|
||||
const indirizzo = document.getElementById('profile-indirizzo').value;
|
||||
|
||||
fetch(`/public/assemblea/${token}/anagrafica`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ email, pec, telefono, indirizzo })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message);
|
||||
} else {
|
||||
showToast('Errore durante l\'invio recapiti.', false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Errore di rete.', false);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@
|
|||
Route::post('/public/assemblea/{token}/ricezione', [\App\Http\Controllers\Condomino\AssembleaConvocazioneController::class, 'confermaRicezione'])->name('public.assemblea.ricezione');
|
||||
Route::post('/public/assemblea/{token}/vota', [\App\Http\Controllers\Condomino\AssembleaConvocazioneController::class, 'vota'])->name('public.assemblea.vota');
|
||||
Route::get('/public/assemblea/{token}/poll', [\App\Http\Controllers\Condomino\AssembleaConvocazioneController::class, 'pollVotazioneAttiva'])->name('public.assemblea.poll');
|
||||
Route::get('/public/assemblea/{token}/zip', [\App\Http\Controllers\Condomino\AssembleaConvocazioneController::class, 'downloadZip'])->name('public.assemblea.zip');
|
||||
Route::post('/public/assemblea/{token}/anagrafica', [\App\Http\Controllers\Condomino\AssembleaConvocazioneController::class, 'updateAnagrafica'])->name('public.assemblea.anagrafica');
|
||||
|
||||
// --- Rotte per Aggiornamento Anagrafica Digitale e OTP (SMS Machine) ---
|
||||
Route::get('/public/anagrafica/aggiornamento/{token}', [\App\Http\Controllers\AnagraficaAggiornamentoController::class, 'show'])->name('anagrafica.update.show');
|
||||
Route::post('/public/anagrafica/aggiornamento/otp', [\App\Http\Controllers\AnagraficaAggiornamentoController::class, 'inviaOtp'])->name('anagrafica.update.otp');
|
||||
Route::post('/public/anagrafica/aggiornamento', [\App\Http\Controllers\AnagraficaAggiornamentoController::class, 'store'])->name('anagrafica.update.store');
|
||||
Route::get('/public/anagrafica/aggiornamento/{token}/pdf', [\App\Http\Controllers\AnagraficaAggiornamentoController::class, 'exportPdf'])->name('anagrafica.update.pdf');
|
||||
|
||||
Route::get('/', function () {
|
||||
return redirect('/admin-filament');
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user