From 478c0a120b9071e69a0635359721ae2cd7045d1d Mon Sep 17 00:00:00 2001 From: michele Date: Wed, 8 Apr 2026 19:13:29 +0000 Subject: [PATCH] Import UI, mobile workflows and ticket context fixes --- .../Pages/Gescon/ImportazioneArchivi.php | 235 ++++++++++++++++++ .../Pages/Gescon/RubricaUniversaleScheda.php | 66 +++++ .../Pages/Impostazioni/GoogleDashboard.php | 33 +++ app/Filament/Pages/Mobile/MobileDashboard.php | 33 +++ app/Filament/Pages/Supporto/TicketMobile.php | 46 ++-- app/Filament/Widgets/TicketMobileOverview.php | 102 ++++++++ .../Filament/AdminFilamentPanelProvider.php | 2 + .../Support/TicketAttachmentUploadService.php | 55 +++- .../gescon/importazione-archivi.blade.php | 144 ++++++----- .../importazione-archivi-linux.blade.php | 43 ++++ .../importazione-archivi-operativa.blade.php | 82 ++++++ .../importazione-archivi-windows.blade.php | 32 +++ .../impostazioni/google-dashboard.blade.php | 10 + .../filament/pages/mobile/dashboard.blade.php | 26 ++ .../pages/supporto/dashboard.blade.php | 14 +- .../pages/supporto/ticket-mobile.blade.php | 54 ++-- .../widgets/ticket-mobile-overview.blade.php | 34 +++ 17 files changed, 912 insertions(+), 99 deletions(-) create mode 100644 app/Filament/Pages/Impostazioni/GoogleDashboard.php create mode 100644 app/Filament/Pages/Mobile/MobileDashboard.php create mode 100644 app/Filament/Widgets/TicketMobileOverview.php create mode 100644 resources/views/filament/pages/gescon/partials/importazione-archivi-linux.blade.php create mode 100644 resources/views/filament/pages/gescon/partials/importazione-archivi-operativa.blade.php create mode 100644 resources/views/filament/pages/gescon/partials/importazione-archivi-windows.blade.php create mode 100644 resources/views/filament/pages/impostazioni/google-dashboard.blade.php create mode 100644 resources/views/filament/pages/mobile/dashboard.blade.php create mode 100644 resources/views/filament/widgets/ticket-mobile-overview.blade.php diff --git a/app/Filament/Pages/Gescon/ImportazioneArchivi.php b/app/Filament/Pages/Gescon/ImportazioneArchivi.php index dad1680..a50201d 100644 --- a/app/Filament/Pages/Gescon/ImportazioneArchivi.php +++ b/app/Filament/Pages/Gescon/ImportazioneArchivi.php @@ -678,6 +678,43 @@ public function aggiornaAnagraficheDaCondomin(): void ->send(); } + public function risincronizzaRelazioniImportate(): void + { + $user = Auth::user(); + if (! $user instanceof User) { + abort(403); + } + + $state = $this->getSchema('form')?->getState() ?? []; + $code = trim((string) ($state['stabile_code'] ?? '')); + if ($code === '') { + Notification::make()->title('Seleziona uno stabile')->danger()->send(); + return; + } + + $stabile = $this->resolveSelectedStabileByCode($code); + if (! $stabile instanceof Stabile) { + Notification::make()->title('Stabile locale non trovato')->body('Prima crea o apri lo stabile selezionato.')->danger()->send(); + return; + } + + $result = $this->runRelationSyncSequence($code, (int) $stabile->id, (bool) ($state['dry_run'] ?? false)); + if (! $result['ok']) { + Notification::make() + ->title('Risincronizzazione relazioni fallita') + ->body((string) ($result['message'] ?? 'Errore non specificato.')) + ->danger() + ->send(); + return; + } + + Notification::make() + ->title((bool) ($state['dry_run'] ?? false) ? 'Dry-run relazioni completato' : 'Risincronizzazione relazioni completata') + ->body((string) ($result['message'] ?? 'Operazione completata')) + ->success() + ->send(); + } + public function aggiornaFornitoriGescon(): void { $user = Auth::user(); @@ -1134,6 +1171,28 @@ public function eseguiImportSelezionato(EssentialImportService $service): void $outputs[] = trim((string) Artisan::output()); } + $shouldSyncRelations = ! empty($options['with_unita']) + || ! empty($options['with_soggetti']) + || ! empty($options['with_diritti']); + + if ($shouldSyncRelations) { + $relationSync = $this->runRelationSyncSequence($code, (int) $stabile->id, (bool) $options['dry']); + if (! $relationSync['ok']) { + Notification::make() + ->title('Import completato con errore nel re-sync relazioni') + ->body((string) ($relationSync['message'] ?? 'Errore non specificato.')) + ->danger() + ->send(); + return; + } + + foreach ((array) ($relationSync['outputs'] ?? []) as $syncOutput) { + if (is_string($syncOutput) && trim($syncOutput) !== '') { + $outputs[] = trim($syncOutput); + } + } + } + $tail = collect($outputs) ->filter() ->map(fn(string $o) => trim((string) collect(preg_split('/\R/', $o) ?: [])->last())) @@ -1250,6 +1309,100 @@ public function caricaAnniDaGenerale(): void ->send(); } + /** @return array */ + public function getCommandPreviewData(): array + { + $state = is_array($this->data) ? $this->data : []; + + $code = trim((string) ($state['stabile_code'] ?? '')); + if ($code !== '') { + $code = str_pad($code, 4, '0', STR_PAD_LEFT); + } + + $path = trim((string) ($state['path_root'] ?? '/mnt/gescon-archives/gescon')); + $annoDir = trim((string) ($state['anno'] ?? '')); + $years = implode(',', array_values(array_filter(array_map( + fn($value) => trim((string) $value), + (array) ($state['align_years_selected'] ?? []) + )))); + $waterYears = implode(',', array_values(array_filter(array_map( + fn($value) => trim((string) $value), + (array) ($state['align_water_years_selected'] ?? []) + )))); + $openYears = implode(',', array_values(array_filter(array_map( + fn($value) => trim((string) $value), + (array) ($state['align_open_years_selected'] ?? []) + )))); + + $adminId = (int) ($state['amministratore_id'] ?? 0); + $stabile = $this->resolveSelectedStabileByCode($code); + $stabileId = $stabile instanceof Stabile ? (int) $stabile->id : null; + + $fornitoriMdb = trim((string) ($state['fornitori_mdb'] ?? '')); + if ($fornitoriMdb === '' && $path !== '') { + $fornitoriMdb = rtrim($path, '/') . '/dbc/Fornitori.mdb'; + } + + $stabiliMdb = trim((string) ($state['stabili_mdb'] ?? '')); + if ($stabiliMdb === '' && $path !== '') { + $stabiliMdb = rtrim($path, '/') . '/dbc/Stabili.mdb'; + } + + $generaleMdb = ($path !== '' && $code !== '') + ? rtrim($path, '/') . '/' . $code . '/generale_stabile.mdb' + : ''; + $singoloMdb = ($path !== '' && $code !== '' && $annoDir !== '') + ? rtrim($path, '/') . '/' . $code . '/' . $annoDir . '/singolo_anno.mdb' + : ''; + + return [ + 'source_mode' => (string) ($state['source_mode'] ?? 'linux_server'), + 'stabile_code' => $code, + 'stabile_id' => $stabileId, + 'amministratore_id' => $adminId > 0 ? $adminId : null, + 'anno_dir' => $annoDir, + 'years_csv' => $years, + 'water_years_csv' => $waterYears, + 'open_years_csv' => $openYears, + 'paths' => [ + 'root' => $path, + 'stabili_mdb' => $stabiliMdb, + 'fornitori_mdb' => $fornitoriMdb, + 'generale_mdb' => $generaleMdb, + 'singolo_mdb' => $singoloMdb, + ], + 'commands' => [ + 'load_stabili' => $stabiliMdb !== '' + ? 'php artisan import:gescon-fornitori ' . max(1, $adminId) . ' --mdb="' . $fornitoriMdb . '" --preview --limit=5' + : null, + 'fornitori_sync' => ($adminId > 0 && $fornitoriMdb !== '') + ? 'php artisan gescon:sync-fornitori-legacy ' . $adminId . ' --mdb="' . $fornitoriMdb . '"' + : null, + 'allineamento' => ($code !== '' && $path !== '' && $years !== '') + ? 'php artisan gescon:import-align --stabile=' . $code . ' --path="' . $path . '" --years=' . $years . ' --water-years=' . ($waterYears !== '' ? $waterYears : $years) . ' --open-years=' . ($openYears !== '' ? $openYears : $years) + : null, + 'unita' => ($code !== '' && $path !== '' && $annoDir !== '') + ? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=unita --anno=' . $annoDir . ' --path="' . $path . '"' + : null, + 'soggetti' => ($code !== '' && $path !== '' && $annoDir !== '') + ? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=soggetti --anno=' . $annoDir . ' --path="' . $path . '"' + : null, + 'diritti' => ($code !== '' && $path !== '' && $annoDir !== '') + ? 'php artisan gescon:import-full --stabile=' . $code . ' --solo=diritti --anno=' . $annoDir . ' --path="' . $path . '"' + : null, + 'anagrafiche_sync' => ($code !== '') + ? 'php artisan gescon:auto-sync-anagrafiche --stabile=' . $code . ' --only=both --link-ruolo --include-comproprietari --apply' + : null, + 'ruoli_sync' => ($stabileId !== null) + ? 'php artisan gescon:sync-rubrica-ruoli --stabile=' . $stabileId . ' --apply' + : null, + 'repair_contacts' => ($code !== '') + ? 'php artisan gescon:repair-nominativi-contacts --stabile=' . $code . ' --apply' + : null, + ], + ]; + } + private function refreshAnniOptionsForStabile(string $code, string $root, ?Set $set = null, mixed $get = null): int { $code = trim($code); @@ -1348,6 +1501,88 @@ private function refreshAnniOptionsForStabile(string $code, string $root, ?Set $ return count($opts); } + private function resolveSelectedStabileByCode(?string $code): ?Stabile + { + $normalized = trim((string) $code); + if ($normalized === '') { + return null; + } + + $trimmed = ltrim($normalized, '0'); + + return Stabile::query() + ->where(function ($query) use ($normalized, $trimmed): void { + $query->where('codice_stabile', $normalized); + if ($trimmed !== '' && $trimmed !== $normalized) { + $query->orWhere('codice_stabile', $trimmed); + } + }) + ->first(); + } + + /** @return array{ok:bool,message:string,outputs:array} */ + private function runRelationSyncSequence(string $code, int $stabileId, bool $dryRun): array + { + $outputs = []; + $statuses = []; + + try { + $syncParams = [ + '--stabile' => $code, + '--limit' => 20000, + '--only' => 'both', + '--link-ruolo' => true, + '--include-comproprietari' => true, + ]; + if (! $dryRun) { + $syncParams['--apply'] = true; + } + + $statuses[] = Artisan::call('gescon:auto-sync-anagrafiche', $syncParams); + $outputs[] = trim((string) Artisan::output()); + + $ruoliParams = [ + '--stabile' => (string) $stabileId, + '--limit' => 5000, + ]; + if (! $dryRun) { + $ruoliParams['--apply'] = true; + } + + $statuses[] = Artisan::call('gescon:sync-rubrica-ruoli', $ruoliParams); + $outputs[] = trim((string) Artisan::output()); + + $repairParams = [ + '--stabile' => $code, + '--limit' => 5000, + ]; + if (! $dryRun) { + $repairParams['--apply'] = true; + } + + $statuses[] = Artisan::call('gescon:repair-nominativi-contacts', $repairParams); + $outputs[] = trim((string) Artisan::output()); + } catch (\Throwable $e) { + return [ + 'ok' => false, + 'message' => $e->getMessage(), + 'outputs' => $outputs, + ]; + } + + $tail = collect($outputs) + ->filter(fn($value) => is_string($value) && trim($value) !== '') + ->map(fn(string $output) => trim((string) collect(preg_split('/\R/', $output) ?: [])->last())) + ->filter(fn($value) => is_string($value) && $value !== '') + ->implode("\n"); + + return [ + 'ok' => collect($statuses)->every(fn($status) => (int) $status === 0), + 'message' => $tail !== '' ? $tail : 'Operazione completata.', + 'outputs' => $outputs, + ]; + } + private function firstNonEmpty(array $row, array $keys): mixed { foreach ($keys as $k) { diff --git a/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php b/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php index bec929a..62a9a0e 100644 --- a/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php +++ b/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php @@ -28,6 +28,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; @@ -1108,6 +1109,16 @@ protected function getHeaderActions(): array $this->startInlineEdit(); }), + Action::make('abilita_accesso_netgescon') + ->label('Abilita accesso') + ->icon('heroicon-o-key') + ->requiresConfirmation() + ->modalHeading('Abilita accesso NetGescon') + ->modalDescription('Crea o collega un utente per questo nominativo e invia il link impostazione password all\'email disponibile in rubrica.') + ->action(function (): void { + $this->abilitaAccessoRubrica(); + }), + Action::make('email_multiple') ->label('Email aggiuntive') ->icon('heroicon-o-envelope') @@ -1859,6 +1870,61 @@ private function getStudioCollaboratoreEmail(): ?string return null; } + private function getRubricaAccessEmail(): ?string + { + return $this->getStudioCollaboratoreEmail(); + } + + public function abilitaAccessoRubrica(): void + { + $email = $this->getRubricaAccessEmail(); + if ($email === null) { + Notification::make() + ->title('Email rubrica mancante') + ->warning() + ->body('Per abilitare l\'accesso serve almeno un indirizzo email principale o aggiuntivo attivo nella scheda rubrica.') + ->send(); + return; + } + + $name = trim((string) ($this->rubrica->nome_completo ?: $this->rubrica->ragione_sociale ?: 'Utente NetGescon')); + $user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first(); + + if (! $user) { + $user = User::query()->create([ + 'name' => $name, + 'email' => $email, + 'password' => Hash::make(Str::random(32)), + 'email_verified_at' => null, + 'is_active' => true, + 'registration_status' => 'approved', + ]); + } + + if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore', 'condomino'])) { + $user->assignRole('condomino'); + } elseif (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore']) && ! $user->hasRole('condomino')) { + $user->assignRole('condomino'); + } + + $status = Password::sendResetLink(['email' => $email]); + + if ($status === Password::RESET_LINK_SENT) { + Notification::make() + ->title('Accesso abilitato') + ->success() + ->body('Utente collegato alla rubrica. Link impostazione password inviato a ' . $email . '.') + ->send(); + return; + } + + Notification::make() + ->title('Utente creato o collegato, ma invio mail non confermato') + ->warning() + ->body('Controlla il mailer attivo. Stato Laravel: ' . __($status)) + ->send(); + } + public function canRequestClickToCall(): bool { $user = Auth::user(); diff --git a/app/Filament/Pages/Impostazioni/GoogleDashboard.php b/app/Filament/Pages/Impostazioni/GoogleDashboard.php new file mode 100644 index 0000000..7adeec0 --- /dev/null +++ b/app/Filament/Pages/Impostazioni/GoogleDashboard.php @@ -0,0 +1,33 @@ +hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } +} \ No newline at end of file diff --git a/app/Filament/Pages/Mobile/MobileDashboard.php b/app/Filament/Pages/Mobile/MobileDashboard.php new file mode 100644 index 0000000..acee57d --- /dev/null +++ b/app/Filament/Pages/Mobile/MobileDashboard.php @@ -0,0 +1,33 @@ +hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } +} \ No newline at end of file diff --git a/app/Filament/Pages/Supporto/TicketMobile.php b/app/Filament/Pages/Supporto/TicketMobile.php index 5650e92..dc15aa5 100644 --- a/app/Filament/Pages/Supporto/TicketMobile.php +++ b/app/Filament/Pages/Supporto/TicketMobile.php @@ -39,7 +39,7 @@ class TicketMobile extends Page protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-device-phone-mobile'; - protected static UnitEnum|string|null $navigationGroup = 'Supporto'; + protected static UnitEnum|string|null $navigationGroup = 'Mobile'; protected static ?int $navigationSort = 25; @@ -1131,14 +1131,14 @@ private function selectCallerRepresentative(Collection $group, int $selectedCall $representative = $selected instanceof RubricaUniversale ? $selected : $group - ->sortByDesc(fn(RubricaUniversale $match): array=> [ - (int) (! empty($match->amministratore_id)), - (int) (! empty($match->riferimento_stabile)), - (int) (! empty($match->riferimento_unita)), - (int) (($match->categoria ?? '') === 'condomino'), - -1 * (int) $match->id, - ]) - ->first(); + ->sortByDesc(fn(RubricaUniversale $match): array => [ + (int) (! empty($match->amministratore_id)), + (int) (! empty($match->riferimento_stabile)), + (int) (! empty($match->riferimento_unita)), + (int) (($match->categoria ?? '') === 'condomino'), + -1 * (int) $match->id, + ]) + ->first(); $duplicateCategories = $group ->pluck('categoria') @@ -1195,20 +1195,33 @@ public function getCallerStabileLabel(int $rubricaId): ?string */ private function resolveCallerTicketContext(?RubricaUniversale $caller, ?int $fallbackStabileId = null): array { - $stabile = $caller instanceof RubricaUniversale - ? $this->resolveCallerStabile($caller, $fallbackStabileId) - : ($fallbackStabileId ? Stabile::query()->find($fallbackStabileId) : null); + $fallbackStabile = $fallbackStabileId ? Stabile::query()->find($fallbackStabileId) : null; + + $callerStabile = $caller instanceof RubricaUniversale + ? $this->resolveCallerStabile($caller) + : null; $soggetto = $caller instanceof RubricaUniversale - ? $this->resolveCallerSoggetto($caller, $stabile?->id) + ? $this->resolveCallerSoggetto($caller, $callerStabile?->id) : null; $unita = $soggetto instanceof Soggetto - ? $this->resolveCallerUnita($soggetto, $stabile?->id, (string) ($caller?->riferimento_unita ?? '')) + ? $this->resolveCallerUnita($soggetto, $callerStabile?->id, (string) ($caller?->riferimento_unita ?? '')) : null; - if (! $stabile && $unita instanceof UnitaImmobiliare) { - $stabile = $unita->stabile; + $stabile = $unita instanceof UnitaImmobiliare + ? $unita->stabile + : $callerStabile; + + if (! $stabile && $soggetto instanceof Soggetto) { + $unita = $this->resolveCallerUnita($soggetto, null, (string) ($caller?->riferimento_unita ?? '')); + if ($unita instanceof UnitaImmobiliare) { + $stabile = $unita->stabile; + } + } + + if (! $stabile && ! $unita && ! $soggetto && $fallbackStabile instanceof Stabile) { + $stabile = $fallbackStabile; } return [ @@ -1261,6 +1274,7 @@ private function resolveCallerStabile(RubricaUniversale $caller, ?int $fallbackS return $fallbackStabileId ? Stabile::query()->find($fallbackStabileId) : null; } + private function resolveCallerSoggetto(RubricaUniversale $caller, ?int $stabileId = null): ?Soggetto { $queries = []; diff --git a/app/Filament/Widgets/TicketMobileOverview.php b/app/Filament/Widgets/TicketMobileOverview.php new file mode 100644 index 0000000..fbfc037 --- /dev/null +++ b/app/Filament/Widgets/TicketMobileOverview.php @@ -0,0 +1,102 @@ + */ + public array $ticketCounters = [ + 'open' => 0, + 'urgent' => 0, + 'closed' => 0, + 'all' => 0, + ]; + + public static function canView(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + + public function mount(): void + { + $user = Auth::user(); + if (! $user instanceof User) { + return; + } + + $stabileIds = StabileContext::accessibleStabili($user) + ->pluck('id') + ->map(fn($value) => (int) $value) + ->filter(fn(int $value) => $value > 0) + ->values() + ->all(); + + if ($stabileIds === []) { + return; + } + + $base = Ticket::query()->whereIn('stabile_id', $stabileIds); + + $this->ticketCounters = [ + 'open' => (clone $base)->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(), + 'urgent' => (clone $base)->where('priorita', 'Urgente')->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(), + 'closed' => (clone $base)->whereIn('stato', ['Risolto', 'Chiuso'])->count(), + 'all' => (clone $base)->count(), + ]; + } + + public function getDashboardFilamentUrl(): string + { + return route('filament.admin-filament.pages.dashboard'); + } + + public function getRubricaFilamentUrl(): string + { + return route('filament.admin-filament.pages.gescon.anagrafica.rubrica-universale'); + } + + public function getAdminMobileHubUrl(): string + { + return route('admin.mobile'); + } + + public function getCondominoMobileTicketUrl(): string + { + return route('condomino.tickets.mobile'); + } + + public function getTicketArchivioUrl(): string + { + return TicketGestione::getUrl(panel: 'admin-filament', parameters: ['status' => 'all']); + } + + public function getTicketMobileUrl(): string + { + return TicketMobile::getUrl(panel: 'admin-filament'); + } + + public function getGestioneTicketUrl(): string + { + return TicketGestione::getUrl(panel: 'admin-filament'); + } + + public function getHelpUrl(): string + { + return TicketMobileHelp::getUrl(panel: 'admin-filament'); + } +} \ No newline at end of file diff --git a/app/Providers/Filament/AdminFilamentPanelProvider.php b/app/Providers/Filament/AdminFilamentPanelProvider.php index 64749ce..c409f58 100644 --- a/app/Providers/Filament/AdminFilamentPanelProvider.php +++ b/app/Providers/Filament/AdminFilamentPanelProvider.php @@ -11,6 +11,7 @@ use App\Filament\Pages\Supporto\SupportoDashboard; use App\Filament\Pages\Supporto\TicketMobile; use App\Filament\Widgets\GoogleScadenziarioOverview; +use App\Filament\Widgets\TicketMobileOverview; use App\Models\Fornitore; use App\Models\FornitoreDipendente; use App\Models\User; @@ -116,6 +117,7 @@ public function panel(Panel $panel): Panel ->widgets([ AccountWidget::class, GoogleScadenziarioOverview::class, + TicketMobileOverview::class, ]) ->middleware([ EncryptCookies::class, diff --git a/app/Services/Support/TicketAttachmentUploadService.php b/app/Services/Support/TicketAttachmentUploadService.php index 6168784..83adf48 100644 --- a/app/Services/Support/TicketAttachmentUploadService.php +++ b/app/Services/Support/TicketAttachmentUploadService.php @@ -37,6 +37,8 @@ public function store(object $file, string $directory, array $options = []): arr $displayName = $originalName !== '' ? $originalName : ($storedName !== '' ? $storedName : 'file'); } + $sourceMetadata = $this->extractImageMetadataFromFileObject($file); + $mime = TicketAttachment::normalizeMimeType( method_exists($file, 'getClientMimeType') ? (string) $file->getClientMimeType() : '', $originalName, @@ -46,7 +48,10 @@ public function store(object $file, string $directory, array $options = []): arr $metadata = []; if (str_starts_with($mime, 'image/')) { - $metadata = $this->extractImageMetadata($path); + $metadata = $this->mergeImageMetadata( + $this->extractImageMetadata($path), + $sourceMetadata, + ); if ($this->shouldOptimizeImage($metadata, $options)) { $this->optimizeImage($path, $mime, $metadata); } @@ -80,6 +85,30 @@ private function shouldOptimizeImage(array $metadata, array $options = []): bool return ! $this->hasEmbeddedImageMetadata($metadata); } + private function mergeImageMetadata(array $storedMetadata, array $sourceMetadata): array + { + $merged = $storedMetadata; + + foreach ($sourceMetadata as $key => $value) { + if (is_array($value)) { + if ($value === []) { + continue; + } + + $merged[$key] = array_replace(is_array($merged[$key] ?? null) ? $merged[$key] : [], $value); + continue; + } + + if ($value === null || $value === '') { + continue; + } + + $merged[$key] = $value; + } + + return $merged; + } + private function hasEmbeddedImageMetadata(array $metadata): bool { return filled($metadata['exif_datetime'] ?? null) @@ -183,6 +212,30 @@ private function applyOrientation($source, int $orientation) private function extractImageMetadata(string $path): array { $absolutePath = Storage::disk('public')->path($path); + return $this->extractImageMetadataFromAbsolutePath($absolutePath); + } + + private function extractImageMetadataFromFileObject(object $file): array + { + $absolutePath = null; + + if (method_exists($file, 'getRealPath')) { + $absolutePath = $file->getRealPath(); + } + + if ((! is_string($absolutePath) || $absolutePath === '') && method_exists($file, 'path')) { + $absolutePath = $file->path(); + } + + if (! is_string($absolutePath) || $absolutePath === '' || ! is_file($absolutePath)) { + return []; + } + + return $this->extractImageMetadataFromAbsolutePath($absolutePath); + } + + private function extractImageMetadataFromAbsolutePath(string $absolutePath): array + { $metadata = []; $imageSize = @getimagesize($absolutePath); diff --git a/resources/views/filament/pages/gescon/importazione-archivi.blade.php b/resources/views/filament/pages/gescon/importazione-archivi.blade.php index 6460f68..2ed75b1 100644 --- a/resources/views/filament/pages/gescon/importazione-archivi.blade.php +++ b/resources/views/filament/pages/gescon/importazione-archivi.blade.php @@ -1,66 +1,98 @@ -
- @php - $sourceMode = data_get($data ?? [], 'source_mode', 'linux_server'); - @endphp + @php + $sourceMode = data_get($data ?? [], 'source_mode', 'linux_server'); + $preview = $this->getCommandPreviewData(); + $selectedLabel = data_get($stabiliOptions ?? [], data_get($preview, 'stabile_code', ''), data_get($preview, 'stabile_code', '')); + @endphp -
- Linux / Server - Windows / Upload -
+
+
+
+
+

Configurazione sorgente e import

+

Seleziona la sorgente, imposta stabile e anni, poi usa la tab operativa per import, aggiornamenti e re-sync.

+
+
+ Linux / Server + Windows / Upload + Tab operativa +
+
-
- {{ $this->getSchema('form') }} -
+
+
+
+
Stabile selezionato
+
{{ $selectedLabel !== '' ? $selectedLabel : 'Nessuno selezionato' }}
+
+
+
Amministratore
+
{{ data_get($preview, 'amministratore_id') ?: '-' }}
+
+
+
Anno legacy diretto
+
{{ data_get($preview, 'anno_dir') ?: '-' }}
+
+
+
-
- - Carica elenco stabili da Stabili.mdb - +
+ {{ $this->getSchema('form') }} +
+
- - Carica anni da generale_stabile.mdb - +
+
+
+ + + +
+
- - Crea / apri stabile selezionato - +
+
+ @include('filament.pages.gescon.partials.importazione-archivi-linux', ['preview' => $preview]) +
- - Importa solo dati selezionati - +
+ @include('filament.pages.gescon.partials.importazione-archivi-windows', ['preview' => $preview]) +
- - Import + allineamento (comando unico) - - - - Applica enrichment (banca/posta/rate/catasto) - - - - Aggiorna fornitori (GESCON) - - - - Refresh + Import TAG legacy (tutti i fornitori) - - - - Aggiorna anagrafiche (condomin → Rubrica) - - - - Enrichment massivo stabili - -
+
+ @include('filament.pages.gescon.partials.importazione-archivi-operativa', [ + 'preview' => $preview, + 'data' => $data ?? [], + 'stabiliOptions' => $stabiliOptions ?? [], + ]) +
+
+
diff --git a/resources/views/filament/pages/gescon/partials/importazione-archivi-linux.blade.php b/resources/views/filament/pages/gescon/partials/importazione-archivi-linux.blade.php new file mode 100644 index 0000000..a7cc0f3 --- /dev/null +++ b/resources/views/filament/pages/gescon/partials/importazione-archivi-linux.blade.php @@ -0,0 +1,43 @@ +
+
+
Percorsi attesi sul server Linux
+
+
+
Root archivio
+
{{ data_get($preview, 'paths.root', '/mnt/gescon-archives/gescon') }}
+
+
+
Stabili.mdb / Fornitori.mdb
+
{{ data_get($preview, 'paths.stabili_mdb', '-') }}
+{{ data_get($preview, 'paths.fornitori_mdb', '-') }}
+
+
+
generale_stabile.mdb
+
{{ data_get($preview, 'paths.generale_mdb', '-') }}
+
+
+
singolo_anno.mdb
+
{{ data_get($preview, 'paths.singolo_mdb', '-') }}
+
+
+
+ +
+
+
Verifica rapida archivio
+

Usa questi comandi se vuoi controllare a terminale che i file legacy siano leggibili dal server staging.

+
ls -la "{{ data_get($preview, 'paths.root', '/mnt/gescon-archives/gescon') }}"
+ls -la "{{ data_get($preview, 'paths.generale_mdb', '') }}"
+ls -la "{{ data_get($preview, 'paths.singolo_mdb', '') }}"
+ls -la "{{ data_get($preview, 'paths.fornitori_mdb', '') }}"
+
+ +
+
Comandi rapidi legacy
+

Comandi compatti già valorizzati con stabile, anni e path selezionati nella pagina.

+
{{ data_get($preview, 'commands.allineamento', 'Seleziona stabile e anni per generare il comando.') }}
+
+{{ data_get($preview, 'commands.fornitori_sync', 'Seleziona amministratore e Fornitori.mdb per generare il comando.') }}
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/gescon/partials/importazione-archivi-operativa.blade.php b/resources/views/filament/pages/gescon/partials/importazione-archivi-operativa.blade.php new file mode 100644 index 0000000..c466f2b --- /dev/null +++ b/resources/views/filament/pages/gescon/partials/importazione-archivi-operativa.blade.php @@ -0,0 +1,82 @@ +@php + $selectedLabel = data_get($stabiliOptions ?? [], data_get($preview, 'stabile_code', ''), data_get($preview, 'stabile_code', '')); +@endphp + +
+
+ Se importi `unità`, `soggetti` o `diritti`, la pagina ora lancia anche il re-sync di anagrafiche, ruoli rubrica e bonifica recapiti/contatti collegati allo stabile selezionato. +
+ +
+
+
1. Setup archivio
+
+ Carica elenco stabili + Carica anni + Crea / apri stabile +
+
Stabile attuale: {{ $selectedLabel !== '' ? $selectedLabel : 'nessuno' }}
+
+ +
+
2. Import / aggiornamento
+
+ Importa dati selezionati + Import + allineamento + Aggiorna fornitori +
+
Usa `Import + allineamento` come flusso principale; `Importa dati selezionati` serve per slice mirate.
+
+ +
+
3. Re-sync relazioni
+
+ Risincronizza relazioni + Sync anagrafiche + Refresh TAG fornitori +
+
Serve quando i dati locali esistono già e vuoi riallineare rubrica, ruoli e recapiti senza reimportare tutto.
+
+
+ +
+
+
Comandi pronti
+
+
+
Flusso principale
+
{{ data_get($preview, 'commands.allineamento', 'Seleziona stabile e anni per generare il comando.') }}
+
+
+
Fornitori
+
{{ data_get($preview, 'commands.fornitori_sync', 'Seleziona amministratore e Fornitori.mdb per generare il comando.') }}
+
+
+
+ +
+
Relazioni e riallineamento
+
+
+
Sync anagrafiche
+
{{ data_get($preview, 'commands.anagrafiche_sync', 'Seleziona uno stabile per generare il comando.') }}
+
+
+
Sync ruoli / repair contatti
+
{{ data_get($preview, 'commands.ruoli_sync', 'Crea o apri prima lo stabile locale per ottenere lo stabile_id.') }}
+
+{{ data_get($preview, 'commands.repair_contacts', 'Seleziona uno stabile per generare il comando.') }}
+
+
+
+
+ +
+
Import modulare
+
+
{{ data_get($preview, 'commands.unita', 'Serve stabile + anno legacy per generare il comando.') }}
+
{{ data_get($preview, 'commands.soggetti', 'Serve stabile + anno legacy per generare il comando.') }}
+
{{ data_get($preview, 'commands.diritti', 'Serve stabile + anno legacy per generare il comando.') }}
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/gescon/partials/importazione-archivi-windows.blade.php b/resources/views/filament/pages/gescon/partials/importazione-archivi-windows.blade.php new file mode 100644 index 0000000..3335c58 --- /dev/null +++ b/resources/views/filament/pages/gescon/partials/importazione-archivi-windows.blade.php @@ -0,0 +1,32 @@ +
+
+ In modalità Windows la pagina non legge i file direttamente dal PC: carichi ZIP, Stabili.mdb e Fornitori.mdb, poi il server staging li usa come sorgente temporanea. +
+ +
+
+
Cosa caricare
+
    +
  • ZIP dell'archivio GESCON con cartella `dbc` e cartelle stabili.
  • +
  • `Stabili.mdb` se vuoi caricare o rileggere l'elenco stabili da file singolo.
  • +
  • `Fornitori.mdb` se vuoi aggiornare solo i fornitori dal legacy.
  • +
+
+ +
+
PowerShell rapido
+

Se devi preparare uno ZIP dal PC Windows.

+
Compress-Archive -Path "C:\\GESCON\\*" -DestinationPath "C:\\temp\\gescon-archive.zip" -Force
+
+
+ +
+
Uso consigliato
+
    +
  1. Carica ZIP o file MDB nella parte alta della pagina.
  2. +
  3. Premi `Carica elenco stabili da Stabili.mdb`.
  4. +
  5. Seleziona stabile e anni, poi vai in `Operativa / Re-sync`.
  6. +
  7. Esegui prima `Import + allineamento` oppure `Aggiorna fornitori` a seconda del blocco dati che vuoi riallineare.
  8. +
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/impostazioni/google-dashboard.blade.php b/resources/views/filament/pages/impostazioni/google-dashboard.blade.php new file mode 100644 index 0000000..ea02023 --- /dev/null +++ b/resources/views/filament/pages/impostazioni/google-dashboard.blade.php @@ -0,0 +1,10 @@ + +
+
+
Google
+
Area dedicata per collegamento Google, sincronizzazioni e template Drive per stabile.
+
+ + @livewire(\App\Filament\Widgets\GoogleWorkspaceOverview::class) +
+
\ No newline at end of file diff --git a/resources/views/filament/pages/mobile/dashboard.blade.php b/resources/views/filament/pages/mobile/dashboard.blade.php new file mode 100644 index 0000000..e5e5e9e --- /dev/null +++ b/resources/views/filament/pages/mobile/dashboard.blade.php @@ -0,0 +1,26 @@ + +
+
+
Mobile
+
Contenitore unico per ticket, letture e prossimi flussi mobile ottimizzati.
+
+ +
+
+
Ticket Mobile
+
Inserimento rapido ticket dal telefono con foto, contesto chiamata e nominativo rubrica.
+ +
+ +
+
Letture servizi
+
Base amministrativa per le letture. Qui agganceremo il flusso dedicato lettura acqua con GPS, EXIF e accesso nominativo.
+ +
+
+
+
\ No newline at end of file diff --git a/resources/views/filament/pages/supporto/dashboard.blade.php b/resources/views/filament/pages/supporto/dashboard.blade.php index c5aafe3..45be049 100644 --- a/resources/views/filament/pages/supporto/dashboard.blade.php +++ b/resources/views/filament/pages/supporto/dashboard.blade.php @@ -5,6 +5,18 @@
Area tecnica per manutenzione, sincronizzazione Google e debug operativo.
- @livewire(\App\Filament\Widgets\GoogleWorkspaceOverview::class) +
+
+
+
Google e scadenzario
+
Il pannello completo Google ora ha una pagina dedicata sotto Impostazioni.
+
+ + Apri pagina Google + +
+
+ + @livewire(\App\Filament\Widgets\GoogleScadenziarioOverview::class) diff --git a/resources/views/filament/pages/supporto/ticket-mobile.blade.php b/resources/views/filament/pages/supporto/ticket-mobile.blade.php index c9934e0..a6ea3cb 100644 --- a/resources/views/filament/pages/supporto/ticket-mobile.blade.php +++ b/resources/views/filament/pages/supporto/ticket-mobile.blade.php @@ -56,20 +56,8 @@ @endif -
- -
-
Aperti: {{ $ticketCounters['open'] ?? 0 }}
-
Urgenti: {{ $ticketCounters['urgent'] ?? 0 }}
-
Chiusi: {{ $ticketCounters['closed'] ?? 0 }}
-
Totali: {{ $ticketCounters['all'] ?? 0 }}
-
+
+ Il riepilogo operativo Ticket Mobile ora e nella dashboard principale, cosi i link rapidi e lo stato ticket restano visibili appena entri in Filament.
@@ -292,11 +280,31 @@ url: (file.type || '').startsWith('image/') ? URL.createObjectURL(file) : null, })); }, + appendPreviews(target, items) { + const existing = new Map((this[target] || []).map((item) => [item.key, item])); + + items.forEach((item) => { + existing.set(item.key, item); + }); + + this[target] = Array.from(existing.values()); + }, setPreviews(event, kind) { const target = kind === 'camera' ? 'localCameraPreviews' : 'localAttachmentPreviews'; - this.revoke(this[target]); - this[target] = this.mapFiles(event?.target?.files || [], kind); + const batchId = Date.now(); + const mapped = Array.from(event?.target?.files || []).map((file, index) => ({ + key: `${kind}-${batchId}-${index}-${file.name}-${file.size}`, + name: file.name, + draftName: this.buildDraftName(file, this.draftSequence + index), + size: file.size || 0, + isImage: (file.type || '').startsWith('image/'), + url: (file.type || '').startsWith('image/') ? URL.createObjectURL(file) : null, + })); + this.appendPreviews(target, mapped); this.persist(); + if (event?.target) { + event.target.value = null; + } }, clearPreviews(kind = null) { if (kind === null || kind === 'camera') { @@ -415,6 +423,11 @@
@foreach($this->selectedUploads as $upload)
+ @if($upload['is_image'] && $upload['preview_url']) +
+ {{ $upload['name'] }} +
+ @endif
{{ $upload['is_image'] ? 'Immagine' : (strtoupper(pathinfo($upload['name'], PATHINFO_EXTENSION) ?: 'FILE')) }} @@ -450,15 +463,6 @@
-
-
Gestione operativa separata
-
Per lavorazione, presa in carico, risoluzione e chiusura usa la pagina dedicata.
- -
-
Suggerimento: aggiungi questa pagina ai preferiti del browser sul telefono per usarla come webapp rapida.
diff --git a/resources/views/filament/widgets/ticket-mobile-overview.blade.php b/resources/views/filament/widgets/ticket-mobile-overview.blade.php new file mode 100644 index 0000000..1f82f6a --- /dev/null +++ b/resources/views/filament/widgets/ticket-mobile-overview.blade.php @@ -0,0 +1,34 @@ + + + Ticket e Hub Mobile + Accesso rapido a inserimento ticket, archivio operativo e flusso mobile/webapp. + +
+
+ +
+
Aperti: {{ $ticketCounters['open'] ?? 0 }}
+
Urgenti: {{ $ticketCounters['urgent'] ?? 0 }}
+
Chiusi: {{ $ticketCounters['closed'] ?? 0 }}
+
Totali: {{ $ticketCounters['all'] ?? 0 }}
+
+
+ +
+
Gestione operativa separata
+
Per lavorazione, presa in carico, risoluzione e chiusura usa la pagina dedicata.
+ +
+
+
+
\ No newline at end of file