diff --git a/app/Console/Commands/ImportGesconFullPipeline.php b/app/Console/Commands/ImportGesconFullPipeline.php index 519727a..795f337 100644 --- a/app/Console/Commands/ImportGesconFullPipeline.php +++ b/app/Console/Commands/ImportGesconFullPipeline.php @@ -24,7 +24,7 @@ class ImportGesconFullPipeline extends Command {--dry-run : Simula senza scrivere sulle tabelle dominio} {--limit= : Limita numero record per debug} {--reset-temp : Svuota staging prima di importare} - {--solo= : Step singolo: unita|relazioni|soggetti|diritti|millesimi|voci|operazioni|rate|incassi|giroconti|straord|addebiti|detrazioni|tutto} + {--solo= : Step singolo: assemblee|unita|relazioni|soggetti|diritti|millesimi|voci|operazioni|rate|incassi|giroconti|straord|addebiti|detrazioni|tutto} {--no-views : Non ricrea le viste QA} {--force-mapping : Applica mapping anche sovrascrivendo valori esistenti} {--fill-missing-millesimi : Crea righe mancanti a zero per ogni unita/tabella (uso eccezionale)} @@ -35,6 +35,7 @@ class ImportGesconFullPipeline extends Command private array $stepsOrder = [ 'stabili', + 'assemblee', 'unita', 'relazioni', 'soggetti', @@ -3915,6 +3916,95 @@ private function stepDetrazioni(?int $limit): int return $count; } + private function stepAssemblee(?int $limit): int + { + if (! Schema::hasTable('assemblee')) { + return 0; + } + + $stabileId = $this->stabileId ?: $this->resolveStabileId(); + $stabile = trim((string) ($this->option('stabile') ?? '')); + if (! $stabileId || $stabile === '') { + return 0; + } + + $sources = $this->resolveAssembleeMdbSources($stabile); + if ($sources === []) { + return 0; + } + + $assembleeColumns = Schema::getColumnListing('assemblee'); + $canMatch = in_array('stabile_id', $assembleeColumns, true) + && in_array('tipo', $assembleeColumns, true) + && in_array('data_prima_convocazione', $assembleeColumns, true) + && in_array('data_seconda_convocazione', $assembleeColumns, true); + + $processed = 0; + foreach ($sources as $source) { + $rows = $this->mdbExportToRows($source['path'], ['Singolo_anno_assemblee', 'singolo_anno_assemblee', 'assemblee']); + if ($rows === []) { + continue; + } + + foreach ($rows as $row) { + if ($limit !== null && $processed >= $limit) { + return $processed; + } + + $firstDate = $this->normalizeLegacyDateTime($row['dt_1_convoc'] ?? null, $row['ora_1_convoc'] ?? null); + $secondDate = $this->normalizeLegacyDateTime($row['dt_2_convoc'] ?? null, $row['ora_2_convoc'] ?? null); + if ($firstDate === null && $secondDate === null) { + continue; + } + + $tipo = $this->normalizeLegacyAssembleaType($row['ordin_straord'] ?? null); + $luogo = $this->firstNonEmptyStr([ + $row['luogo_2_convoc'] ?? null, + $row['luogo_1_convoc'] ?? null, + ], 255); + $reference = $secondDate ?? $firstDate; + $referenceTs = $reference ? strtotime($reference) : false; + $note = $this->buildLegacyAssembleaNote($row, (string) ($source['legacy_year'] ?? '')); + + $payload = [ + 'stabile_id' => $stabileId, + 'tipo' => $tipo, + 'data_assemblea' => $reference, + 'data_prima_convocazione' => $firstDate, + 'data_seconda_convocazione' => $secondDate, + 'luogo' => $luogo, + 'note' => $note, + 'stato' => ($referenceTs !== false && $referenceTs < time()) ? 'svolta' : 'convocata', + 'data_convocazione' => $firstDate ? substr($firstDate, 0, 10) : null, + 'data_svolgimento' => ($referenceTs !== false && $referenceTs < time()) ? substr($reference, 0, 10) : null, + 'created_at' => now(), + 'updated_at' => now(), + ]; + + if ($canMatch) { + $match = [ + 'stabile_id' => $stabileId, + 'tipo' => $tipo, + 'data_prima_convocazione' => $firstDate, + 'data_seconda_convocazione' => $secondDate, + ]; + + if (! $this->isDryRun) { + DB::table('assemblee')->updateOrInsert($match, array_merge($payload, [ + 'updated_at' => now(), + ])); + } + } else { + $this->dynamicInsert('assemblee', $payload); + } + + $processed++; + } + } + + return $processed; + } + private function stub(string $table, ?int $limit): int { // Placeholder: sostituire con logica di lettura da MDB / staging @@ -5252,6 +5342,175 @@ private function normalizeLegacyDate(?string $value): ?string return date('Y-m-d', $ts); } + private function normalizeLegacyDateTime(?string $dateValue, ?string $timeValue = null): ?string + { + if (! is_string($dateValue)) { + return null; + } + + $dateValue = trim($dateValue); + if ($dateValue === '') { + return null; + } + + $dateToken = preg_split('/\s+/', $dateValue)[0] ?? ''; + $dateToken = trim((string) $dateToken); + if ($dateToken === '') { + return null; + } + + $datePart = null; + foreach (['!m/d/y', '!d/m/y', '!m/d/Y', '!d/m/Y', '!Y-m-d'] as $format) { + $parsed = \DateTime::createFromFormat($format, $dateToken); + if ($parsed instanceof \DateTime) { + $datePart = $parsed->format('Y-m-d'); + break; + } + } + + if ($datePart === null) { + $ts = strtotime($dateValue); + if ($ts === false) { + return null; + } + $datePart = date('Y-m-d', $ts); + } + + return $datePart . ' ' . ($this->normalizeLegacyTime($timeValue) ?? '00:00:00'); + } + + private function normalizeLegacyTime(?string $value): ?string + { + if (! is_string($value)) { + return null; + } + + $value = trim($value); + if ($value === '') { + return null; + } + + $value = str_replace([',', '.'], ':', $value); + $value = preg_replace('/\s+/', '', $value ?? ''); + if ($value === null || $value === '') { + return null; + } + + if (preg_match('/^(\d{1,2})$/', $value, $matches)) { + return sprintf('%02d:00:00', (int) $matches[1]); + } + + if (preg_match('/^(\d{1,2}):(\d{1,2})$/', $value, $matches)) { + return sprintf('%02d:%02d:00', (int) $matches[1], (int) $matches[2]); + } + + if (preg_match('/^(\d{1,2}):(\d{1,2}):(\d{1,2})$/', $value, $matches)) { + return sprintf('%02d:%02d:%02d', (int) $matches[1], (int) $matches[2], (int) $matches[3]); + } + + return null; + } + + private function normalizeLegacyAssembleaType(?string $value): string + { + $normalized = Str::lower(trim((string) $value)); + + return str_contains($normalized, 'stra') ? 'straordinaria' : 'ordinaria'; + } + + private function buildLegacyAssembleaNote(array $row, string $legacyYear): ?string + { + $parts = []; + + $numero = trim((string) ($row['num_ass'] ?? '')); + if ($numero !== '') { + $parts[] = 'Assemblea legacy n. ' . $numero; + } + + if ($legacyYear !== '') { + $parts[] = 'Cartella legacy: ' . $legacyYear; + } + + $noteConvocazione = trim((string) ($row['note_convocaz'] ?? '')); + if ($noteConvocazione !== '') { + $parts[] = 'Note convocazione: ' . $noteConvocazione; + } + + $noteAssemblea = trim((string) ($row['note_assemblea'] ?? '')); + if ($noteAssemblea !== '') { + $parts[] = 'Note assemblea: ' . $noteAssemblea; + } + + $noteInterne = trim((string) ($row['note_assemblea_int'] ?? '')); + if ($noteInterne !== '') { + $parts[] = 'Note interne legacy: ' . $noteInterne; + } + + $ordineGiorno = trim((string) ($row['ordine_del_giorno'] ?? '')); + if ($ordineGiorno !== '') { + $parts[] = 'Ordine del giorno legacy:' . PHP_EOL . $ordineGiorno; + } + + if ($parts === []) { + return null; + } + + return implode(PHP_EOL . PHP_EOL, $parts); + } + + private function resolveAssembleeMdbSources(string $stabile): array + { + $root = rtrim((string) ($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/'); + $stableDir = $root . '/' . $stabile; + if (! is_dir($stableDir)) { + return []; + } + + $anno = trim((string) ($this->option('anno') ?? '')); + $sources = []; + if ($anno !== '' && strcasecmp($anno, 'all') !== 0) { + $candidates = array_unique(array_filter([ + $stableDir . '/' . $anno . '/singolo_anno.mdb', + is_numeric($anno) ? $stableDir . '/' . sprintf('%04d', (int) $anno) . '/singolo_anno.mdb' : null, + ])); + + foreach ($candidates as $candidate) { + if (is_string($candidate) && is_file($candidate)) { + $sources[] = [ + 'path' => $candidate, + 'legacy_year' => basename(dirname($candidate)), + ]; + } + } + } else { + foreach ((array) glob($stableDir . '/*/singolo_anno.mdb') as $candidate) { + if (! is_string($candidate) || ! is_file($candidate)) { + continue; + } + $sources[] = [ + 'path' => $candidate, + 'legacy_year' => basename(dirname($candidate)), + ]; + } + } + + if ($sources === []) { + $resolved = $this->resolveCondominMdbForStabile($stabile, is_numeric($anno) ? sprintf('%04d', (int) $anno) : null); + if ($resolved !== null && ! empty($resolved['path']) && is_file((string) $resolved['path'])) { + $sources[] = [ + 'path' => (string) $resolved['path'], + 'legacy_year' => (string) ($resolved['legacy_year'] ?? basename(dirname((string) $resolved['path']))), + ]; + } + } + + usort($sources, static function (array $left, array $right): int { + return strcmp((string) ($left['legacy_year'] ?? ''), (string) ($right['legacy_year'] ?? '')); + }); + + return array_values(array_unique($sources, SORT_REGULAR)); + } + private function resolveLegacySnapshotYear(?string $legacyYear, ?object $row = null): ?int { $resolved = $this->resolveAnnoFromLegacyYear($legacyYear); diff --git a/app/Filament/Pages/Condomini/AssembleeHub.php b/app/Filament/Pages/Condomini/AssembleeHub.php index db06a11..f176220 100644 --- a/app/Filament/Pages/Condomini/AssembleeHub.php +++ b/app/Filament/Pages/Condomini/AssembleeHub.php @@ -3,16 +3,17 @@ use App\Filament\Pages\Strumenti\DocumentiArchivio; use App\Models\Assemblea; +use App\Models\AssembleaVerbale; use App\Models\Stabile; use App\Models\User; use App\Support\StabileContext; use BackedEnum; use Filament\Actions\Action; use Filament\Pages\Page; +use Illuminate\Database\QueryException; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Schema; -use Illuminate\Database\QueryException; use UnitEnum; class AssembleeHub extends Page @@ -74,7 +75,8 @@ public function getStatsProperty(): array } try { - $query = Assemblea::query()->where('stabile_id', $stabileId); + $query = Assemblea::query()->where('stabile_id', $stabileId); + $hasVerbali = class_exists(AssembleaVerbale::class) && Schema::hasTable('assemblee_verbali'); return [ 'totale' => (clone $query)->count(), @@ -87,7 +89,7 @@ public function getStatsProperty(): array ->orWhereDate('data_seconda_convocazione', '>=', $today->toDateString()); }) ->count(), - 'con_verbale' => (clone $query)->whereHas('verbale')->count(), + 'con_verbale' => $hasVerbali ? (clone $query)->whereHas('verbale')->count() : 0, ]; } catch (QueryException) { return ['totale' => 0, 'convocate' => 0, 'in_calendario' => 0, 'con_verbale' => 0]; @@ -102,15 +104,41 @@ public function getAssembleeRowsProperty(): Collection } try { - return Assemblea::query() - ->with(['verbale:id,assemblea_id']) - ->withCount(['ordineGiorno', 'convocazioni', 'presenze', 'documenti']) + $query = Assemblea::query() ->where('stabile_id', $stabileId) ->orderByDesc('data_seconda_convocazione') ->orderByDesc('data_prima_convocazione') ->orderByDesc('id') - ->limit(16) - ->get(); + ->limit(16); + + if (Schema::hasTable('assemblee_verbali')) { + $query->with(['verbale:id,assemblea_id']); + } + + $withCount = []; + if (Schema::hasTable('ordine_giorno')) { + $withCount[] = 'ordineGiorno'; + } + if (Schema::hasTable('convocazioni')) { + $withCount[] = 'convocazioni'; + } + + if ($withCount !== []) { + $query->withCount($withCount); + } + + return $query->get()->map(function (Assemblea $assemblea): Assemblea { + $assemblea->ordine_giorno_count = (int) ($assemblea->ordine_giorno_count ?? 0); + $assemblea->convocazioni_count = (int) ($assemblea->convocazioni_count ?? 0); + $assemblea->presenze_count = 0; + $assemblea->documenti_count = 0; + + if (! $assemblea->relationLoaded('verbale')) { + $assemblea->setRelation('verbale', null); + } + + return $assemblea; + }); } catch (QueryException) { return collect(); } diff --git a/app/Filament/Pages/Gescon/ImportazioneArchivi.php b/app/Filament/Pages/Gescon/ImportazioneArchivi.php index a66a61a..59755a4 100644 --- a/app/Filament/Pages/Gescon/ImportazioneArchivi.php +++ b/app/Filament/Pages/Gescon/ImportazioneArchivi.php @@ -112,6 +112,7 @@ public function mount(): void 'with_voci' => true, 'with_banche' => true, 'with_gestioni' => false, + 'with_assemblee' => false, 'with_operazioni' => false, 'with_rate' => false, 'with_incassi' => false, @@ -417,6 +418,7 @@ public function form(Schema $schema): Schema }), Checkbox::make('with_banche')->label('Banche / casse'), Checkbox::make('with_gestioni')->label('Gestioni (anni)'), + Checkbox::make('with_assemblee')->label('Assemblee legacy'), Checkbox::make('with_operazioni') ->label('Operazioni') ->reactive() @@ -968,6 +970,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void 'with_voci' => (bool) ($state['with_voci'] ?? false), 'with_banche' => (bool) ($state['with_banche'] ?? false), 'with_gestioni' => (bool) ($state['with_gestioni'] ?? false), + 'with_assemblee' => (bool) ($state['with_assemblee'] ?? false), 'with_operazioni' => (bool) ($state['with_operazioni'] ?? false), 'with_rate' => (bool) ($state['with_rate'] ?? false), 'with_incassi' => (bool) ($state['with_incassi'] ?? false), @@ -1008,6 +1011,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void 'millesimi', 'voci', 'banche', + 'assemblee', 'operazioni', 'rate', 'incassi', diff --git a/app/Filament/Pages/Strumenti/DocumentiArchivio.php b/app/Filament/Pages/Strumenti/DocumentiArchivio.php index 32f34fc..4992fea 100644 --- a/app/Filament/Pages/Strumenti/DocumentiArchivio.php +++ b/app/Filament/Pages/Strumenti/DocumentiArchivio.php @@ -928,38 +928,39 @@ public function getGenericLabelPreviewProperty(): array ]); $payload = [ - 'mnemonic_code' => $mnemonicCode, - 'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')), - 'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')), - 'tipo_label' => DocumentoArchivioFisicoItem::TIPI[(string) ($this->genericLabelForm['tipo_item'] ?? 'scatola')] ?? 'Contenitore', - 'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')), - 'percorso_compatto' => trim(implode(' / ', array_filter([ + 'mnemonic_code' => $mnemonicCode, + 'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')), + 'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')), + 'tipo_label' => DocumentoArchivioFisicoItem::TIPI[(string) ($this->genericLabelForm['tipo_item'] ?? 'scatola')] ?? 'Contenitore', + 'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')), + 'percorso_compatto' => trim(implode(' / ', array_filter([ $this->resolveActiveStabile()?->denominazione, $this->genericLabelForm['supporto_fisico'] ?? null, $this->genericLabelForm['titolo'] ?? null, ]))), - 'ubicazione' => trim(implode(' · ', array_filter([ + 'ubicazione' => trim(implode(' · ', array_filter([ $this->genericLabelForm['magazzino'] ?? null, $this->genericLabelForm['scaffale'] ?? null, $this->genericLabelForm['ripiano'] ?? null, $this->genericLabelForm['ubicazione_dettaglio'] ?? null, ]))), - 'note' => trim((string) ($this->genericLabelForm['note'] ?? '')), - 'stabile' => trim((string) ($this->resolveActiveStabile()?->denominazione ?? 'Stabile attivo')), - 'stabile_code' => $stabileCode, - 'stabile_address' => $stabileAddress, - 'stabile_summary' => trim(implode(' · ', array_filter([ + 'note' => trim((string) ($this->genericLabelForm['note'] ?? '')), + 'stabile' => trim((string) ($this->resolveActiveStabile()?->denominazione ?? 'Stabile attivo')), + 'stabile_code' => $stabileCode, + 'stabile_address' => $stabileAddress, + 'stabile_summary' => trim(implode(' · ', array_filter([ trim((string) ($this->resolveActiveStabile()?->denominazione ?? '')), $stabileCode, $stabileAddress, ]))), - 'codice_univoco' => 'AF-QR-PREVIEW', - 'qr_payload' => (string) ($qrProfiles['xml_open'] ?? ''), - 'qr_payload_zip' => (string) ($qrProfiles['xml_zip'] ?? ''), - 'nfc_payload' => (string) ($qrProfiles['xml_nfc'] ?? ''), - 'qr_payload_length' => (int) ($qrProfiles['xml_open_length'] ?? 0), - 'nfc_payload_length'=> (int) ($qrProfiles['xml_nfc_length'] ?? 0), - 'amazon_url' => $amazonUrl, + 'codice_univoco' => 'AF-QR-PREVIEW', + 'qr_payload' => (string) ($qrProfiles['xml_open'] ?? ''), + 'qr_payload_zip' => (string) ($qrProfiles['xml_zip'] ?? ''), + 'nfc_payload' => (string) ($qrProfiles['xml_nfc'] ?? ''), + 'qr_payload_length' => (int) ($qrProfiles['xml_open_length'] ?? 0), + 'nfc_payload_length' => (int) ($qrProfiles['xml_nfc_length'] ?? 0), + 'qr_markup' => $this->buildGenericLabelQrMarkup((string) ($qrProfiles['xml_open'] ?? '')), + 'amazon_url' => $amazonUrl, ]; $stableLines = $this->buildGenericLabelLineRows($payload); @@ -991,6 +992,42 @@ public function getGenericLabelPreviewProperty(): array ]; } + private function buildGenericLabelQrMarkup(string $data): string + { + $data = trim($data); + if ($data === '') { + return ''; + } + + if (class_exists(\chillerlan\QRCode\QROptions::class) && class_exists(\chillerlan\QRCode\QRCode::class)) { + $options = new \chillerlan\QRCode\QROptions([ + 'outputType' => \chillerlan\QRCode\QRCode::OUTPUT_MARKUP_SVG, + 'eccLevel' => \chillerlan\QRCode\QRCode::ECC_M, + 'addQuietzone' => true, + 'quietzoneSize' => 1, + 'connectPaths' => true, + ]); + + $svg = (new \chillerlan\QRCode\QRCode($options))->render($data); + + if (str_starts_with($svg, 'data:image/svg+xml;base64,')) { + $decoded = base64_decode(substr($svg, strlen('data:image/svg+xml;base64,')), true); + + if (is_string($decoded) && $decoded !== '') { + $svg = $decoded; + } + } + + if (str_contains($svg, ''; + } + /** @return array */ public function getAudienceGroupsProperty(): array { @@ -2011,13 +2048,13 @@ private function initializeScannerAcquisitionForm(): void /** @return array */ public function getScannerQrPreviewProperty(): array { - $title = trim((string) ($this->scannerAcquisitionForm['titolo'] ?? '')); - $supporto = trim((string) ($this->scannerAcquisitionForm['supporto_fisico'] ?? '')); - $stabileId = (int) ($this->scannerAcquisitionForm['stabile_id'] ?? 0); - $categoria = trim((string) ($this->scannerAcquisitionForm['categoria'] ?? 'altro')); - $classe = trim((string) ($this->scannerAcquisitionForm['archivio_classe'] ?? 'altro')); - $busta = trim((string) ($this->scannerAcquisitionForm['busta_numero'] ?? '')); - $fascicolo = trim((string) ($this->scannerAcquisitionForm['fascicolo_numero'] ?? '')); + $title = trim((string) ($this->scannerAcquisitionForm['titolo'] ?? '')); + $supporto = trim((string) ($this->scannerAcquisitionForm['supporto_fisico'] ?? '')); + $stabileId = (int) ($this->scannerAcquisitionForm['stabile_id'] ?? 0); + $categoria = trim((string) ($this->scannerAcquisitionForm['categoria'] ?? 'altro')); + $classe = trim((string) ($this->scannerAcquisitionForm['archivio_classe'] ?? 'altro')); + $busta = trim((string) ($this->scannerAcquisitionForm['busta_numero'] ?? '')); + $fascicolo = trim((string) ($this->scannerAcquisitionForm['fascicolo_numero'] ?? '')); $contenitore = trim((string) ($this->scannerAcquisitionForm['contenitore_numero'] ?? '')); $archiveCode = trim(implode('-', array_filter([ @@ -2042,12 +2079,12 @@ public function getScannerQrPreviewProperty(): array ]); return [ - 'code' => $archiveCode, - 'xml_open' => (string) ($profiles['xml_open'] ?? ''), - 'xml_zip' => (string) ($profiles['xml_zip'] ?? ''), - 'xml_nfc' => (string) ($profiles['xml_nfc'] ?? ''), + 'code' => $archiveCode, + 'xml_open' => (string) ($profiles['xml_open'] ?? ''), + 'xml_zip' => (string) ($profiles['xml_zip'] ?? ''), + 'xml_nfc' => (string) ($profiles['xml_nfc'] ?? ''), 'xml_open_length' => (int) ($profiles['xml_open_length'] ?? 0), - 'xml_nfc_length' => (int) ($profiles['xml_nfc_length'] ?? 0), + 'xml_nfc_length' => (int) ($profiles['xml_nfc_length'] ?? 0), ]; } diff --git a/app/Models/Assemblea.php b/app/Models/Assemblea.php index a9cc2f1..3039c48 100755 --- a/app/Models/Assemblea.php +++ b/app/Models/Assemblea.php @@ -1,5 +1,4 @@ 'datetime', + 'data_assemblea' => 'datetime', + 'data_prima_convocazione' => 'datetime', 'data_seconda_convocazione' => 'datetime', - 'data_convocazione' => 'date', - 'data_svolgimento' => 'date', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'data_convocazione' => 'date', + 'data_svolgimento' => 'date', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /** @@ -70,7 +71,7 @@ public function presenze() */ public function verbale() { - return $this->hasOne(Verbale::class, 'assemblea_id'); + return $this->hasOne(AssembleaVerbale::class, 'assemblea_id'); } /** @@ -110,20 +111,20 @@ public function scopeTipo($query, $tipo) */ public function inviaConvocazioni($canali = ['email'], $userId) { - $unitaImmobiliari = $this->stabile->unitaImmobiliari()->with('proprieta.soggetto')->get(); + $unitaImmobiliari = $this->stabile->unitaImmobiliari()->with('proprieta.soggetto')->get(); $convocazioniInviate = 0; foreach ($unitaImmobiliari as $unita) { foreach ($unita->proprieta as $proprieta) { $soggetto = $proprieta->soggetto; - + foreach ($canali as $canale) { if ($this->verificaCanaleDisponibile($soggetto, $canale)) { $convocazione = $this->creaConvocazione($soggetto, $unita, $canale); - + if ($this->inviaConvocazione($convocazione)) { $convocazioniInviate++; - + // Registra nel protocollo $this->registraProtocollo($convocazione, $userId); } @@ -134,7 +135,7 @@ public function inviaConvocazioni($canali = ['email'], $userId) // Aggiorna stato assemblea $this->update([ - 'stato' => 'convocata', + 'stato' => 'convocata', 'data_convocazione' => now(), ]); @@ -148,12 +149,12 @@ private function verificaCanaleDisponibile($soggetto, $canale) { switch ($canale) { case 'email': - return !empty($soggetto->email); + return ! empty($soggetto->email); case 'pec': - return !empty($soggetto->pec); + return ! empty($soggetto->pec); case 'whatsapp': case 'telegram': - return !empty($soggetto->telefono); + return ! empty($soggetto->telefono); default: return true; } @@ -165,12 +166,12 @@ private function verificaCanaleDisponibile($soggetto, $canale) private function creaConvocazione($soggetto, $unita, $canale) { return Convocazione::create([ - 'assemblea_id' => $this->id, - 'soggetto_id' => $soggetto->id_soggetto, + 'assemblea_id' => $this->id, + 'soggetto_id' => $soggetto->id_soggetto, 'unita_immobiliare_id' => $unita->id_unita, - 'canale_invio' => $canale, - 'data_invio' => now(), - 'esito_invio' => 'inviato', + 'canale_invio' => $canale, + 'data_invio' => now(), + 'esito_invio' => 'inviato', ]); } @@ -181,10 +182,10 @@ private function inviaConvocazione($convocazione) { // Implementazione invio basata sul canale // Qui si integrerebbe con servizi email, SMS, etc. - + // Simula invio riuscito $convocazione->update([ - 'esito_invio' => 'consegnato', + 'esito_invio' => 'consegnato', 'riferimento_invio' => 'REF-' . time(), ]); @@ -199,16 +200,16 @@ private function registraProtocollo($convocazione, $userId) $numeroProtocollo = $this->generaNumeroProtocollo(); RegistroProtocollo::create([ - 'numero_protocollo' => $numeroProtocollo, - 'tipo_comunicazione' => 'convocazione', - 'assemblea_id' => $this->id, + 'numero_protocollo' => $numeroProtocollo, + 'tipo_comunicazione' => 'convocazione', + 'assemblea_id' => $this->id, 'soggetto_destinatario_id' => $convocazione->soggetto_id, - 'oggetto' => "Convocazione Assemblea {$this->tipo} - {$this->data_prima_convocazione->format('d/m/Y')}", - 'canale' => $convocazione->canale_invio, - 'data_invio' => $convocazione->data_invio, - 'esito' => $convocazione->esito_invio, + 'oggetto' => "Convocazione Assemblea {$this->tipo} - {$this->data_prima_convocazione->format('d/m/Y')}", + 'canale' => $convocazione->canale_invio, + 'data_invio' => $convocazione->data_invio, + 'esito' => $convocazione->esito_invio, 'riferimento_esterno' => $convocazione->riferimento_invio, - 'creato_da_user_id' => $userId, + 'creato_da_user_id' => $userId, ]); } @@ -217,7 +218,7 @@ private function registraProtocollo($convocazione, $userId) */ private function generaNumeroProtocollo() { - $anno = date('Y'); + $anno = date('Y'); $ultimoProtocollo = RegistroProtocollo::whereYear('created_at', $anno) ->orderBy('numero_protocollo', 'desc') ->first(); @@ -236,14 +237,14 @@ private function generaNumeroProtocollo() */ public function calcolaQuorum() { - $totaleMillesimi = $this->stabile->unitaImmobiliari()->sum('millesimi_proprieta'); + $totaleMillesimi = $this->stabile->unitaImmobiliari()->sum('millesimi_proprieta'); $millesimiPresenti = $this->presenze()->sum('millesimi_rappresentati'); - + return [ - 'totale_millesimi' => $totaleMillesimi, - 'millesimi_presenti' => $millesimiPresenti, + 'totale_millesimi' => $totaleMillesimi, + 'millesimi_presenti' => $millesimiPresenti, 'percentuale_presenza' => $totaleMillesimi > 0 ? ($millesimiPresenti / $totaleMillesimi) * 100 : 0, - 'quorum_raggiunto' => $millesimiPresenti >= ($totaleMillesimi / 2), + 'quorum_raggiunto' => $millesimiPresenti >= ($totaleMillesimi / 2), ]; } @@ -253,14 +254,14 @@ public function calcolaQuorum() public function generaReportPresenze() { $presenze = $this->presenze()->with(['soggetto', 'unitaImmobiliare'])->get(); - $quorum = $this->calcolaQuorum(); + $quorum = $this->calcolaQuorum(); return [ - 'quorum' => $quorum, - 'presenze' => $presenze, + 'quorum' => $quorum, + 'presenze' => $presenze, 'totale_presenti' => $presenze->where('tipo_presenza', 'presente')->count(), 'totale_delegati' => $presenze->where('tipo_presenza', 'delegato')->count(), - 'totale_assenti' => $this->stabile->unitaImmobiliari()->count() - $presenze->count(), + 'totale_assenti' => $this->stabile->unitaImmobiliari()->count() - $presenze->count(), ]; } -} \ No newline at end of file +} diff --git a/app/Providers/Filament/AdminFilamentPanelProvider.php b/app/Providers/Filament/AdminFilamentPanelProvider.php index 80cd773..b12b9fd 100644 --- a/app/Providers/Filament/AdminFilamentPanelProvider.php +++ b/app/Providers/Filament/AdminFilamentPanelProvider.php @@ -171,4 +171,5 @@ protected function resolveSupplierCatalogUrl(): string return TicketOperativi::getUrl(panel: 'admin-filament'); } + } diff --git a/database/migrations/2026_04_29_120000_create_assemblee_domain_table.php b/database/migrations/2026_04_29_120000_create_assemblee_domain_table.php new file mode 100644 index 0000000..4916a4a --- /dev/null +++ b/database/migrations/2026_04_29_120000_create_assemblee_domain_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('stabile_id')->nullable()->constrained('stabili')->nullOnDelete(); + $table->string('tipo', 50)->default('ordinaria'); + $table->dateTime('data_assemblea')->nullable()->index(); + $table->dateTime('data_prima_convocazione')->nullable()->index(); + $table->dateTime('data_seconda_convocazione')->nullable()->index(); + $table->date('data_convocazione')->nullable(); + $table->date('data_svolgimento')->nullable(); + $table->string('luogo')->nullable(); + $table->text('note')->nullable(); + $table->string('stato', 50)->default('bozza')->index(); + $table->foreignId('creato_da_user_id')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->index(['stabile_id', 'tipo']); + }); + } + + public function down(): void + { + Schema::dropIfExists('assemblee'); + } +}; diff --git a/resources/css/filament/admin/theme.css b/resources/css/filament/admin/theme.css index 6f8ebdf..994224b 100644 --- a/resources/css/filament/admin/theme.css +++ b/resources/css/filament/admin/theme.css @@ -120,6 +120,35 @@ @media (min-width: 64rem) { @media (max-width: 63.998rem), (hover: none) and (pointer: coarse) { + .fi-header { + flex-direction: column !important; + align-items: stretch !important; + gap: calc(var(--spacing) * 3) !important; + } + + .fi-header>div:first-child { + width: 100% !important; + min-width: 0 !important; + } + + .fi-header-actions-ctn, + .fi-ac.fi-align-start { + width: 100% !important; + max-width: 100% !important; + flex-wrap: wrap !important; + align-items: stretch !important; + justify-content: flex-start !important; + row-gap: calc(var(--spacing) * 2) !important; + } + + .fi-header-actions-ctn .fi-btn, + .fi-ac.fi-align-start .fi-btn, + .fi-header-actions-ctn .fi-dropdown, + .fi-ac.fi-align-start .fi-dropdown { + width: 100% !important; + max-width: 100% !important; + } + .fi-body.fi-body-has-navigation .fi-topbar { position: static !important; top: auto !important; diff --git a/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php b/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php index 1c07fd1..032e7dc 100644 --- a/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php +++ b/resources/views/filament/pages/strumenti/partials/generic-label-builder.blade.php @@ -335,7 +335,9 @@ - + + {!! data_get($preview, 'payload.qr_markup', '') !!} + QR @@ -374,7 +376,9 @@ - + + {!! data_get($preview, 'payload.qr_markup', '') !!} + diff --git a/scripts/ops/netgescon-sync-prod-update.sh b/scripts/ops/netgescon-sync-prod-update.sh index a33ea55..4c7b329 100755 --- a/scripts/ops/netgescon-sync-prod-update.sh +++ b/scripts/ops/netgescon-sync-prod-update.sh @@ -76,7 +76,6 @@ RSYNC_ARGS=( --exclude=node_modules/ --exclude=vendor/ --exclude=bootstrap/cache/ - --exclude=public/build/ ) if [[ "$DRY_RUN" == "yes" ]]; then