Compare commits

...

2 Commits

Author SHA1 Message Date
772a077e01 Launcher status and import operativa polish 2026-04-08 20:22:28 +00:00
c079737b5b Ticket visibility and dashboard cleanup 2026-04-08 19:57:14 +00:00
14 changed files with 430 additions and 56 deletions

View File

@ -5,6 +5,11 @@ # Changelog
## [Unreleased]
- Initial open-source release prep.
- Reworked Supporto > Aggiornamento Nodo to show local/remote commit dates, explicit pending commit counts, and a clear note that Git updates do not move storage archives or private uploaded data.
- Reorganized the Importazione Archivi operational tab so the action buttons come first, followed by the explanatory blocks and command fields.
- Removed active-stabile filtering from topbar avvisi/urgenze counters and from the Google ticket-operativi box so cross-stabile operational tickets stay visible.
- Removed the Google Workspace widget from the main/support dashboard, leaving the full panel only under Impostazioni > Google.
- Tightened Filament sidebar navigation spacing between main menu groups for denser top-level navigation.
- Added immediate local preview for Ticket Mobile photos and attachments before final send on phone.
- Split insurance handling out of Ticket Gestione into dedicated Tab 4 and added centralized Supporto claims list.
- Fixed supplier ticket-detail access by resolving both intervention-id and ticket-id safely in supplier context.

View File

@ -85,12 +85,20 @@ class Modifiche extends Page
public ?string $gitCurrentCommit = null;
public ?string $gitCurrentCommitDate = null;
public ?string $gitRemoteCommit = null;
public ?string $gitRemoteCommitDate = null;
public bool $gitWorkingTreeDirty = false;
public string $gitAheadBehind = '-';
public int $gitAheadCount = 0;
public int $gitBehindCount = 0;
public bool $gitSyncInProgress = false;
public ?string $gitSyncJobId = null;
@ -1051,12 +1059,18 @@ private function loadGitWorkspaceStatus(): void
{
$this->gitCurrentBranch = $this->runGitProcess(['rev-parse', '--abbrev-ref', 'HEAD']);
$this->gitCurrentCommit = $this->runGitProcess(['rev-parse', '--short', 'HEAD']);
$this->gitCurrentCommitDate = $this->runGitProcess(['show', '-s', '--date=format:%d/%m/%Y %H:%M', '--format=%cd', 'HEAD']);
$this->gitRemoteCommit = $this->runGitProcess(['rev-parse', '--short', 'refs/remotes/' . $this->gitRemote . '/' . $this->gitBranch]);
$this->gitRemoteCommitDate = $this->runGitProcess(['show', '-s', '--date=format:%d/%m/%Y %H:%M', '--format=%cd', 'refs/remotes/' . $this->gitRemote . '/' . $this->gitBranch]);
$this->gitWorkingTreeDirty = trim((string) ($this->runGitProcess(['status', '--porcelain']) ?? '')) !== '';
$this->gitAheadCount = 0;
$this->gitBehindCount = 0;
$aheadBehind = $this->runGitProcess(['rev-list', '--left-right', '--count', 'HEAD...refs/remotes/' . $this->gitRemote . '/' . $this->gitBranch]);
if (is_string($aheadBehind) && preg_match('/^(\d+)\s+(\d+)$/', trim($aheadBehind), $matches) === 1) {
$this->gitAheadBehind = 'ahead ' . $matches[1] . ' / behind ' . $matches[2];
$this->gitAheadCount = (int) $matches[1];
$this->gitBehindCount = (int) $matches[2];
$this->gitAheadBehind = 'ahead ' . $this->gitAheadCount . ' / behind ' . $this->gitBehindCount;
} else {
$this->gitAheadBehind = '-';
}

View File

@ -114,6 +114,9 @@ class TicketGestione extends Page
/** @var array<string,mixed>|null */
public ?array $attachmentPreview = null;
/** @var array<string,mixed>|null */
public ?array $attachmentMapPreview = null;
/** @var \Illuminate\Support\Collection<int, Ticket> */
public $tickets;
@ -320,6 +323,7 @@ public function openAttachmentPreview(int $attachmentId): void
$isImage = $attachment->isImage();
$isPdf = $attachment->isPdf();
$details = $this->getAttachmentDetails($attachment);
$this->attachmentPreview = [
'id' => (int) $attachment->id,
@ -329,12 +333,58 @@ public function openAttachmentPreview(int $attachmentId): void
'is_image' => $isImage,
'is_pdf' => $isPdf,
'mime' => $mime,
'details' => $details,
];
}
public function openAttachmentMapPreview(int $attachmentId): void
{
$ticket = $this->selectedTicket;
if (! $ticket) {
return;
}
$attachment = $ticket->attachments->firstWhere('id', $attachmentId);
if (! $attachment instanceof TicketAttachment) {
return;
}
$details = $this->getAttachmentDetails($attachment);
$gps = $details['gps'] ?? [];
$lat = $gps['lat'] ?? null;
$lng = $gps['lng'] ?? null;
if (! is_numeric($lat) || ! is_numeric($lng)) {
Notification::make()->title('Coordinate GPS non disponibili per questo allegato')->warning()->send();
return;
}
$this->attachmentMapPreview = [
'attachment_id' => (int) $attachment->id,
'name' => (string) ($attachment->original_file_name ?? 'Allegato'),
'label' => (string) ($gps['label'] ?? (number_format((float) $lat, 5, '.', '') . ', ' . number_format((float) $lng, 5, '.', ''))),
'maps_url' => (string) ($details['maps_url'] ?? ('https://maps.google.com/?q=' . $lat . ',' . $lng)),
'embed_url' => (string) ($details['maps_embed_url'] ?? ('https://maps.google.com/maps?q=' . $lat . ',' . $lng . '&z=17&output=embed')),
];
}
public function closeAttachmentPreview(): void
{
$this->attachmentPreview = null;
$this->attachmentMapPreview = null;
}
public function closeAttachmentMapPreview(): void
{
$this->attachmentMapPreview = null;
}
/**
* @return array<string,mixed>
*/
public function getAttachmentDetails(TicketAttachment $attachment): array
{
return $this->parseAttachmentDescription((string) ($attachment->description ?? ''));
}
public function getSelectedTicketProperty(): ?Ticket
@ -1276,6 +1326,95 @@ private function syncTicketAttachmentsArchive(Ticket $ticket): void
}
}
/**
* @return array<string,mixed>
*/
private function parseAttachmentDescription(string $description): array
{
$parts = preg_split('/\s*\|\s*/', trim($description)) ?: [];
$details = [
'primary_description' => '',
'gps' => null,
'maps_url' => null,
'maps_embed_url' => null,
'exif_datetime' => null,
'device' => null,
'file_label' => null,
'original_label' => null,
'other' => [],
'full_description' => trim($description),
];
foreach ($parts as $part) {
$part = trim((string) $part);
if ($part === '') {
continue;
}
if (str_starts_with($part, 'GPS:')) {
$gpsValue = trim((string) Str::after($part, 'GPS:'));
if (preg_match('/^\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/', $gpsValue, $matches) === 1) {
$lat = (float) $matches[1];
$lng = (float) $matches[2];
$details['gps'] = [
'lat' => $lat,
'lng' => $lng,
'label' => number_format($lat, 5, '.', '') . ', ' . number_format($lng, 5, '.', ''),
];
$details['maps_embed_url'] = 'https://maps.google.com/maps?q=' . $lat . ',' . $lng . '&z=17&output=embed';
}
continue;
}
if (str_starts_with($part, 'Maps:')) {
$details['maps_url'] = trim((string) Str::after($part, 'Maps:'));
continue;
}
if (str_starts_with($part, 'EXIF:')) {
$details['exif_datetime'] = trim((string) Str::after($part, 'EXIF:'));
continue;
}
if (str_starts_with($part, 'Device:')) {
$details['device'] = trim((string) Str::after($part, 'Device:'));
continue;
}
if (str_starts_with($part, 'File:')) {
$details['file_label'] = trim((string) Str::after($part, 'File:'));
continue;
}
if (str_starts_with($part, 'Orig:')) {
$details['original_label'] = trim((string) Str::after($part, 'Orig:'));
continue;
}
if ($details['primary_description'] === '') {
$details['primary_description'] = $part;
continue;
}
$details['other'][] = $part;
}
if ($details['maps_url'] === null && is_array($details['gps'])) {
$details['maps_url'] = 'https://maps.google.com/?q=' . $details['gps']['lat'] . ',' . $details['gps']['lng'];
}
if ($details['primary_description'] === '') {
$details['primary_description'] = $details['full_description'] !== ''
? $details['full_description']
: 'Allegato ticket';
}
return $details;
}
private function archiveTicketAttachmentDocument(Ticket $ticket, TicketAttachment $attachment): void
{
if (! Schema::hasTable('documenti')) {

View File

@ -673,15 +673,21 @@ private function loadOpenTickets(): void
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
$stabileIds = StabileContext::accessibleStabili($user)
->pluck('id')
->map(fn($value) => (int) $value)
->filter(fn(int $value) => $value > 0)
->values()
->all();
if ($stabileIds === []) {
$this->openTickets = [];
return;
}
$items = Ticket::query()
->with('stabile:id,denominazione')
->where('stabile_id', $stabileId)
->whereIn('stabile_id', $stabileIds)
->aperti()
->orderByDesc('created_at')
->limit(8)

View File

@ -10,7 +10,6 @@
use App\Filament\Pages\Gescon\RubricaUniversaleArchivio;
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;
@ -116,7 +115,6 @@ public function panel(Panel $panel): Panel
->discoverWidgets(in: app_path('Filament/Widgets'), for : 'App\\Filament\\Widgets')
->widgets([
AccountWidget::class,
GoogleScadenziarioOverview::class,
TicketMobileOverview::class,
])
->middleware([

View File

@ -1,3 +1,62 @@
# SESSION HANDOFF - 2026-04-08
## Contesto operativo attuale
- Stabile pilota: `0021`
- Obiettivo reale: import sicuro solo per stabili gia presenti, senza fallback vuoti o inventati su unita, nominativi, millesimi e voci di spesa.
- Regola utente vincolante:
- l'ultimo anno disponibile e la base autorevole dello stato corrente
- la chiave certa dell'unita e `condomin.cod_cond`
- il codice mnemonico operatore resta `Palazzina/Scala/Interno`, ma non deve diventare la chiave interna in fallback
- gli anni precedenti servono per costruire cronologia e variazioni, non per sovrascrivere il nominativo corrente
- niente righe vuote, `interno` inventati, o completamenti artificiali che rompano telefono, nominativi o allineamento unita
## Fix gia applicate e verificate
- `app/Console/Commands/ImportGesconFullPipeline.php`
- i placeholder `dettaglio_millesimi` a zero vengono creati solo con opzione esplicita `--fill-missing-millesimi`
- migliorato il fallback di aggancio `voci_spesa -> tabelle_millesimali` quando manca il match per anno gestione
- preflight staging reso sensibile a `cod_stabile + legacy_year`, non solo a `cod_stabile`
- `app/Console/Commands/NetgesconQaMillesimiSpeseCommand.php`
- nuovo comando QA per verificare allineamento unita, nominativi, millesimi, voci e archivi legacy
- `app/Filament/Pages/Strumenti/PostItGestione.php`
- raggruppamento post-it ora phone-first
- chiamate ai gruppi `601` e `603` non risposte da un interno reale trattate come missed in UI
- `app/Console/Commands/SmdrListenCommand.php`
- nuove chiamate SMDR su `601`/`603` marcate missed se non emerge un vero interno finale
## Evidenze gia verificate
- Per `0021`, il preflight prima caricava solo `legacy_year=0003`; dopo il fix ha caricato correttamente anche `0004`.
- Dry-run verificato:
- `condomin 0021/0004 = 214`
- `comproprietari 0021/0004 = 11`
- `voc_spe 0021/0004 = 73`
- Dopo il fix sulle voci, per `0021/0003` i codici legacy voce trovano tutti la corrispondente voce di dominio e non restano piu voci agganciate senza tabella default per i casi verificati.
- Sul call board, il numero `3388155553` collassa in un solo gruppo; esiste almeno un caso reale `601 -> 206` che resta correttamente answered.
## Punto aperto principale
La prossima modifica strutturale da fare e in `ImportGesconFullPipeline.php`, nella parte che ricostruisce unita/nominativi da staging:
1. partire dall'ultimo anno disponibile come snapshot corrente autorevole
2. identificare l'unita da `cod_cond` e usare `scala`, `interno`, `sub` solo come supporto di riconciliazione
3. evitare la creazione di unita artificiali o fallback tipo `A/0` se non esiste una base certa in archivio
4. aggiungere gli anni precedenti come variazioni storiche in `unita_immobiliare_nominativi`, senza riscrivere il corrente con dati piu vecchi
5. mantenere continuita contabile con collegamento a unita e ruolo storico `C`/`I` per rate emesse e incassi
## Anomalie note da non coprire con fallback
- In dominio esistono ancora 5 unita attive di `0021` senza nominativo corrente: `A/0`, `A/42`, `A/43`, `A/215`, `A/216`.
- Verifica fatta: queste unita non risultano in `gescon_import.condomin` per `0021/0003` nei dati controllati; non vanno auto-riempite con nominativi fittizi.
- Decisione da prendere nella prossima sessione: review/archiviazione/disattivazione guidata, non compensazione automatica.
## Prompt di ripartenza consigliato
"Riparti da docs/ai/SESSION_HANDOFF.md, docs/ai/TOPICS_INDEX.md, docs/ai/LATEST_2_CHATS_RECOVERY.md e dal topic docs/ai/topics/import-unita-cod-cond.md. Prima allinea la logica di import unita/nominativi all'ultimo anno autorevole basato su cod_cond, senza fallback inventati. Poi valida l'impatto contabile su rate e incassi."
---
# SESSION HANDOFF 2026-02-27
## Contesto operativo
@ -50,10 +109,12 @@ ## Prompt di ripartenza consigliato
## CHECKPOINT 2026-03-02 17:45:02
### Stato rapido
- Branch: main
- Ultimo commit: 49002765 2025-11-10 Auto-sync commit: 2025-11-10 12:40:03
### File modificati (git status --short)
```
M .gitignore
M .vscode/tasks.json
@ -10568,6 +10629,7 @@ ### File modificati (git status --short)
```
### Ultimi file toccati (max 20)
```
.gitignore
.vscode/tasks.json
@ -10592,15 +10654,19 @@ ### Ultimi file toccati (max 20)
```
### Bug trovati / verificati in questa sessione
-
-
### Decisioni prese
-
-
### Prossimo step operativo
-
-
### Prompt di ripartenza
Riparti da docs/ai/SESSION_HANDOFF.md, usa l'ultimo CHECKPOINT, continua dal "Prossimo step operativo" senza rifare analisi generale.
---
@ -10608,10 +10674,12 @@ ### Prompt di ripartenza
## CHECKPOINT 2026-03-02 18:33:07
### Stato rapido
- Branch: main
- Ultimo commit: 49002765 2025-11-10 Auto-sync commit: 2025-11-10 12:40:03
### File modificati (git status --short)
```
M .gitignore
M .vscode/tasks.json
@ -21126,6 +21194,7 @@ ### File modificati (git status --short)
```
### Ultimi file toccati (max 20)
```
.gitignore
.vscode/tasks.json
@ -21150,15 +21219,19 @@ ### Ultimi file toccati (max 20)
```
### Bug trovati / verificati in questa sessione
-
-
### Decisioni prese
-
-
### Prossimo step operativo
-
-
### Prompt di ripartenza
Riparti da docs/ai/SESSION_HANDOFF.md, usa l'ultimo CHECKPOINT, continua dal "Prossimo step operativo" senza rifare analisi generale.
---
@ -21166,10 +21239,12 @@ ### Prompt di ripartenza
## CHECKPOINT 2026-03-03 09:35:25
### Stato rapido
- Branch: main
- Ultimo commit: 49002765 2025-11-10 Auto-sync commit: 2025-11-10 12:40:03
### File modificati (git status --short)
```
M .gitignore
M .vscode/tasks.json
@ -31684,6 +31759,7 @@ ### File modificati (git status --short)
```
### Ultimi file toccati (max 20)
```
.gitignore
.vscode/tasks.json
@ -31708,15 +31784,19 @@ ### Ultimi file toccati (max 20)
```
### Bug trovati / verificati in questa sessione
-
-
### Decisioni prese
-
-
### Prossimo step operativo
-
-
### Prompt di ripartenza
Riparti da docs/ai/SESSION_HANDOFF.md, usa l'ultimo CHECKPOINT, continua dal "Prossimo step operativo" senza rifare analisi generale.
---
@ -31724,10 +31804,12 @@ ### Prompt di ripartenza
## CHECKPOINT 2026-03-03 10:26:27
### Stato rapido
- Branch: main
- Ultimo commit: 49002765 2025-11-10 Auto-sync commit: 2025-11-10 12:40:03
### File modificati (git status --short)
```
M .gitignore
M .vscode/tasks.json
@ -42242,6 +42324,7 @@ ### File modificati (git status --short)
```
### Ultimi file toccati (max 20)
```
.gitignore
.vscode/tasks.json
@ -42266,13 +42349,17 @@ ### Ultimi file toccati (max 20)
```
### Bug trovati / verificati in questa sessione
-
-
### Decisioni prese
-
-
### Prossimo step operativo
-
-
### Prompt di ripartenza
Riparti da docs/ai/SESSION_HANDOFF.md, usa l'ultimo CHECKPOINT, continua dal "Prossimo step operativo" senza rifare analisi generale.

View File

@ -35,4 +35,18 @@ .netgescon-footer {
width: 100% !important;
flex: 0 0 auto;
margin-top: auto;
}
.fi-sidebar-nav,
.fi-sidebar-nav-groups {
row-gap: calc(var(--spacing) * 4);
}
.fi-sidebar-group-btn,
.fi-sidebar-group-dropdown-trigger-btn {
padding-block: calc(var(--spacing) * 1.25);
}
.fi-sidebar-group-label {
line-height: calc(var(--spacing) * 5);
}

View File

@ -1,17 +1,22 @@
@php
$user = \Illuminate\Support\Facades\Auth::user();
$stabileId = null;
$stabileIds = [];
if ($user instanceof \App\Models\User) {
$stabileId = \App\Support\StabileContext::resolveActiveStabileId($user);
$stabileIds = \App\Support\StabileContext::accessibleStabili($user)
->pluck('id')
->map(fn($value) => (int) $value)
->filter(fn(int $value) => $value > 0)
->values()
->all();
}
$avvisiCount = 0;
$urgenzeCount = 0;
if ($stabileId) {
if ($stabileIds !== []) {
$baseQuery = \App\Models\Ticket::query()
->where('stabile_id', $stabileId)
->whereIn('stabile_id', $stabileIds)
->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']);
$urgenzeCount = (clone $baseQuery)->where('priorita', 'Urgente')->count();

View File

@ -5,11 +5,17 @@
$avvisi = 0;
if ($user instanceof \App\Models\User) {
$stabileId = \App\Support\StabileContext::resolveActiveStabileId($user);
if ($stabileId) {
$stabileIds = \App\Support\StabileContext::accessibleStabili($user)
->pluck('id')
->map(fn($value) => (int) $value)
->filter(fn(int $value) => $value > 0)
->values()
->all();
if ($stabileIds !== []) {
$base = \App\Models\Ticket::query()
->where('stabile_id', $stabileId)
->where('stato', 'Aperto');
->whereIn('stabile_id', $stabileIds)
->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']);
$urgenti = (clone $base)->where('priorita', 'Urgente')->count();
$avvisi = (clone $base)->where('priorita', '!=', 'Urgente')->count();

View File

@ -7,35 +7,42 @@
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.
</div>
<div class="rounded-xl border border-slate-200 bg-white p-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="text-sm font-semibold text-slate-900">Azioni operative</div>
<div class="mt-1 text-xs text-slate-600">Prima esegui i pulsanti di preparazione, poi il flusso di import, infine gli eventuali riallineamenti.</div>
</div>
<div class="text-xs text-slate-600">Stabile attuale: <span class="font-semibold text-slate-900">{{ $selectedLabel !== '' ? $selectedLabel : 'nessuno' }}</span></div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
<x-filament::button type="button" size="sm" wire:click="loadStabili">Carica elenco stabili</x-filament::button>
<x-filament::button type="button" size="sm" color="gray" wire:click="caricaAnniDaGenerale" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Carica anni</x-filament::button>
<x-filament::button type="button" size="sm" color="success" wire:click="creaStabileDaMdb" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Crea / apri stabile</x-filament::button>
<x-filament::button type="button" size="sm" color="gray" wire:click="eseguiImportSelezionato" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Importa dati selezionati</x-filament::button>
<x-filament::button type="button" size="sm" color="primary" wire:click="eseguiImportAllineamentoCompleto" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Import + allineamento</x-filament::button>
<x-filament::button type="button" size="sm" color="info" wire:click="aggiornaFornitoriGescon" :disabled="blank(data_get($data ?? [], 'path_root'))">Aggiorna fornitori</x-filament::button>
<x-filament::button type="button" size="sm" color="warning" wire:click="risincronizzaRelazioniImportate" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Risincronizza relazioni</x-filament::button>
<x-filament::button type="button" size="sm" color="info" wire:click="aggiornaAnagraficheDaCondomin" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Sync anagrafiche</x-filament::button>
<x-filament::button type="button" size="sm" color="warning" wire:click="refreshImportTagLegacyFornitoriGlobale" :disabled="blank(data_get($data ?? [], 'path_root'))">Refresh TAG fornitori</x-filament::button>
</div>
</div>
<div class="grid gap-4 xl:grid-cols-3">
<div class="rounded-xl border border-slate-200 bg-white p-4">
<div class="text-sm font-semibold text-slate-900">1. Setup archivio</div>
<div class="mt-3 flex flex-wrap gap-2">
<x-filament::button type="button" size="sm" wire:click="loadStabili">Carica elenco stabili</x-filament::button>
<x-filament::button type="button" size="sm" color="gray" wire:click="caricaAnniDaGenerale" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Carica anni</x-filament::button>
<x-filament::button type="button" size="sm" color="success" wire:click="creaStabileDaMdb" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Crea / apri stabile</x-filament::button>
</div>
<div class="mt-3 text-xs text-slate-600">Stabile attuale: {{ $selectedLabel !== '' ? $selectedLabel : 'nessuno' }}</div>
<div class="mt-2 text-xs text-slate-600">Carica elenco stabili, recupera gli anni dalla base generale e crea o riapri lo stabile locale prima dell'import vero e proprio.</div>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-4">
<div class="text-sm font-semibold text-slate-900">2. Import / aggiornamento</div>
<div class="mt-3 flex flex-wrap gap-2">
<x-filament::button type="button" size="sm" color="gray" wire:click="eseguiImportSelezionato" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Importa dati selezionati</x-filament::button>
<x-filament::button type="button" size="sm" color="primary" wire:click="eseguiImportAllineamentoCompleto" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Import + allineamento</x-filament::button>
<x-filament::button type="button" size="sm" color="info" wire:click="aggiornaFornitoriGescon" :disabled="blank(data_get($data ?? [], 'path_root'))">Aggiorna fornitori</x-filament::button>
</div>
<div class="mt-3 text-xs text-slate-600">Usa `Import + allineamento` come flusso principale; `Importa dati selezionati` serve per slice mirate.</div>
<div class="mt-2 text-xs text-slate-600">Usa Import + allineamento come flusso principale. Importa dati selezionati resta utile solo per slice mirate o recuperi puntuali.</div>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-4">
<div class="text-sm font-semibold text-slate-900">3. Re-sync relazioni</div>
<div class="mt-3 flex flex-wrap gap-2">
<x-filament::button type="button" size="sm" color="warning" wire:click="risincronizzaRelazioniImportate" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Risincronizza relazioni</x-filament::button>
<x-filament::button type="button" size="sm" color="info" wire:click="aggiornaAnagraficheDaCondomin" :disabled="empty($stabiliOptions) || blank(data_get($data ?? [], 'stabile_code'))">Sync anagrafiche</x-filament::button>
<x-filament::button type="button" size="sm" color="warning" wire:click="refreshImportTagLegacyFornitoriGlobale" :disabled="blank(data_get($data ?? [], 'path_root'))">Refresh TAG fornitori</x-filament::button>
</div>
<div class="mt-3 text-xs text-slate-600">Serve quando i dati locali esistono già e vuoi riallineare rubrica, ruoli e recapiti senza reimportare tutto.</div>
<div class="mt-2 text-xs text-slate-600">Serve quando i dati locali esistono già e vuoi riallineare rubrica, ruoli, recapiti e tag fornitori senza rilanciare l'intero import.</div>
</div>
</div>

View File

@ -82,12 +82,26 @@
<div class="mt-3 space-y-1 text-[11px] text-slate-600">
<div>Branch locale: <span class="font-mono font-medium">{{ $this->gitCurrentBranch ?? '-' }}</span></div>
<div>Commit locale: <span class="font-mono font-medium">{{ $this->gitCurrentCommit ?? '-' }}</span></div>
<div>Data commit locale: <span class="font-medium">{{ $this->gitCurrentCommitDate ?? '-' }}</span></div>
<div>Commit remoto: <span class="font-mono font-medium">{{ $this->gitRemoteCommit ?? '-' }}</span></div>
<div>Data commit remoto: <span class="font-medium">{{ $this->gitRemoteCommitDate ?? '-' }}</span></div>
<div>Tracking: <span class="font-medium">{{ $this->gitAheadBehind }}</span></div>
<div>Aggiornamenti da caricare sul nodo: <span class="font-semibold {{ (int) $this->pendingGitCommitCount > 0 ? 'text-amber-700' : 'text-emerald-700' }}">{{ (int) $this->pendingGitCommitCount }}</span></div>
<div>Commit locali da inviare: <span class="font-semibold {{ (int) $this->gitAheadCount > 0 ? 'text-sky-700' : 'text-slate-700' }}">{{ (int) $this->gitAheadCount }}</span></div>
<div>Working tree: <span class="font-medium {{ $this->gitWorkingTreeDirty ? 'text-rose-700' : 'text-emerald-700' }}">{{ $this->gitWorkingTreeDirty ? 'sporco' : 'pulito' }}</span></div>
<div>Ultima sync Git: <span class="font-medium">{{ $this->lastGitSyncAt ?? '-' }}</span></div>
<div>Ultimo update: <span class="font-medium">{{ $this->lastUpdateAt ?? '-' }}</span></div>
</div>
<div class="mt-3 rounded-lg border border-slate-200 bg-slate-50 p-3 text-[11px] text-slate-600">
Git sync e update codice aggiornano i file versionati nel repository. Gli archivi caricati manualmente dentro storage o cartelle private non vengono trasferiti da Git, salvo procedure dedicate di backup/sync dati.
</div>
@if(filled($this->pendingGitPreviewIssue))
<div class="mt-3 rounded-lg border border-amber-300 bg-amber-50 p-3 text-[11px] text-amber-900">
{{ $this->pendingGitPreviewIssue }}
</div>
@endif
</div>
</div>

View File

@ -16,7 +16,5 @@
</a>
</div>
</div>
@livewire(\App\Filament\Widgets\GoogleScadenziarioOverview::class)
</div>
</x-filament-panels::page>

View File

@ -332,6 +332,7 @@
<div class="mt-4 grid gap-3 sm:grid-cols-2">
@forelse($ticket->attachments as $a)
@php($details = $this->getAttachmentDetails($a))
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
<div class="aspect-[4/3] rounded-lg border bg-gray-50 p-2">
@if($a->isImage())
@ -347,10 +348,48 @@
</div>
<div class="mt-2 font-medium">{{ $a->original_file_name }}</div>
<div class="mt-1 text-gray-600">{{ $a->description ?: 'Nessuna descrizione' }}</div>
<div class="mt-1 text-gray-700">{{ $details['primary_description'] ?: 'Nessuna descrizione' }}</div>
<div class="mt-2 flex flex-wrap gap-1">
@if(!empty($details['gps']))
<span class="inline-flex items-center rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-semibold text-emerald-800">GPS</span>
@endif
@if(!empty($details['exif_datetime']))
<span class="inline-flex items-center rounded-full bg-sky-100 px-2 py-0.5 text-[10px] font-semibold text-sky-800">EXIF</span>
@endif
@if(!empty($details['device']))
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-700">{{ $details['device'] }}</span>
@endif
</div>
@if(!empty($details['gps']) || !empty($details['exif_datetime']) || !empty($details['file_label']) || !empty($details['original_label']) || !empty($details['other']))
<div class="mt-2 rounded-lg border border-slate-200 bg-slate-50 p-2 text-[11px] text-slate-700">
@if(!empty($details['gps']))
<div><span class="font-medium">Coordinate:</span> {{ $details['gps']['label'] }}</div>
@endif
@if(!empty($details['exif_datetime']))
<div><span class="font-medium">Data EXIF:</span> {{ $details['exif_datetime'] }}</div>
@endif
@if(!empty($details['device']))
<div><span class="font-medium">Dispositivo:</span> {{ $details['device'] }}</div>
@endif
@if(!empty($details['file_label']))
<div><span class="font-medium">Nome file:</span> {{ $details['file_label'] }}</div>
@endif
@if(!empty($details['original_label']))
<div><span class="font-medium">Origine:</span> {{ $details['original_label'] }}</div>
@endif
@foreach($details['other'] as $otherPart)
<div>{{ $otherPart }}</div>
@endforeach
</div>
@endif
<div class="mt-2 text-[11px] text-gray-500">{{ optional($a->created_at)->format('d/m/Y H:i') }}</div>
<button type="button" wire:click="openAttachmentPreview({{ (int) $a->id }})" class="mt-2 inline-flex items-center rounded-md bg-indigo-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-indigo-500">Visualizza in modal</button>
<div class="mt-2 flex flex-wrap gap-2">
<button type="button" wire:click="openAttachmentPreview({{ (int) $a->id }})" class="inline-flex items-center rounded-md bg-indigo-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-indigo-500">Visualizza allegato</button>
@if(!empty($details['gps']))
<button type="button" wire:click="openAttachmentMapPreview({{ (int) $a->id }})" class="inline-flex items-center rounded-md bg-emerald-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-emerald-500">Apri mappa</button>
@endif
</div>
</div>
@empty
<div class="text-xs text-gray-500">Nessun allegato.</div>
@ -545,8 +584,21 @@
<div class="flex items-center justify-between border-b px-4 py-3">
<div>
<div class="text-sm font-semibold">{{ $attachmentPreview['name'] ?? 'Allegato' }}</div>
@if(!empty($attachmentPreview['description']))
<div class="text-xs text-gray-500">{{ $attachmentPreview['description'] }}</div>
@if(!empty($attachmentPreview['details']['primary_description']))
<div class="text-xs text-gray-500">{{ $attachmentPreview['details']['primary_description'] }}</div>
@endif
@if(!empty($attachmentPreview['details']['gps']) || !empty($attachmentPreview['details']['exif_datetime']) || !empty($attachmentPreview['details']['device']))
<div class="mt-1 flex flex-wrap gap-2 text-[11px] text-slate-600">
@if(!empty($attachmentPreview['details']['gps']))
<span>GPS {{ $attachmentPreview['details']['gps']['label'] }}</span>
@endif
@if(!empty($attachmentPreview['details']['exif_datetime']))
<span>EXIF {{ $attachmentPreview['details']['exif_datetime'] }}</span>
@endif
@if(!empty($attachmentPreview['details']['device']))
<span>{{ $attachmentPreview['details']['device'] }}</span>
@endif
</div>
@endif
</div>
<div class="flex items-center gap-2">
@ -560,12 +612,21 @@
<button type="button" x-on:click="nextPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina successiva</button>
<button type="button" x-on:click="printPdf()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Stampa PDF</button>
@endif
@if(!empty($attachmentPreview['details']['gps']))
<button type="button" wire:click="openAttachmentMapPreview({{ (int) ($attachmentPreview['id'] ?? 0) }})" class="rounded-md bg-emerald-100 px-2 py-1 text-xs text-emerald-800 hover:bg-emerald-200">Google Maps</button>
@endif
<a href="{{ $attachmentPreview['url'] }}" target="_blank" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Apri in nuova scheda</a>
<button type="button" wire:click="closeAttachmentPreview" class="rounded-md bg-gray-100 px-2 py-1 text-xs hover:bg-gray-200">Chiudi</button>
</div>
</div>
<div class="h-[calc(92vh-64px)] overflow-auto p-4">
@if(!empty($attachmentPreview['details']['full_description']))
<div class="mb-3 rounded-lg border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700">
<div class="font-medium text-slate-900">Dettagli allegato</div>
<div class="mt-1 whitespace-pre-wrap">{{ $attachmentPreview['details']['full_description'] }}</div>
</div>
@endif
@if($attachmentPreview['is_image'] ?? false)
<div class="h-full overflow-auto rounded-lg bg-slate-50 p-4">
<div class="flex min-h-full min-w-full items-start justify-center">
@ -584,4 +645,24 @@
</div>
</div>
@endif
@if($attachmentMapPreview)
<div class="fixed inset-0 z-[60] flex items-center justify-center bg-black/70 p-4">
<div class="h-[88vh] w-full max-w-5xl rounded-xl bg-white shadow-2xl">
<div class="flex items-center justify-between border-b px-4 py-3">
<div>
<div class="text-sm font-semibold">Mappa allegato</div>
<div class="text-xs text-slate-500">{{ $attachmentMapPreview['name'] ?? 'Allegato' }} · {{ $attachmentMapPreview['label'] ?? '' }}</div>
</div>
<div class="flex items-center gap-2">
<a href="{{ $attachmentMapPreview['maps_url'] }}" target="_blank" rel="noopener noreferrer" class="rounded-md bg-emerald-100 px-2 py-1 text-xs text-emerald-800 hover:bg-emerald-200">Apri Google Maps</a>
<button type="button" wire:click="closeAttachmentMapPreview" class="rounded-md bg-gray-100 px-2 py-1 text-xs hover:bg-gray-200">Chiudi</button>
</div>
</div>
<div class="h-[calc(88vh-64px)] p-4">
<iframe src="{{ $attachmentMapPreview['embed_url'] }}" class="h-full w-full rounded-lg border" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
</div>
</div>
</div>
@endif
</x-filament-panels::page>

View File

@ -364,7 +364,7 @@
<div x-show="localCameraPreviews.length || localAttachmentPreviews.length" x-cloak class="mt-3 rounded-xl border border-sky-200 bg-sky-50 p-3">
<div class="text-xs font-semibold text-sky-900">Anteprima immediata sul telefono</div>
<div class="mt-1 text-[11px] text-sky-800">Le foto e gli allegati selezionati si vedono subito qui, prima dell'accodamento definitivo nella bozza ticket.</div>
<div class="mt-1 text-[11px] text-sky-800">Le foto e gli allegati selezionati si vedono subito qui mentre entrano in coda. Nella griglia sotto ogni miniatura resta nello stesso box della sua descrizione.</div>
<template x-if="localCameraPreviews.length">
<div class="mt-3">
@ -419,13 +419,13 @@
@if(count($this->selectedUploads) > 0)
<div class="md:col-span-2">
<div class="mb-2 text-xs font-semibold text-gray-700">Coda allegati pronta per il ticket</div>
<div class="mb-2 text-[11px] text-gray-500">Qui sotto restano solo i dati operativi del file. L'anteprima immagine resta nel riquadro sopra fino all'invio del ticket.</div>
<div class="mb-2 text-[11px] text-gray-500">Ogni allegato mantiene qui miniatura, protocollo e descrizione nello stesso riquadro, cosi puoi descrivere correttamente piu foto consecutive.</div>
<div class="grid gap-3 sm:grid-cols-2">
@foreach($this->selectedUploads as $upload)
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
@if($upload['is_image'] && $upload['preview_url'])
<div class="mb-2 aspect-[4/3] overflow-hidden rounded-lg border bg-slate-50">
<img src="{{ $upload['preview_url'] }}" alt="{{ $upload['name'] }}" class="h-full w-full object-cover" />
@if($upload['is_image'])
<div class="mb-2 aspect-[4/3] overflow-hidden rounded-lg border bg-slate-50" x-show="Boolean(@js($upload['preview_url']) || previewFor(@js($upload['name']), @js($upload['original_name'])))">
<img x-bind:src="@js($upload['preview_url']) || previewFor(@js($upload['name']), @js($upload['original_name']))" alt="{{ $upload['name'] }}" class="h-full w-full object-cover" />
</div>
@endif
<div class="flex flex-wrap items-center gap-2 rounded-lg border bg-gray-50 px-3 py-2">
@ -433,7 +433,7 @@
{{ $upload['is_image'] ? 'Immagine' : (strtoupper(pathinfo($upload['name'], PATHINFO_EXTENSION) ?: 'FILE')) }}
</span>
<div class="text-[11px] text-gray-600">
{{ $upload['is_image'] ? 'Anteprima visibile sopra' : 'Allegato pronto per l\'invio' }}
{{ $upload['is_image'] ? 'Miniatura agganciata a questa descrizione' : 'Allegato pronto per l\'invio' }}
</div>
</div>