feat: import legacy assemblee and fix label preview

This commit is contained in:
michele 2026-04-29 14:15:08 +00:00
parent df441bcc43
commit d8d5a3e671
10 changed files with 484 additions and 84 deletions

View File

@ -24,7 +24,7 @@ class ImportGesconFullPipeline extends Command
{--dry-run : Simula senza scrivere sulle tabelle dominio} {--dry-run : Simula senza scrivere sulle tabelle dominio}
{--limit= : Limita numero record per debug} {--limit= : Limita numero record per debug}
{--reset-temp : Svuota staging prima di importare} {--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} {--no-views : Non ricrea le viste QA}
{--force-mapping : Applica mapping anche sovrascrivendo valori esistenti} {--force-mapping : Applica mapping anche sovrascrivendo valori esistenti}
{--fill-missing-millesimi : Crea righe mancanti a zero per ogni unita/tabella (uso eccezionale)} {--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 = [ private array $stepsOrder = [
'stabili', 'stabili',
'assemblee',
'unita', 'unita',
'relazioni', 'relazioni',
'soggetti', 'soggetti',
@ -3915,6 +3916,95 @@ private function stepDetrazioni(?int $limit): int
return $count; 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 private function stub(string $table, ?int $limit): int
{ {
// Placeholder: sostituire con logica di lettura da MDB / staging // 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); 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 private function resolveLegacySnapshotYear(?string $legacyYear, ?object $row = null): ?int
{ {
$resolved = $this->resolveAnnoFromLegacyYear($legacyYear); $resolved = $this->resolveAnnoFromLegacyYear($legacyYear);

View File

@ -3,16 +3,17 @@
use App\Filament\Pages\Strumenti\DocumentiArchivio; use App\Filament\Pages\Strumenti\DocumentiArchivio;
use App\Models\Assemblea; use App\Models\Assemblea;
use App\Models\AssembleaVerbale;
use App\Models\Stabile; use App\Models\Stabile;
use App\Models\User; use App\Models\User;
use App\Support\StabileContext; use App\Support\StabileContext;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Pages\Page; use Filament\Pages\Page;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Database\QueryException;
use UnitEnum; use UnitEnum;
class AssembleeHub extends Page class AssembleeHub extends Page
@ -74,7 +75,8 @@ public function getStatsProperty(): array
} }
try { 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 [ return [
'totale' => (clone $query)->count(), 'totale' => (clone $query)->count(),
@ -87,7 +89,7 @@ public function getStatsProperty(): array
->orWhereDate('data_seconda_convocazione', '>=', $today->toDateString()); ->orWhereDate('data_seconda_convocazione', '>=', $today->toDateString());
}) })
->count(), ->count(),
'con_verbale' => (clone $query)->whereHas('verbale')->count(), 'con_verbale' => $hasVerbali ? (clone $query)->whereHas('verbale')->count() : 0,
]; ];
} catch (QueryException) { } catch (QueryException) {
return ['totale' => 0, 'convocate' => 0, 'in_calendario' => 0, 'con_verbale' => 0]; return ['totale' => 0, 'convocate' => 0, 'in_calendario' => 0, 'con_verbale' => 0];
@ -102,15 +104,41 @@ public function getAssembleeRowsProperty(): Collection
} }
try { try {
return Assemblea::query() $query = Assemblea::query()
->with(['verbale:id,assemblea_id'])
->withCount(['ordineGiorno', 'convocazioni', 'presenze', 'documenti'])
->where('stabile_id', $stabileId) ->where('stabile_id', $stabileId)
->orderByDesc('data_seconda_convocazione') ->orderByDesc('data_seconda_convocazione')
->orderByDesc('data_prima_convocazione') ->orderByDesc('data_prima_convocazione')
->orderByDesc('id') ->orderByDesc('id')
->limit(16) ->limit(16);
->get();
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) { } catch (QueryException) {
return collect(); return collect();
} }

View File

@ -112,6 +112,7 @@ public function mount(): void
'with_voci' => true, 'with_voci' => true,
'with_banche' => true, 'with_banche' => true,
'with_gestioni' => false, 'with_gestioni' => false,
'with_assemblee' => false,
'with_operazioni' => false, 'with_operazioni' => false,
'with_rate' => false, 'with_rate' => false,
'with_incassi' => false, 'with_incassi' => false,
@ -417,6 +418,7 @@ public function form(Schema $schema): Schema
}), }),
Checkbox::make('with_banche')->label('Banche / casse'), Checkbox::make('with_banche')->label('Banche / casse'),
Checkbox::make('with_gestioni')->label('Gestioni (anni)'), Checkbox::make('with_gestioni')->label('Gestioni (anni)'),
Checkbox::make('with_assemblee')->label('Assemblee legacy'),
Checkbox::make('with_operazioni') Checkbox::make('with_operazioni')
->label('Operazioni') ->label('Operazioni')
->reactive() ->reactive()
@ -968,6 +970,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'with_voci' => (bool) ($state['with_voci'] ?? false), 'with_voci' => (bool) ($state['with_voci'] ?? false),
'with_banche' => (bool) ($state['with_banche'] ?? false), 'with_banche' => (bool) ($state['with_banche'] ?? false),
'with_gestioni' => (bool) ($state['with_gestioni'] ?? false), 'with_gestioni' => (bool) ($state['with_gestioni'] ?? false),
'with_assemblee' => (bool) ($state['with_assemblee'] ?? false),
'with_operazioni' => (bool) ($state['with_operazioni'] ?? false), 'with_operazioni' => (bool) ($state['with_operazioni'] ?? false),
'with_rate' => (bool) ($state['with_rate'] ?? false), 'with_rate' => (bool) ($state['with_rate'] ?? false),
'with_incassi' => (bool) ($state['with_incassi'] ?? false), 'with_incassi' => (bool) ($state['with_incassi'] ?? false),
@ -1008,6 +1011,7 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
'millesimi', 'millesimi',
'voci', 'voci',
'banche', 'banche',
'assemblee',
'operazioni', 'operazioni',
'rate', 'rate',
'incassi', 'incassi',

View File

@ -928,38 +928,39 @@ public function getGenericLabelPreviewProperty(): array
]); ]);
$payload = [ $payload = [
'mnemonic_code' => $mnemonicCode, 'mnemonic_code' => $mnemonicCode,
'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')), 'titolo' => trim((string) ($this->genericLabelForm['titolo'] ?? '')),
'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')), 'linea_secondaria' => trim((string) ($this->genericLabelForm['linea_secondaria'] ?? '')),
'tipo_label' => DocumentoArchivioFisicoItem::TIPI[(string) ($this->genericLabelForm['tipo_item'] ?? 'scatola')] ?? 'Contenitore', 'tipo_label' => DocumentoArchivioFisicoItem::TIPI[(string) ($this->genericLabelForm['tipo_item'] ?? 'scatola')] ?? 'Contenitore',
'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')), 'supporto_fisico' => trim((string) ($this->genericLabelForm['supporto_fisico'] ?? '')),
'percorso_compatto' => trim(implode(' / ', array_filter([ 'percorso_compatto' => trim(implode(' / ', array_filter([
$this->resolveActiveStabile()?->denominazione, $this->resolveActiveStabile()?->denominazione,
$this->genericLabelForm['supporto_fisico'] ?? null, $this->genericLabelForm['supporto_fisico'] ?? null,
$this->genericLabelForm['titolo'] ?? null, $this->genericLabelForm['titolo'] ?? null,
]))), ]))),
'ubicazione' => trim(implode(' · ', array_filter([ 'ubicazione' => trim(implode(' · ', array_filter([
$this->genericLabelForm['magazzino'] ?? null, $this->genericLabelForm['magazzino'] ?? null,
$this->genericLabelForm['scaffale'] ?? null, $this->genericLabelForm['scaffale'] ?? null,
$this->genericLabelForm['ripiano'] ?? null, $this->genericLabelForm['ripiano'] ?? null,
$this->genericLabelForm['ubicazione_dettaglio'] ?? null, $this->genericLabelForm['ubicazione_dettaglio'] ?? null,
]))), ]))),
'note' => trim((string) ($this->genericLabelForm['note'] ?? '')), 'note' => trim((string) ($this->genericLabelForm['note'] ?? '')),
'stabile' => trim((string) ($this->resolveActiveStabile()?->denominazione ?? 'Stabile attivo')), 'stabile' => trim((string) ($this->resolveActiveStabile()?->denominazione ?? 'Stabile attivo')),
'stabile_code' => $stabileCode, 'stabile_code' => $stabileCode,
'stabile_address' => $stabileAddress, 'stabile_address' => $stabileAddress,
'stabile_summary' => trim(implode(' · ', array_filter([ 'stabile_summary' => trim(implode(' · ', array_filter([
trim((string) ($this->resolveActiveStabile()?->denominazione ?? '')), trim((string) ($this->resolveActiveStabile()?->denominazione ?? '')),
$stabileCode, $stabileCode,
$stabileAddress, $stabileAddress,
]))), ]))),
'codice_univoco' => 'AF-QR-PREVIEW', 'codice_univoco' => 'AF-QR-PREVIEW',
'qr_payload' => (string) ($qrProfiles['xml_open'] ?? ''), 'qr_payload' => (string) ($qrProfiles['xml_open'] ?? ''),
'qr_payload_zip' => (string) ($qrProfiles['xml_zip'] ?? ''), 'qr_payload_zip' => (string) ($qrProfiles['xml_zip'] ?? ''),
'nfc_payload' => (string) ($qrProfiles['xml_nfc'] ?? ''), 'nfc_payload' => (string) ($qrProfiles['xml_nfc'] ?? ''),
'qr_payload_length' => (int) ($qrProfiles['xml_open_length'] ?? 0), 'qr_payload_length' => (int) ($qrProfiles['xml_open_length'] ?? 0),
'nfc_payload_length'=> (int) ($qrProfiles['xml_nfc_length'] ?? 0), 'nfc_payload_length' => (int) ($qrProfiles['xml_nfc_length'] ?? 0),
'amazon_url' => $amazonUrl, 'qr_markup' => $this->buildGenericLabelQrMarkup((string) ($qrProfiles['xml_open'] ?? '')),
'amazon_url' => $amazonUrl,
]; ];
$stableLines = $this->buildGenericLabelLineRows($payload); $stableLines = $this->buildGenericLabelLineRows($payload);
@ -991,6 +992,42 @@ public function getGenericLabelPreviewProperty(): array
]; ];
} }
private function buildGenericLabelQrMarkup(string $data): string
{
$data = trim($data);
if ($data === '') {
return '<span aria-hidden="true"></span>';
}
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, '<svg')) {
$svg = preg_replace('/<svg\b/', '<svg aria-label="QR anteprima etichetta" role="img" preserveAspectRatio="xMidYMid meet"', $svg, 1) ?: $svg;
return $svg;
}
}
return '<img alt="QR anteprima etichetta" src="https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=' . rawurlencode($data) . '">';
}
/** @return array<int, string> */ /** @return array<int, string> */
public function getAudienceGroupsProperty(): array public function getAudienceGroupsProperty(): array
{ {
@ -2011,13 +2048,13 @@ private function initializeScannerAcquisitionForm(): void
/** @return array<string, mixed> */ /** @return array<string, mixed> */
public function getScannerQrPreviewProperty(): array public function getScannerQrPreviewProperty(): array
{ {
$title = trim((string) ($this->scannerAcquisitionForm['titolo'] ?? '')); $title = trim((string) ($this->scannerAcquisitionForm['titolo'] ?? ''));
$supporto = trim((string) ($this->scannerAcquisitionForm['supporto_fisico'] ?? '')); $supporto = trim((string) ($this->scannerAcquisitionForm['supporto_fisico'] ?? ''));
$stabileId = (int) ($this->scannerAcquisitionForm['stabile_id'] ?? 0); $stabileId = (int) ($this->scannerAcquisitionForm['stabile_id'] ?? 0);
$categoria = trim((string) ($this->scannerAcquisitionForm['categoria'] ?? 'altro')); $categoria = trim((string) ($this->scannerAcquisitionForm['categoria'] ?? 'altro'));
$classe = trim((string) ($this->scannerAcquisitionForm['archivio_classe'] ?? 'altro')); $classe = trim((string) ($this->scannerAcquisitionForm['archivio_classe'] ?? 'altro'));
$busta = trim((string) ($this->scannerAcquisitionForm['busta_numero'] ?? '')); $busta = trim((string) ($this->scannerAcquisitionForm['busta_numero'] ?? ''));
$fascicolo = trim((string) ($this->scannerAcquisitionForm['fascicolo_numero'] ?? '')); $fascicolo = trim((string) ($this->scannerAcquisitionForm['fascicolo_numero'] ?? ''));
$contenitore = trim((string) ($this->scannerAcquisitionForm['contenitore_numero'] ?? '')); $contenitore = trim((string) ($this->scannerAcquisitionForm['contenitore_numero'] ?? ''));
$archiveCode = trim(implode('-', array_filter([ $archiveCode = trim(implode('-', array_filter([
@ -2042,12 +2079,12 @@ public function getScannerQrPreviewProperty(): array
]); ]);
return [ return [
'code' => $archiveCode, 'code' => $archiveCode,
'xml_open' => (string) ($profiles['xml_open'] ?? ''), 'xml_open' => (string) ($profiles['xml_open'] ?? ''),
'xml_zip' => (string) ($profiles['xml_zip'] ?? ''), 'xml_zip' => (string) ($profiles['xml_zip'] ?? ''),
'xml_nfc' => (string) ($profiles['xml_nfc'] ?? ''), 'xml_nfc' => (string) ($profiles['xml_nfc'] ?? ''),
'xml_open_length' => (int) ($profiles['xml_open_length'] ?? 0), '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),
]; ];
} }

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -14,6 +13,7 @@ class Assemblea extends Model
protected $fillable = [ protected $fillable = [
'stabile_id', 'stabile_id',
'tipo', 'tipo',
'data_assemblea',
'data_prima_convocazione', 'data_prima_convocazione',
'data_seconda_convocazione', 'data_seconda_convocazione',
'luogo', 'luogo',
@ -25,12 +25,13 @@ class Assemblea extends Model
]; ];
protected $casts = [ protected $casts = [
'data_prima_convocazione' => 'datetime', 'data_assemblea' => 'datetime',
'data_prima_convocazione' => 'datetime',
'data_seconda_convocazione' => 'datetime', 'data_seconda_convocazione' => 'datetime',
'data_convocazione' => 'date', 'data_convocazione' => 'date',
'data_svolgimento' => 'date', 'data_svolgimento' => 'date',
'created_at' => 'datetime', 'created_at' => 'datetime',
'updated_at' => 'datetime', 'updated_at' => 'datetime',
]; ];
/** /**
@ -70,7 +71,7 @@ public function presenze()
*/ */
public function verbale() 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) public function inviaConvocazioni($canali = ['email'], $userId)
{ {
$unitaImmobiliari = $this->stabile->unitaImmobiliari()->with('proprieta.soggetto')->get(); $unitaImmobiliari = $this->stabile->unitaImmobiliari()->with('proprieta.soggetto')->get();
$convocazioniInviate = 0; $convocazioniInviate = 0;
foreach ($unitaImmobiliari as $unita) { foreach ($unitaImmobiliari as $unita) {
foreach ($unita->proprieta as $proprieta) { foreach ($unita->proprieta as $proprieta) {
$soggetto = $proprieta->soggetto; $soggetto = $proprieta->soggetto;
foreach ($canali as $canale) { foreach ($canali as $canale) {
if ($this->verificaCanaleDisponibile($soggetto, $canale)) { if ($this->verificaCanaleDisponibile($soggetto, $canale)) {
$convocazione = $this->creaConvocazione($soggetto, $unita, $canale); $convocazione = $this->creaConvocazione($soggetto, $unita, $canale);
if ($this->inviaConvocazione($convocazione)) { if ($this->inviaConvocazione($convocazione)) {
$convocazioniInviate++; $convocazioniInviate++;
// Registra nel protocollo // Registra nel protocollo
$this->registraProtocollo($convocazione, $userId); $this->registraProtocollo($convocazione, $userId);
} }
@ -134,7 +135,7 @@ public function inviaConvocazioni($canali = ['email'], $userId)
// Aggiorna stato assemblea // Aggiorna stato assemblea
$this->update([ $this->update([
'stato' => 'convocata', 'stato' => 'convocata',
'data_convocazione' => now(), 'data_convocazione' => now(),
]); ]);
@ -148,12 +149,12 @@ private function verificaCanaleDisponibile($soggetto, $canale)
{ {
switch ($canale) { switch ($canale) {
case 'email': case 'email':
return !empty($soggetto->email); return ! empty($soggetto->email);
case 'pec': case 'pec':
return !empty($soggetto->pec); return ! empty($soggetto->pec);
case 'whatsapp': case 'whatsapp':
case 'telegram': case 'telegram':
return !empty($soggetto->telefono); return ! empty($soggetto->telefono);
default: default:
return true; return true;
} }
@ -165,12 +166,12 @@ private function verificaCanaleDisponibile($soggetto, $canale)
private function creaConvocazione($soggetto, $unita, $canale) private function creaConvocazione($soggetto, $unita, $canale)
{ {
return Convocazione::create([ return Convocazione::create([
'assemblea_id' => $this->id, 'assemblea_id' => $this->id,
'soggetto_id' => $soggetto->id_soggetto, 'soggetto_id' => $soggetto->id_soggetto,
'unita_immobiliare_id' => $unita->id_unita, 'unita_immobiliare_id' => $unita->id_unita,
'canale_invio' => $canale, 'canale_invio' => $canale,
'data_invio' => now(), 'data_invio' => now(),
'esito_invio' => 'inviato', 'esito_invio' => 'inviato',
]); ]);
} }
@ -181,10 +182,10 @@ private function inviaConvocazione($convocazione)
{ {
// Implementazione invio basata sul canale // Implementazione invio basata sul canale
// Qui si integrerebbe con servizi email, SMS, etc. // Qui si integrerebbe con servizi email, SMS, etc.
// Simula invio riuscito // Simula invio riuscito
$convocazione->update([ $convocazione->update([
'esito_invio' => 'consegnato', 'esito_invio' => 'consegnato',
'riferimento_invio' => 'REF-' . time(), 'riferimento_invio' => 'REF-' . time(),
]); ]);
@ -199,16 +200,16 @@ private function registraProtocollo($convocazione, $userId)
$numeroProtocollo = $this->generaNumeroProtocollo(); $numeroProtocollo = $this->generaNumeroProtocollo();
RegistroProtocollo::create([ RegistroProtocollo::create([
'numero_protocollo' => $numeroProtocollo, 'numero_protocollo' => $numeroProtocollo,
'tipo_comunicazione' => 'convocazione', 'tipo_comunicazione' => 'convocazione',
'assemblea_id' => $this->id, 'assemblea_id' => $this->id,
'soggetto_destinatario_id' => $convocazione->soggetto_id, 'soggetto_destinatario_id' => $convocazione->soggetto_id,
'oggetto' => "Convocazione Assemblea {$this->tipo} - {$this->data_prima_convocazione->format('d/m/Y')}", 'oggetto' => "Convocazione Assemblea {$this->tipo} - {$this->data_prima_convocazione->format('d/m/Y')}",
'canale' => $convocazione->canale_invio, 'canale' => $convocazione->canale_invio,
'data_invio' => $convocazione->data_invio, 'data_invio' => $convocazione->data_invio,
'esito' => $convocazione->esito_invio, 'esito' => $convocazione->esito_invio,
'riferimento_esterno' => $convocazione->riferimento_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() private function generaNumeroProtocollo()
{ {
$anno = date('Y'); $anno = date('Y');
$ultimoProtocollo = RegistroProtocollo::whereYear('created_at', $anno) $ultimoProtocollo = RegistroProtocollo::whereYear('created_at', $anno)
->orderBy('numero_protocollo', 'desc') ->orderBy('numero_protocollo', 'desc')
->first(); ->first();
@ -236,14 +237,14 @@ private function generaNumeroProtocollo()
*/ */
public function calcolaQuorum() public function calcolaQuorum()
{ {
$totaleMillesimi = $this->stabile->unitaImmobiliari()->sum('millesimi_proprieta'); $totaleMillesimi = $this->stabile->unitaImmobiliari()->sum('millesimi_proprieta');
$millesimiPresenti = $this->presenze()->sum('millesimi_rappresentati'); $millesimiPresenti = $this->presenze()->sum('millesimi_rappresentati');
return [ return [
'totale_millesimi' => $totaleMillesimi, 'totale_millesimi' => $totaleMillesimi,
'millesimi_presenti' => $millesimiPresenti, 'millesimi_presenti' => $millesimiPresenti,
'percentuale_presenza' => $totaleMillesimi > 0 ? ($millesimiPresenti / $totaleMillesimi) * 100 : 0, '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() public function generaReportPresenze()
{ {
$presenze = $this->presenze()->with(['soggetto', 'unitaImmobiliare'])->get(); $presenze = $this->presenze()->with(['soggetto', 'unitaImmobiliare'])->get();
$quorum = $this->calcolaQuorum(); $quorum = $this->calcolaQuorum();
return [ return [
'quorum' => $quorum, 'quorum' => $quorum,
'presenze' => $presenze, 'presenze' => $presenze,
'totale_presenti' => $presenze->where('tipo_presenza', 'presente')->count(), 'totale_presenti' => $presenze->where('tipo_presenza', 'presente')->count(),
'totale_delegati' => $presenze->where('tipo_presenza', 'delegato')->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(),
]; ];
} }
} }

View File

@ -171,4 +171,5 @@ protected function resolveSupplierCatalogUrl(): string
return TicketOperativi::getUrl(panel: 'admin-filament'); return TicketOperativi::getUrl(panel: 'admin-filament');
} }
} }

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('assemblee')) {
return;
}
Schema::create('assemblee', function (Blueprint $table): void {
$table->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');
}
};

View File

@ -120,6 +120,35 @@ @media (min-width: 64rem) {
@media (max-width: 63.998rem), @media (max-width: 63.998rem),
(hover: none) and (pointer: coarse) { (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 { .fi-body.fi-body-has-navigation .fi-topbar {
position: static !important; position: static !important;
top: auto !important; top: auto !important;

View File

@ -335,7 +335,9 @@
</div> </div>
<div class="flex flex-col items-center justify-end gap-3"> <div class="flex flex-col items-center justify-end gap-3">
<div class="{{ $qrClass }} rounded-xl border border-slate-300 bg-[linear-gradient(135deg,#0f172a_16%,transparent_16%,transparent_34%,#0f172a_34%,#0f172a_50%,transparent_50%,transparent_66%,#0f172a_66%,#0f172a_84%,transparent_84%)] bg-[length:16px_16px] bg-white"></div> <div class="{{ $qrClass }} flex items-center justify-center overflow-hidden rounded-xl border border-slate-300 bg-white p-1 text-slate-900 [&_img]:h-full [&_img]:w-full [&_img]:object-contain [&_svg]:h-full [&_svg]:w-full">
{!! data_get($preview, 'payload.qr_markup', '') !!}
</div>
<div class="text-center text-[10px] font-semibold uppercase tracking-[0.2em] text-slate-500">QR</div> <div class="text-center text-[10px] font-semibold uppercase tracking-[0.2em] text-slate-500">QR</div>
</div> </div>
</div> </div>
@ -374,7 +376,9 @@
</div> </div>
</div> </div>
<div class="{{ $isBottomQr ? 'flex justify-center pt-1' : ($qrPosition === 'right-top' ? 'flex items-start justify-end' : 'flex items-center justify-end') }} shrink-0"> <div class="{{ $isBottomQr ? 'flex justify-center pt-1' : ($qrPosition === 'right-top' ? 'flex items-start justify-end' : 'flex items-center justify-end') }} shrink-0">
<div class="{{ $qrClass }} rounded-xl border border-slate-300 bg-[linear-gradient(135deg,#0f172a_16%,transparent_16%,transparent_34%,#0f172a_34%,#0f172a_50%,transparent_50%,transparent_66%,#0f172a_66%,#0f172a_84%,transparent_84%)] bg-[length:16px_16px] bg-white"></div> <div class="{{ $qrClass }} flex items-center justify-center overflow-hidden rounded-xl border border-slate-300 bg-white p-1 text-slate-900 [&_img]:h-full [&_img]:w-full [&_img]:object-contain [&_svg]:h-full [&_svg]:w-full">
{!! data_get($preview, 'payload.qr_markup', '') !!}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -76,7 +76,6 @@ RSYNC_ARGS=(
--exclude=node_modules/ --exclude=node_modules/
--exclude=vendor/ --exclude=vendor/
--exclude=bootstrap/cache/ --exclude=bootstrap/cache/
--exclude=public/build/
) )
if [[ "$DRY_RUN" == "yes" ]]; then if [[ "$DRY_RUN" == "yes" ]]; then