Checkpoint Google supplier workflow cleanup

This commit is contained in:
michele 2026-04-12 08:06:52 +00:00
parent ce5451fe1a
commit c3efa1754b
30 changed files with 636 additions and 315 deletions

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\RubricaContattoCanale; use App\Models\RubricaContattoCanale;
@ -32,8 +31,8 @@ public function handle(): int
return self::FAILURE; return self::FAILURE;
} }
$apply = (bool) $this->option('apply'); $apply = (bool) $this->option('apply');
$limit = max(1, min((int) ($this->option('limit') ?? 5000), 50000)); $limit = max(1, min((int) ($this->option('limit') ?? 5000), 50000));
$adminId = (int) ($this->option('admin-id') ?: 0); $adminId = (int) ($this->option('admin-id') ?: 0);
$stabile = trim((string) ($this->option('stabile') ?? '')); $stabile = trim((string) ($this->option('stabile') ?? ''));
@ -60,11 +59,11 @@ public function handle(): int
} }
$stats = [ $stats = [
'analizzate' => 0, 'analizzate' => 0,
'rubriche_aggiornate' => 0, 'rubriche_aggiornate' => 0,
'canali_creati' => 0, 'canali_creati' => 0,
'canali_aggiornati' => 0, 'canali_aggiornati' => 0,
'saltate' => 0, 'saltate' => 0,
]; ];
foreach ($records as $rubrica) { foreach ($records as $rubrica) {
@ -103,21 +102,21 @@ public function handle(): int
} }
if ($existing instanceof RubricaContattoCanale) { if ($existing instanceof RubricaContattoCanale) {
$existing->etichetta = $channel['etichetta']; $existing->etichetta = $channel['etichetta'];
$existing->is_principale = $channel['is_principale']; $existing->is_principale = $channel['is_principale'];
$existing->meta = $channel['meta']; $existing->meta = $channel['meta'];
$existing->save(); $existing->save();
$stats['canali_aggiornati']++; $stats['canali_aggiornati']++;
continue; continue;
} }
RubricaContattoCanale::query()->create([ RubricaContattoCanale::query()->create([
'rubrica_id' => (int) $rubrica->id, 'rubrica_id' => (int) $rubrica->id,
'tipo' => $channel['tipo'], 'tipo' => $channel['tipo'],
'etichetta' => $channel['etichetta'], 'etichetta' => $channel['etichetta'],
'valore' => $channel['valore'], 'valore' => $channel['valore'],
'is_principale' => $channel['is_principale'], 'is_principale' => $channel['is_principale'],
'meta' => $channel['meta'], 'meta' => $channel['meta'],
]); ]);
$stats['canali_creati']++; $stats['canali_creati']++;
} }
@ -136,7 +135,7 @@ private function resolveScopedRubricaIds(string $stabileCode): array
return []; return [];
} }
$trimmed = ltrim($stabileCode, '0'); $trimmed = ltrim($stabileCode, '0');
$stabileIds = Stabile::query() $stabileIds = Stabile::query()
->where(function (Builder $query) use ($stabileCode, $trimmed): void { ->where(function (Builder $query) use ($stabileCode, $trimmed): void {
$query->where('codice_stabile', $stabileCode); $query->where('codice_stabile', $stabileCode);
@ -184,21 +183,21 @@ private function resolveScopedRubricaIds(string $stabileCode): array
private function extractStructuredContacts(RubricaUniversale $rubrica): array private function extractStructuredContacts(RubricaUniversale $rubrica): array
{ {
$emails = $this->uniqueEmails([ $emails = $this->uniqueEmails([
...$this->splitEmails((string) ($rubrica->email ?? '')), ...$this->splitEmails((string) ($rubrica->email ?? '')),
]); ]);
$pecs = $this->uniqueEmails([ $pecs = $this->uniqueEmails([
...$this->splitEmails((string) ($rubrica->pec ?? '')), ...$this->splitEmails((string) ($rubrica->pec ?? '')),
]); ]);
$mobilePhones = $this->normalizePhones($this->splitPhones((string) ($rubrica->telefono_cellulare ?? '')), 'cellulare'); $mobilePhones = $this->normalizePhones($this->splitPhones((string) ($rubrica->telefono_cellulare ?? '')), 'cellulare');
$officePhones = $this->normalizePhones($this->splitPhones((string) ($rubrica->telefono_ufficio ?? '')), 'telefono'); $officePhones = $this->normalizePhones($this->splitPhones((string) ($rubrica->telefono_ufficio ?? '')), 'telefono');
$homePhones = $this->normalizePhones($this->splitPhones((string) ($rubrica->telefono_casa ?? '')), 'telefono'); $homePhones = $this->normalizePhones($this->splitPhones((string) ($rubrica->telefono_casa ?? '')), 'telefono');
$primaryEmail = $emails[0] ?? $this->normalizeEmail((string) ($rubrica->email ?? '')); $primaryEmail = $emails[0] ?? $this->normalizeEmail((string) ($rubrica->email ?? ''));
$primaryPec = $pecs[0] ?? $this->normalizeEmail((string) ($rubrica->pec ?? '')); $primaryPec = $pecs[0] ?? $this->normalizeEmail((string) ($rubrica->pec ?? ''));
$primaryMobile = $mobilePhones[0]['value'] ?? $this->normalizePhoneDisplay((string) ($rubrica->telefono_cellulare ?? '')); $primaryMobile = $mobilePhones[0]['value'] ?? $this->normalizePhoneDisplay((string) ($rubrica->telefono_cellulare ?? ''));
$primaryOffice = $officePhones[0]['value'] ?? $this->normalizePhoneDisplay((string) ($rubrica->telefono_ufficio ?? '')); $primaryOffice = $officePhones[0]['value'] ?? $this->normalizePhoneDisplay((string) ($rubrica->telefono_ufficio ?? ''));
$primaryHome = $homePhones[0]['value'] ?? $this->normalizePhoneDisplay((string) ($rubrica->telefono_casa ?? '')); $primaryHome = $homePhones[0]['value'] ?? $this->normalizePhoneDisplay((string) ($rubrica->telefono_casa ?? ''));
$updates = []; $updates = [];
if ($primaryEmail !== $this->normalizeEmail((string) ($rubrica->email ?? ''))) { if ($primaryEmail !== $this->normalizeEmail((string) ($rubrica->email ?? ''))) {
@ -208,9 +207,9 @@ private function extractStructuredContacts(RubricaUniversale $rubrica): array
$updates['pec'] = $primaryPec; $updates['pec'] = $primaryPec;
} }
if ($primaryMobile !== $this->normalizePhoneDisplay((string) ($rubrica->telefono_cellulare ?? ''))) { if ($primaryMobile !== $this->normalizePhoneDisplay((string) ($rubrica->telefono_cellulare ?? ''))) {
$updates['telefono_cellulare'] = $primaryMobile; $updates['telefono_cellulare'] = $primaryMobile;
[$e164, $prefix, $national] = PhoneNumber::splitItaly($primaryMobile); [$e164, $prefix, $national] = PhoneNumber::splitItaly($primaryMobile);
$updates['prefisso_cellulare'] = $prefix; $updates['prefisso_cellulare'] = $prefix;
$updates['telefono_cellulare_nazionale'] = $national; $updates['telefono_cellulare_nazionale'] = $national;
} }
if ($primaryOffice !== $this->normalizePhoneDisplay((string) ($rubrica->telefono_ufficio ?? ''))) { if ($primaryOffice !== $this->normalizePhoneDisplay((string) ($rubrica->telefono_ufficio ?? ''))) {
@ -238,7 +237,7 @@ private function extractStructuredContacts(RubricaUniversale $rubrica): array
} }
return [ return [
'updates' => $updates, 'updates' => $updates,
'channels' => $this->deduplicateChannels($channels), 'channels' => $this->deduplicateChannels($channels),
]; ];
} }
@ -320,19 +319,19 @@ private function normalizePhones(array $values, string $defaultType): array
$result[] = [ $result[] = [
'value' => $normalizedValue, 'value' => $normalizedValue,
'type' => $type, 'type' => $type,
]; ];
} }
$unique = []; $unique = [];
$seen = []; $seen = [];
foreach ($result as $row) { foreach ($result as $row) {
$key = $row['type'] . '|' . PhoneNumber::normalizeDigits($row['value']); $key = $row['type'] . '|' . PhoneNumber::normalizeDigits($row['value']);
if (isset($seen[$key])) { if (isset($seen[$key])) {
continue; continue;
} }
$seen[$key] = true; $seen[$key] = true;
$unique[] = $row; $unique[] = $row;
} }
return $unique; return $unique;
@ -359,11 +358,11 @@ private function uniqueEmails(array $values): array
private function buildChannel(string $tipo, string $valore, string $etichetta, bool $isPrincipale, string $source): array private function buildChannel(string $tipo, string $valore, string $etichetta, bool $isPrincipale, string $source): array
{ {
return [ return [
'tipo' => $tipo, 'tipo' => $tipo,
'valore' => $valore, 'valore' => $valore,
'etichetta' => $etichetta, 'etichetta' => $etichetta,
'is_principale' => $isPrincipale, 'is_principale' => $isPrincipale,
'meta' => [ 'meta' => [
'source' => $source, 'source' => $source,
], ],
]; ];
@ -375,7 +374,7 @@ private function buildChannel(string $tipo, string $valore, string $etichetta, b
private function deduplicateChannels(array $channels): array private function deduplicateChannels(array $channels): array
{ {
$result = []; $result = [];
$seen = []; $seen = [];
foreach ($channels as $channel) { foreach ($channels as $channel) {
$key = $channel['tipo'] . '|'; $key = $channel['tipo'] . '|';
@ -393,9 +392,9 @@ private function deduplicateChannels(array $channels): array
} }
$seen[$key] = count($result); $seen[$key] = count($result);
$result[] = $channel; $result[] = $channel;
} }
return $result; return $result;
} }
} }

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Stabile; use App\Models\Stabile;
@ -29,15 +28,15 @@ public function handle(GmailTicketImportService $service): int
$posta = (array) Arr::get($stabile->configurazione_avanzata ?? [], 'posta', []); $posta = (array) Arr::get($stabile->configurazione_avanzata ?? [], 'posta', []);
$caselle = array_values(array_filter((array) Arr::get($posta, 'caselle', []), function ($mailbox): bool { $caselle = array_values(array_filter((array) Arr::get($posta, 'caselle', []), function ($mailbox): bool {
return is_array($mailbox) return is_array($mailbox)
&& (bool) ($mailbox['enabled'] ?? false) && (bool) ($mailbox['enabled'] ?? false)
&& strtolower(trim((string) ($mailbox['tipo'] ?? ''))) === 'gmail'; && strtolower(trim((string) ($mailbox['tipo'] ?? ''))) === 'gmail';
})); }));
$filter = trim((string) $this->option('casella')); $filter = trim((string) $this->option('casella'));
if ($filter !== '') { if ($filter !== '') {
$caselle = array_values(array_filter($caselle, static function (array $mailbox) use ($filter): bool { $caselle = array_values(array_filter($caselle, static function (array $mailbox) use ($filter): bool {
return strcasecmp((string) ($mailbox['label'] ?? ''), $filter) === 0 return strcasecmp((string) ($mailbox['label'] ?? ''), $filter) === 0
|| strcasecmp((string) ($mailbox['email'] ?? ''), $filter) === 0; || strcasecmp((string) ($mailbox['email'] ?? ''), $filter) === 0;
})); }));
} }
@ -80,4 +79,4 @@ private function resolveStabile(): ?Stabile
return null; return null;
} }
} }

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Amministratore; use App\Models\Amministratore;
@ -24,16 +23,16 @@ class GooglePushRubricaContactsCommand extends Command
public function handle(): int public function handle(): int
{ {
$dryRun = (bool) $this->option('dry-run'); $dryRun = (bool) $this->option('dry-run');
$limit = max(1, (int) $this->option('limit')); $limit = max(1, (int) $this->option('limit'));
$categorie = collect((array) $this->option('categoria')) $categorie = collect((array) $this->option('categoria'))
->map(fn ($v) => trim((string) $v)) ->map(fn($v) => trim((string) $v))
->filter(fn (string $v) => $v !== '') ->filter(fn(string $v) => $v !== '')
->values() ->values()
->all(); ->all();
$filterAdminIds = collect((array) $this->option('admin-id')) $filterAdminIds = collect((array) $this->option('admin-id'))
->map(fn ($v) => (int) $v) ->map(fn($v) => (int) $v)
->filter(fn (int $v) => $v > 0) ->filter(fn(int $v) => $v > 0)
->values() ->values()
->all(); ->all();
@ -51,11 +50,11 @@ public function handle(): int
$totalCreated = 0; $totalCreated = 0;
$totalUpdated = 0; $totalUpdated = 0;
$totalSkipped = 0; $totalSkipped = 0;
$totalErrors = 0; $totalErrors = 0;
foreach ($amministratori as $amministratore) { foreach ($amministratori as $amministratore) {
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []); $google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$oauth = Arr::get($google, 'oauth', []); $oauth = Arr::get($google, 'oauth', []);
if (! (bool) Arr::get($oauth, 'connected', false)) { if (! (bool) Arr::get($oauth, 'connected', false)) {
$this->line("Admin {$amministratore->id}: Google non collegato, salto."); $this->line("Admin {$amministratore->id}: Google non collegato, salto.");
@ -85,13 +84,13 @@ public function handle(): int
$created = 0; $created = 0;
$updated = 0; $updated = 0;
$skipped = 0; $skipped = 0;
$errors = 0; $errors = 0;
foreach ($records as $record) { foreach ($records as $record) {
$emails = $this->emailsForGoogle($record); $emails = $this->emailsForGoogle($record);
$phones = $this->phonesForGoogle($record); $phones = $this->phonesForGoogle($record);
$email = $emails[0]['value'] ?? ''; $email = $emails[0]['value'] ?? '';
$phone = $phones[0]['value'] ?? ''; $phone = $phones[0]['value'] ?? '';
$phoneNormalized = $this->normalizePhone($phone); $phoneNormalized = $this->normalizePhone($phone);
[$givenName, $familyName, $fullName] = $this->buildGoogleNameParts($record); [$givenName, $familyName, $fullName] = $this->buildGoogleNameParts($record);
@ -99,8 +98,8 @@ public function handle(): int
$payload = [ $payload = [
'names' => [[ 'names' => [[
'displayName' => $fullName, 'displayName' => $fullName,
'givenName' => $givenName, 'givenName' => $givenName,
'familyName' => $familyName, 'familyName' => $familyName,
]], ]],
]; ];
@ -146,7 +145,7 @@ public function handle(): int
]); ]);
if ($personResponse->status() === 401) { if ($personResponse->status() === 401) {
$token = $this->resolveAccessToken($amministratore, $google, $oauth, false, true) ?? $token; $token = $this->resolveAccessToken($amministratore, $google, $oauth, false, true) ?? $token;
$personResponse = Http::withToken($token) $personResponse = Http::withToken($token)
->acceptJson() ->acceptJson()
->get("https://people.googleapis.com/v1/{$resourceName}", [ ->get("https://people.googleapis.com/v1/{$resourceName}", [
@ -164,7 +163,7 @@ public function handle(): int
$etag = (string) Arr::get($personResponse->json(), 'metadata.sources.0.etag', ''); $etag = (string) Arr::get($personResponse->json(), 'metadata.sources.0.etag', '');
} }
$updatePayload = $payload; $updatePayload = $payload;
$updatePayload['resourceName'] = $resourceName; $updatePayload['resourceName'] = $resourceName;
if ($etag !== '') { if ($etag !== '') {
$updatePayload['etag'] = $etag; $updatePayload['etag'] = $etag;
@ -175,7 +174,7 @@ public function handle(): int
->patch("https://people.googleapis.com/v1/{$resourceName}:updateContact?updatePersonFields=names,emailAddresses,phoneNumbers", $updatePayload); ->patch("https://people.googleapis.com/v1/{$resourceName}:updateContact?updatePersonFields=names,emailAddresses,phoneNumbers", $updatePayload);
if ($updateResponse->status() === 401) { if ($updateResponse->status() === 401) {
$token = $this->resolveAccessToken($amministratore, $google, $oauth, false, true) ?? $token; $token = $this->resolveAccessToken($amministratore, $google, $oauth, false, true) ?? $token;
$updateResponse = Http::withToken($token) $updateResponse = Http::withToken($token)
->acceptJson() ->acceptJson()
->patch("https://people.googleapis.com/v1/{$resourceName}:updateContact?updatePersonFields=names,emailAddresses,phoneNumbers", $updatePayload); ->patch("https://people.googleapis.com/v1/{$resourceName}:updateContact?updatePersonFields=names,emailAddresses,phoneNumbers", $updatePayload);
@ -206,7 +205,7 @@ public function handle(): int
->post('https://people.googleapis.com/v1/people:createContact', $payload); ->post('https://people.googleapis.com/v1/people:createContact', $payload);
if ($createResponse->status() === 401) { if ($createResponse->status() === 401) {
$token = $this->resolveAccessToken($amministratore, $google, $oauth, false, true) ?? $token; $token = $this->resolveAccessToken($amministratore, $google, $oauth, false, true) ?? $token;
$createResponse = Http::withToken($token) $createResponse = Http::withToken($token)
->acceptJson() ->acceptJson()
->post('https://people.googleapis.com/v1/people:createContact', $payload); ->post('https://people.googleapis.com/v1/people:createContact', $payload);
@ -234,7 +233,7 @@ public function handle(): int
$totalCreated += $created; $totalCreated += $created;
$totalUpdated += $updated; $totalUpdated += $updated;
$totalSkipped += $skipped; $totalSkipped += $skipped;
$totalErrors += $errors; $totalErrors += $errors;
$mode = $dryRun ? 'dry-run' : 'apply'; $mode = $dryRun ? 'dry-run' : 'apply';
$this->info("Admin {$amministratore->id} ({$mode}): creati {$created}, aggiornati {$updated}, saltati {$skipped}, errori {$errors}."); $this->info("Admin {$amministratore->id} ({$mode}): creati {$created}, aggiornati {$updated}, saltati {$skipped}, errori {$errors}.");
@ -253,25 +252,25 @@ private function loadGoogleContactIndex(Amministratore $amministratore, array $g
{ {
$existingEmails = []; $existingEmails = [];
$existingPhones = []; $existingPhones = [];
$nextPageToken = null; $nextPageToken = null;
for ($page = 0; $page < 5; $page++) { for ($page = 0; $page < 5; $page++) {
$response = Http::withToken($token) $response = Http::withToken($token)
->acceptJson() ->acceptJson()
->get('https://people.googleapis.com/v1/people/me/connections', [ ->get('https://people.googleapis.com/v1/people/me/connections', [
'personFields' => 'emailAddresses,phoneNumbers', 'personFields' => 'emailAddresses,phoneNumbers',
'pageSize' => 500, 'pageSize' => 500,
'pageToken' => $nextPageToken, 'pageToken' => $nextPageToken,
]); ]);
if ($response->status() === 401) { if ($response->status() === 401) {
$token = $this->resolveAccessToken($amministratore, $google, $oauth, $dryRun, true) ?? $token; $token = $this->resolveAccessToken($amministratore, $google, $oauth, $dryRun, true) ?? $token;
$response = Http::withToken($token) $response = Http::withToken($token)
->acceptJson() ->acceptJson()
->get('https://people.googleapis.com/v1/people/me/connections', [ ->get('https://people.googleapis.com/v1/people/me/connections', [
'personFields' => 'emailAddresses,phoneNumbers', 'personFields' => 'emailAddresses,phoneNumbers',
'pageSize' => 500, 'pageSize' => 500,
'pageToken' => $nextPageToken, 'pageToken' => $nextPageToken,
]); ]);
} }
@ -314,7 +313,7 @@ private function loadGoogleContactIndex(Amministratore $amministratore, array $g
private function resolveAccessToken(Amministratore $amministratore, array $google, array $oauth, bool $dryRun, bool $forceRefresh = false): ?string private function resolveAccessToken(Amministratore $amministratore, array $google, array $oauth, bool $dryRun, bool $forceRefresh = false): ?string
{ {
$accessToken = trim((string) Arr::get($oauth, 'access_token', '')); $accessToken = trim((string) Arr::get($oauth, 'access_token', ''));
$refreshToken = trim((string) Arr::get($oauth, 'refresh_token', '')); $refreshToken = trim((string) Arr::get($oauth, 'refresh_token', ''));
if (! $forceRefresh && $accessToken !== '') { if (! $forceRefresh && $accessToken !== '') {
@ -325,10 +324,10 @@ private function resolveAccessToken(Amministratore $amministratore, array $googl
return null; return null;
} }
$settingsClientId = trim((string) Arr::get($google, 'client_id', '')); $settingsClientId = trim((string) Arr::get($google, 'client_id', ''));
$settingsClientSecret = trim((string) Arr::get($google, 'client_secret', '')); $settingsClientSecret = trim((string) Arr::get($google, 'client_secret', ''));
$configClientId = trim((string) config('services.google.client_id')); $configClientId = trim((string) config('services.google.client_id'));
$configClientSecret = trim((string) config('services.google.client_secret')); $configClientSecret = trim((string) config('services.google.client_secret'));
$credentialPairs = []; $credentialPairs = [];
if ($settingsClientId !== '' && $settingsClientSecret !== '') { if ($settingsClientId !== '' && $settingsClientSecret !== '') {
@ -346,14 +345,14 @@ private function resolveAccessToken(Amministratore $amministratore, array $googl
return null; return null;
} }
$payload = null; $payload = null;
$lastError = null; $lastError = null;
foreach ($credentialPairs as [$clientId, $clientSecret, $source]) { foreach ($credentialPairs as [$clientId, $clientSecret, $source]) {
$response = Http::asForm()->post('https://oauth2.googleapis.com/token', [ $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [
'client_id' => $clientId, 'client_id' => $clientId,
'client_secret' => $clientSecret, 'client_secret' => $clientSecret,
'refresh_token' => $refreshToken, 'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token', 'grant_type' => 'refresh_token',
]); ]);
if ($response->successful()) { if ($response->successful()) {
@ -361,7 +360,7 @@ private function resolveAccessToken(Amministratore $amministratore, array $googl
break; break;
} }
$message = (string) (Arr::get($response->json(), 'error_description') ?: Arr::get($response->json(), 'error.message') ?: 'refresh_failed'); $message = (string) (Arr::get($response->json(), 'error_description') ?: Arr::get($response->json(), 'error.message') ?: 'refresh_failed');
$lastError = "{$source}:{$response->status()} {$message}"; $lastError = "{$source}:{$response->status()} {$message}";
} }
@ -376,19 +375,19 @@ private function resolveAccessToken(Amministratore $amministratore, array $googl
} }
if (! $dryRun) { if (! $dryRun) {
$impostazioni = $amministratore->impostazioni ?? []; $impostazioni = $amministratore->impostazioni ?? [];
$impostazioniGoogle = Arr::get($impostazioni, 'google', []); $impostazioniGoogle = Arr::get($impostazioni, 'google', []);
$impostazioniOauth = Arr::get($impostazioniGoogle, 'oauth', []); $impostazioniOauth = Arr::get($impostazioniGoogle, 'oauth', []);
$impostazioniOauth['access_token'] = $newAccessToken; $impostazioniOauth['access_token'] = $newAccessToken;
$impostazioniOauth['expires_in'] = (int) Arr::get($payload, 'expires_in', 3600); $impostazioniOauth['expires_in'] = (int) Arr::get($payload, 'expires_in', 3600);
$impostazioniOauth['refreshed_at'] = Carbon::now()->toDateTimeString(); $impostazioniOauth['refreshed_at'] = Carbon::now()->toDateTimeString();
if (isset($payload['refresh_token']) && is_string($payload['refresh_token']) && $payload['refresh_token'] !== '') { if (isset($payload['refresh_token']) && is_string($payload['refresh_token']) && $payload['refresh_token'] !== '') {
$impostazioniOauth['refresh_token'] = $payload['refresh_token']; $impostazioniOauth['refresh_token'] = $payload['refresh_token'];
} }
$impostazioniGoogle['oauth'] = $impostazioniOauth; $impostazioniGoogle['oauth'] = $impostazioniOauth;
$impostazioni['google'] = $impostazioniGoogle; $impostazioni['google'] = $impostazioniGoogle;
$amministratore->impostazioni = $impostazioni; $amministratore->impostazioni = $impostazioni;
$amministratore->save(); $amministratore->save();
} }
@ -407,7 +406,7 @@ private function mobileForGoogle(RubricaUniversale $record): ?string
if ($candidate === '') { if ($candidate === '') {
$national = trim((string) ($record->telefono_cellulare_nazionale ?? '')); $national = trim((string) ($record->telefono_cellulare_nazionale ?? ''));
if ($national !== '') { if ($national !== '') {
$prefix = trim((string) ($record->prefisso_cellulare ?? '+39')); $prefix = trim((string) ($record->prefisso_cellulare ?? '+39'));
$candidate = $prefix . $national; $candidate = $prefix . $national;
} }
} }
@ -419,7 +418,7 @@ private function mobileForGoogle(RubricaUniversale $record): ?string
private function emailsForGoogle(RubricaUniversale $record): array private function emailsForGoogle(RubricaUniversale $record): array
{ {
$emails = []; $emails = [];
$seen = []; $seen = [];
$push = function (?string $value, string $type, string $formattedType) use (&$emails, &$seen): void { $push = function (?string $value, string $type, string $formattedType) use (&$emails, &$seen): void {
$email = strtolower(trim((string) $value)); $email = strtolower(trim((string) $value));
@ -427,9 +426,9 @@ private function emailsForGoogle(RubricaUniversale $record): array
return; return;
} }
$seen[$email] = true; $seen[$email] = true;
$emails[] = [ $emails[] = [
'value' => $email, 'value' => $email,
'type' => $type, 'type' => $type,
'formattedType' => $formattedType, 'formattedType' => $formattedType,
]; ];
}; };
@ -461,7 +460,7 @@ private function emailsForGoogle(RubricaUniversale $record): array
private function phonesForGoogle(RubricaUniversale $record): array private function phonesForGoogle(RubricaUniversale $record): array
{ {
$phones = []; $phones = [];
$seen = []; $seen = [];
$push = function (?string $value, string $type, string $formattedType) use (&$phones, &$seen): void { $push = function (?string $value, string $type, string $formattedType) use (&$phones, &$seen): void {
$phone = trim((string) $value); $phone = trim((string) $value);
@ -475,9 +474,9 @@ private function phonesForGoogle(RubricaUniversale $record): array
} }
$seen[$normalized] = true; $seen[$normalized] = true;
$phones[] = [ $phones[] = [
'value' => $phone, 'value' => $phone,
'type' => $type, 'type' => $type,
'formattedType' => $formattedType, 'formattedType' => $formattedType,
]; ];
}; };
@ -505,7 +504,7 @@ private function phonesForGoogle(RubricaUniversale $record): array
foreach ($channels as $channel) { foreach ($channels as $channel) {
$isMobile = (string) ($channel->tipo ?? '') === 'cellulare'; $isMobile = (string) ($channel->tipo ?? '') === 'cellulare';
$value = $isMobile ? (PhoneNumber::toE164Italy((string) ($channel->valore ?? '')) ?? (string) ($channel->valore ?? '')) : (string) ($channel->valore ?? ''); $value = $isMobile ? (PhoneNumber::toE164Italy((string) ($channel->valore ?? '')) ?? (string) ($channel->valore ?? '')) : (string) ($channel->valore ?? '');
$push($value, $isMobile ? 'mobile' : 'work', $isMobile ? 'Cellulare' : 'Telefono'); $push($value, $isMobile ? 'mobile' : 'work', $isMobile ? 'Cellulare' : 'Telefono');
} }
} }
@ -540,14 +539,14 @@ private function splitPhoneValues(string $value): array
*/ */
private function buildGoogleNameParts(RubricaUniversale $record): array private function buildGoogleNameParts(RubricaUniversale $record): array
{ {
$nome = trim((string) ($record->nome ?? '')); $nome = trim((string) ($record->nome ?? ''));
$cognome = trim((string) ($record->cognome ?? '')); $cognome = trim((string) ($record->cognome ?? ''));
// Legacy imports can contain "Nome Cognome" inside nome with empty cognome. // Legacy imports can contain "Nome Cognome" inside nome with empty cognome.
if ($cognome === '' && str_contains($nome, ' ')) { if ($cognome === '' && str_contains($nome, ' ')) {
$parts = preg_split('/\s+/', $nome) ?: []; $parts = preg_split('/\s+/', $nome) ?: [];
if (count($parts) > 1) { if (count($parts) > 1) {
$nome = trim((string) array_shift($parts)); $nome = trim((string) array_shift($parts));
$cognome = trim(implode(' ', $parts)); $cognome = trim(implode(' ', $parts));
} }
} }

View File

@ -424,16 +424,16 @@ private function syncAdditionalGoogleChannels(RubricaUniversale $rubrica, array
$tipo = str_contains($value, '@cert.') ? 'pec' : 'email'; $tipo = str_contains($value, '@cert.') ? 'pec' : 'email';
RubricaContattoCanale::query()->updateOrCreate( RubricaContattoCanale::query()->updateOrCreate(
[ [
'rubrica_id' => (int) $rubrica->id, 'rubrica_id' => (int) $rubrica->id,
'tipo' => $tipo, 'tipo' => $tipo,
'valore' => $value, 'valore' => $value,
'stabile_id' => null, 'stabile_id' => null,
'unita_immobiliare_id' => null, 'unita_immobiliare_id' => null,
], ],
[ [
'etichetta' => $tipo === 'pec' ? 'PEC Google' : 'Email Google', 'etichetta' => $tipo === 'pec' ? 'PEC Google' : 'Email Google',
'is_principale' => $index === 0, 'is_principale' => $index === 0,
'meta' => ['source' => 'google_workspace'], 'meta' => ['source' => 'google_workspace'],
] ]
); );
} }
@ -450,21 +450,21 @@ private function syncAdditionalGoogleChannels(RubricaUniversale $rubrica, array
continue; continue;
} }
$tipo = str_starts_with($digits, '3') || str_starts_with($digits, '39') ? 'cellulare' : 'telefono'; $tipo = str_starts_with($digits, '3') || str_starts_with($digits, '39') ? 'cellulare' : 'telefono';
$value = $tipo === 'cellulare' ? (PhoneNumber::toE164Italy($raw) ?? $raw) : $raw; $value = $tipo === 'cellulare' ? (PhoneNumber::toE164Italy($raw) ?? $raw) : $raw;
RubricaContattoCanale::query()->updateOrCreate( RubricaContattoCanale::query()->updateOrCreate(
[ [
'rubrica_id' => (int) $rubrica->id, 'rubrica_id' => (int) $rubrica->id,
'tipo' => $tipo, 'tipo' => $tipo,
'valore' => $value, 'valore' => $value,
'stabile_id' => null, 'stabile_id' => null,
'unita_immobiliare_id' => null, 'unita_immobiliare_id' => null,
], ],
[ [
'etichetta' => $tipo === 'cellulare' ? 'Cellulare Google' : 'Telefono Google', 'etichetta' => $tipo === 'cellulare' ? 'Cellulare Google' : 'Telefono Google',
'is_principale' => $index === 0, 'is_principale' => $index === 0,
'meta' => ['source' => 'google_workspace'], 'meta' => ['source' => 'google_workspace'],
] ]
); );
} }

View File

@ -312,7 +312,7 @@ private function resolveLocalFornitoreByLegacyIds(int $legacyId, string $legacyC
} }
return $normalizedRagione !== '' return $normalizedRagione !== ''
&& $this->normalizeComparableString((string) ($fornitore->ragione_sociale ?? '')) === $normalizedRagione; && $this->normalizeComparableString((string) ($fornitore->ragione_sociale ?? '')) === $normalizedRagione;
})->values(); })->values();
if ($filtered->count() === 1) { if ($filtered->count() === 1) {

View File

@ -101,10 +101,10 @@ public function handle(): int
return self::FAILURE; return self::FAILURE;
} }
$remoteRef = 'refs/remotes/' . $remote . '/' . $branch; $remoteRef = 'refs/remotes/' . $remote . '/' . $branch;
$docsRepoPath = $deployPath; $docsRepoPath = $deployPath;
$newCommit = $currentCommit; $newCommit = $currentCommit;
$cleanupWorktree = null; $cleanupWorktree = null;
$sourceRepoAligned = ! $deploySync; $sourceRepoAligned = ! $deploySync;
$sourceRepoMessage = null; $sourceRepoMessage = null;
@ -321,21 +321,21 @@ public function handle(): int
} }
$this->persistSummary([ $this->persistSummary([
'status' => 'completed', 'status' => 'completed',
'exit_code' => 0, 'exit_code' => 0,
'message' => 'Sync Git completata', 'message' => 'Sync Git completata',
'remote' => $remote, 'remote' => $remote,
'branch' => $branch, 'branch' => $branch,
'before_commit' => $currentCommit, 'before_commit' => $currentCommit,
'after_commit' => $newCommit, 'after_commit' => $newCommit,
'remote_commit' => $remoteCommit, 'remote_commit' => $remoteCommit,
'working_tree_dirty' => $isDirty, 'working_tree_dirty' => $isDirty,
'repo_path' => $repoPath, 'repo_path' => $repoPath,
'deploy_path' => $deployPath, 'deploy_path' => $deployPath,
'mode' => $deploySync ? 'external-repo-rsync' : 'in-place-git', 'mode' => $deploySync ? 'external-repo-rsync' : 'in-place-git',
'source_repo_aligned'=> $sourceRepoAligned, 'source_repo_aligned' => $sourceRepoAligned,
'source_repo_message'=> $sourceRepoMessage, 'source_repo_message' => $sourceRepoMessage,
'rubrica_legami_qa' => $rubricaQaExit === 0 ? 'completed' : 'warning', 'rubrica_legami_qa' => $rubricaQaExit === 0 ? 'completed' : 'warning',
]); ]);
$this->info('Sync Git completata con successo.'); $this->info('Sync Git completata con successo.');

View File

@ -248,7 +248,7 @@ public function handle(): int
$lineSuffix = $lineLabel !== '' ? ' - linea ' . $lineLabel : ''; $lineSuffix = $lineLabel !== '' ? ' - linea ' . $lineLabel : '';
$isMissedResponseGroup = $this->isMissedResponseGroupCall($parsed, $routing); $isMissedResponseGroup = $this->isMissedResponseGroupCall($parsed, $routing);
$caller = $this->resolveCallerDisplay($phone, $amministratoreId); $caller = $this->resolveCallerDisplay($phone, $amministratoreId);
ChiamataPostIt::query()->create([ ChiamataPostIt::query()->create([
'telefono' => $phone, 'telefono' => $phone,

View File

@ -162,6 +162,15 @@ public function getProdottiUrl(): string
return ProdottiCatalogo::getUrl(panel: 'admin-filament'); return ProdottiCatalogo::getUrl(panel: 'admin-filament');
} }
public function getImpostazioniUrl(): string
{
if ($this->fornitoreId) {
return ImpostazioniArchivio::getUrl(['fornitore' => $this->fornitoreId], panel: 'admin-filament');
}
return ImpostazioniArchivio::getUrl(panel: 'admin-filament');
}
protected function buildBaseQuery(Fornitore $fornitore, ?FornitoreDipendente $dipendente): Builder protected function buildBaseQuery(Fornitore $fornitore, ?FornitoreDipendente $dipendente): Builder
{ {
$query = TicketIntervento::query() $query = TicketIntervento::query()

View File

@ -2,6 +2,7 @@
namespace App\Filament\Pages\Fornitore; namespace App\Filament\Pages\Fornitore;
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext; use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
use App\Models\AssistenzaTecnorepairScheda;
use App\Models\Fornitore; use App\Models\Fornitore;
use App\Models\FornitoreDipendente; use App\Models\FornitoreDipendente;
use App\Models\TicketAttachment; use App\Models\TicketAttachment;
@ -95,13 +96,22 @@ public function refreshData(): void
$query = $this->buildBaseQuery($fornitore, $dipendente); $query = $this->buildBaseQuery($fornitore, $dipendente);
$this->applyStatusFilter($query, $this->status); $this->applyStatusFilter($query, $this->status);
$this->rows = $query $ticketRows = $query
->limit(100) ->limit(100)
->get() ->get()
->map(function (TicketIntervento $intervento): array { ->map(function (TicketIntervento $intervento): array {
$base = $this->buildInterventoRow($intervento); $base = $this->buildInterventoRow($intervento);
return array_merge($base, [ return array_merge($base, [
'row_type' => 'ticket',
'row_class' => $this->resolveTicketRowClass((string) $intervento->stato),
'badge_class' => $this->resolveTicketBadgeClass((string) $intervento->stato),
'numero' => (string) $intervento->ticket_id,
'cliente' => $base['contatto'],
'difetto' => $base['problema'],
'modello' => $base['apparato'],
'seriale' => '-',
'cod_prodotto' => '-',
'id' => (int) $intervento->id, 'id' => (int) $intervento->id,
'ticket_id' => (int) $intervento->ticket_id, 'ticket_id' => (int) $intervento->ticket_id,
'titolo' => (string) ($intervento->ticket->titolo ?? '-'), 'titolo' => (string) ($intervento->ticket->titolo ?? '-'),
@ -110,17 +120,34 @@ public function refreshData(): void
'operatore' => (string) $intervento->operatore_assegnato_label, 'operatore' => (string) $intervento->operatore_assegnato_label,
'tempo_minuti' => (int) ($intervento->tempo_minuti ?? 0), 'tempo_minuti' => (int) ($intervento->tempo_minuti ?? 0),
'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-', 'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
'sort_at' => optional($intervento->created_at)->format('Y-m-d H:i:s') ?: '1970-01-01 00:00:00',
'origine' => 'Ticket amministratore',
'url' => TicketInterventoScheda::getUrl(['record' => $intervento->id], panel: 'admin-filament'), 'url' => TicketInterventoScheda::getUrl(['record' => $intervento->id], panel: 'admin-filament'),
]); ]);
}) })
->all(); ->all();
$tecnorepairRows = $this->buildTecnorepairRows($fornitore);
$rows = array_merge($ticketRows, $tecnorepairRows);
usort($rows, function (array $left, array $right): int {
return strcmp((string) ($right['sort_at'] ?? ''), (string) ($left['sort_at'] ?? ''));
});
$this->rows = array_slice($rows, 0, 150);
$this->totals = [ $this->totals = [
'aperti' => (clone $this->buildBaseQuery($fornitore, $dipendente)) 'aperti' => (clone $this->buildBaseQuery($fornitore, $dipendente))
->whereNotIn('stato', ['chiuso']) ->whereNotIn('stato', ['chiuso'])
->count() + AssistenzaTecnorepairScheda::query()
->where('fornitore_id', (int) $fornitore->id)
->whereIn('status_bucket', ['open', 'waiting'])
->count(), ->count(),
'chiusi' => (clone $this->buildBaseQuery($fornitore, $dipendente)) 'chiusi' => (clone $this->buildBaseQuery($fornitore, $dipendente))
->whereIn('stato', ['chiuso', 'fatturato']) ->whereIn('stato', ['chiuso', 'fatturato'])
->count() + AssistenzaTecnorepairScheda::query()
->where('fornitore_id', (int) $fornitore->id)
->where('status_bucket', 'closed')
->count(), ->count(),
'fatturabili' => (clone $this->buildBaseQuery($fornitore, $dipendente)) 'fatturabili' => (clone $this->buildBaseQuery($fornitore, $dipendente))
->whereIn('stato', ['fatturabile', 'fatturato']) ->whereIn('stato', ['fatturabile', 'fatturato'])
@ -152,6 +179,15 @@ public function getContabilitaUrl(): string
return Contabilita::getUrl(panel: 'admin-filament'); return Contabilita::getUrl(panel: 'admin-filament');
} }
public function getImpostazioniUrl(): string
{
if ($this->fornitoreId && Auth::user()?->hasAnyRole(['super-admin', 'admin', 'amministratore'])) {
return ImpostazioniArchivio::getUrl(['fornitore' => $this->fornitoreId], panel: 'admin-filament');
}
return ImpostazioniArchivio::getUrl(panel: 'admin-filament');
}
public function openInterventoModal(int $interventoId): void public function openInterventoModal(int $interventoId): void
{ {
if (! $this->fornitoreId) { if (! $this->fornitoreId) {
@ -248,4 +284,79 @@ protected function applyStatusFilter(Builder $query, string $status): void
$query->whereNotIn('stato', ['chiuso']); $query->whereNotIn('stato', ['chiuso']);
} }
/**
* @return array<int, array<string, mixed>>
*/
protected function buildTecnorepairRows(Fornitore $fornitore): array
{
$query = AssistenzaTecnorepairScheda::query()
->where('fornitore_id', (int) $fornitore->id)
->orderByDesc('date_received')
->orderByDesc('updated_at');
if ($this->status === 'chiusi') {
$query->where('status_bucket', 'closed');
} elseif ($this->status === 'aperti') {
$query->whereIn('status_bucket', ['open', 'waiting']);
} elseif ($this->status === 'fatturabili') {
return [];
}
return $query
->limit(120)
->get()
->map(function (AssistenzaTecnorepairScheda $scheda): array {
return [
'row_type' => 'tecnorepair',
'row_class' => (string) $scheda->row_classes,
'badge_class' => (string) $scheda->status_badge_classes,
'numero' => (string) ($scheda->legacy_numero_scheda ?: $scheda->legacy_id),
'ingresso' => optional($scheda->date_received)->format('d/m/Y') ?: '-',
'cliente' => (string) ($scheda->customer_name ?: 'Cliente TecnoRepair'),
'contatto' => (string) ($scheda->customer_name ?: 'Cliente TecnoRepair'),
'telefono' => (string) ($scheda->customer_phone ?: $scheda->customer_phone_alt ?: ''),
'difetto' => (string) ($scheda->defect_reported ?: '-'),
'problema' => (string) ($scheda->defect_reported ?: '-'),
'apparato' => (string) ($scheda->product_model ?: '-'),
'modello' => (string) ($scheda->product_model ?: '-'),
'seriale' => (string) $scheda->serial_label,
'cod_prodotto' => (string) ($scheda->product_code ?: '-'),
'id' => (int) $scheda->id,
'ticket_id' => null,
'titolo' => (string) $scheda->display_title,
'stato' => (string) ($scheda->status_label ?: $scheda->status_bucket ?: '-'),
'stabile' => 'Archivio TecnoRepair',
'operatore' => (string) ($scheda->technician_name ?: $scheda->operator_name ?: '-'),
'tempo_minuti' => 0,
'updated_at' => optional($scheda->updated_at)->format('d/m/Y H:i') ?: '-',
'sort_at' => optional($scheda->date_received ?? $scheda->updated_at)->format('Y-m-d H:i:s') ?: '1970-01-01 00:00:00',
'origine' => 'TecnoRepair MDB',
'url' => null,
];
})
->all();
}
protected function resolveTicketRowClass(string $status): string
{
$status = strtolower(trim($status));
return match (true) {
in_array($status, ['chiuso', 'fatturato'], true) => 'bg-emerald-50/70 hover:bg-emerald-50',
in_array($status, ['fatturabile', 'verifica'], true) => 'bg-amber-50/70 hover:bg-amber-50',
default => 'bg-sky-50/60 hover:bg-sky-50',
};
}
protected function resolveTicketBadgeClass(string $status): string
{
$status = strtolower(trim($status));
return match (true) {
in_array($status, ['chiuso', 'fatturato'], true) => 'border-emerald-200 bg-emerald-50 text-emerald-800',
in_array($status, ['fatturabile', 'verifica'], true) => 'border-amber-200 bg-amber-50 text-amber-800',
default => 'border-sky-200 bg-sky-50 text-sky-800',
};
}
} }

View File

@ -317,13 +317,13 @@ public function collegaDipendenteDaRubrica(): void
$dipendente->created_by_user_id = Auth::id(); $dipendente->created_by_user_id = Auth::id();
} }
$dipendente->rubrica_id = $rubricaId; $dipendente->rubrica_id = $rubricaId;
$dipendente->nome = (string) ($rubrica->nome ?: $rubrica->ragione_sociale ?: 'Dipendente'); $dipendente->nome = (string) ($rubrica->nome ?: $rubrica->ragione_sociale ?: 'Dipendente');
$dipendente->cognome = trim((string) ($rubrica->cognome ?? '')) !== '' ? (string) $rubrica->cognome : null; $dipendente->cognome = trim((string) ($rubrica->cognome ?? '')) !== '' ? (string) $rubrica->cognome : null;
$dipendente->email = $email !== '' ? $email : null; $dipendente->email = $email !== '' ? $email : null;
$dipendente->telefono = trim((string) ($rubrica->telefono_cellulare ?: $rubrica->telefono_ufficio ?: $rubrica->telefono_casa)) ?: null; $dipendente->telefono = trim((string) ($rubrica->telefono_cellulare ?: $rubrica->telefono_ufficio ?: $rubrica->telefono_casa)) ?: null;
$dipendente->attivo = true; $dipendente->attivo = true;
$dipendente->updated_by_user_id = Auth::id(); $dipendente->updated_by_user_id = Auth::id();
$dipendente->save(); $dipendente->save();
$this->resetDipendenteRubricaSelection(); $this->resetDipendenteRubricaSelection();

View File

@ -28,6 +28,6 @@ public static function canAccess(): bool
$user = Auth::user(); $user = Auth::user();
return $user instanceof User return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
} }
} }

View File

@ -28,6 +28,6 @@ public static function canAccess(): bool
$user = Auth::user(); $user = Auth::user();
return $user instanceof User return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
} }
} }

View File

@ -4,10 +4,10 @@
use App\Models\ChiamataPostIt; use App\Models\ChiamataPostIt;
use App\Models\Fornitore; use App\Models\Fornitore;
use App\Models\RubricaUniversale; use App\Models\RubricaUniversale;
use App\Services\Anagrafiche\RubricaPhoneLookupService;
use App\Models\Stabile; use App\Models\Stabile;
use App\Models\Ticket; use App\Models\Ticket;
use App\Models\User; use App\Models\User;
use App\Services\Anagrafiche\RubricaPhoneLookupService;
use App\Support\PhoneNumber; use App\Support\PhoneNumber;
use App\Support\StabileContext; use App\Support\StabileContext;
use BackedEnum; use BackedEnum;

View File

@ -23,11 +23,11 @@ class PostItGestione extends Page
{ {
/** @var array<string,string> */ /** @var array<string,string> */
private const SMDR_PUBLIC_LINE_FALLBACKS = [ private const SMDR_PUBLIC_LINE_FALLBACKS = [
'1' => '0639731100', '1' => '0639731100',
'01' => '0639731100', '01' => '0639731100',
'0001' => '0639731100', '0001' => '0639731100',
'3' => '0688812703', '3' => '0688812703',
'03' => '0688812703', '03' => '0688812703',
'0003' => '0688812703', '0003' => '0688812703',
]; ];

View File

@ -1087,17 +1087,17 @@ private function loadGitWorkspaceStatus(): void
{ {
$repoPath = $this->resolveGitWorkspacePath(); $repoPath = $this->resolveGitWorkspacePath();
$this->gitWorkspacePath = $repoPath; $this->gitWorkspacePath = $repoPath;
$this->gitCurrentBranch = $this->runGitProcessAt($repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']); $this->gitCurrentBranch = $this->runGitProcessAt($repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
$this->gitSourceCommit = $this->runGitProcessAt($repoPath, ['rev-parse', '--short', 'HEAD']); $this->gitSourceCommit = $this->runGitProcessAt($repoPath, ['rev-parse', '--short', 'HEAD']);
$this->gitSourceCommitDate = $this->runGitProcessAt($repoPath, ['show', '-s', '--date=format:%d/%m/%Y %H:%M', '--format=%cd', 'HEAD']); $this->gitSourceCommitDate = $this->runGitProcessAt($repoPath, ['show', '-s', '--date=format:%d/%m/%Y %H:%M', '--format=%cd', 'HEAD']);
$this->gitCurrentCommit = $this->gitSourceCommit; $this->gitCurrentCommit = $this->gitSourceCommit;
$this->gitCurrentCommitDate = $this->gitSourceCommitDate; $this->gitCurrentCommitDate = $this->gitSourceCommitDate;
$this->gitRemoteCommit = $this->runGitProcessAt($repoPath, ['rev-parse', '--short', 'refs/remotes/' . $this->gitRemote . '/' . $this->gitBranch]); $this->gitRemoteCommit = $this->runGitProcessAt($repoPath, ['rev-parse', '--short', 'refs/remotes/' . $this->gitRemote . '/' . $this->gitBranch]);
$this->gitRemoteCommitDate = $this->runGitProcessAt($repoPath, ['show', '-s', '--date=format:%d/%m/%Y %H:%M', '--format=%cd', 'refs/remotes/' . $this->gitRemote . '/' . $this->gitBranch]); $this->gitRemoteCommitDate = $this->runGitProcessAt($repoPath, ['show', '-s', '--date=format:%d/%m/%Y %H:%M', '--format=%cd', 'refs/remotes/' . $this->gitRemote . '/' . $this->gitBranch]);
$this->gitWorkingTreeDirty = trim((string) ($this->runGitProcessAt($repoPath, ['status', '--porcelain']) ?? '')) !== ''; $this->gitWorkingTreeDirty = trim((string) ($this->runGitProcessAt($repoPath, ['status', '--porcelain']) ?? '')) !== '';
$this->gitAheadCount = 0; $this->gitAheadCount = 0;
$this->gitBehindCount = 0; $this->gitBehindCount = 0;
$this->gitStatusUsesSyncSummary = false; $this->gitStatusUsesSyncSummary = false;
$currentRef = 'HEAD'; $currentRef = 'HEAD';
@ -1106,11 +1106,11 @@ private function loadGitWorkspaceStatus(): void
if (is_file($summaryPath)) { if (is_file($summaryPath)) {
$summary = json_decode((string) @file_get_contents($summaryPath), true); $summary = json_decode((string) @file_get_contents($summaryPath), true);
if (is_array($summary)) { if (is_array($summary)) {
$summaryMode = (string) ($summary['mode'] ?? ''); $summaryMode = (string) ($summary['mode'] ?? '');
$summaryStatus = (string) ($summary['status'] ?? ''); $summaryStatus = (string) ($summary['status'] ?? '');
$summaryCurrentCommit = isset($summary['after_commit']) ? (string) $summary['after_commit'] : (isset($summary['before_commit']) ? (string) $summary['before_commit'] : ''); $summaryCurrentCommit = isset($summary['after_commit']) ? (string) $summary['after_commit'] : (isset($summary['before_commit']) ? (string) $summary['before_commit'] : '');
$summaryRemoteCommit = isset($summary['remote_commit']) ? (string) $summary['remote_commit'] : ''; $summaryRemoteCommit = isset($summary['remote_commit']) ? (string) $summary['remote_commit'] : '';
$syncDate = isset($summary['synced_at']) ? (string) $summary['synced_at'] : null; $syncDate = isset($summary['synced_at']) ? (string) $summary['synced_at'] : null;
$this->gitCurrentBranch = $this->gitCurrentBranch ?: (isset($summary['branch']) ? (string) $summary['branch'] : null); $this->gitCurrentBranch = $this->gitCurrentBranch ?: (isset($summary['branch']) ? (string) $summary['branch'] : null);

View File

@ -1131,14 +1131,14 @@ private function selectCallerRepresentative(Collection $group, int $selectedCall
$representative = $selected instanceof RubricaUniversale $representative = $selected instanceof RubricaUniversale
? $selected ? $selected
: $group : $group
->sortByDesc(fn(RubricaUniversale $match): array => [ ->sortByDesc(fn(RubricaUniversale $match): array=> [
(int) (! empty($match->amministratore_id)), (int) (! empty($match->amministratore_id)),
(int) (! empty($match->riferimento_stabile)), (int) (! empty($match->riferimento_stabile)),
(int) (! empty($match->riferimento_unita)), (int) (! empty($match->riferimento_unita)),
(int) (($match->categoria ?? '') === 'condomino'), (int) (($match->categoria ?? '') === 'condomino'),
-1 * (int) $match->id, -1 * (int) $match->id,
]) ])
->first(); ->first();
$duplicateCategories = $group $duplicateCategories = $group
->pluck('categoria') ->pluck('categoria')
@ -1274,7 +1274,6 @@ private function resolveCallerStabile(RubricaUniversale $caller, ?int $fallbackS
return $fallbackStabileId ? Stabile::query()->find($fallbackStabileId) : null; return $fallbackStabileId ? Stabile::query()->find($fallbackStabileId) : null;
} }
private function resolveCallerSoggetto(RubricaUniversale $caller, ?int $stabileId = null): ?Soggetto private function resolveCallerSoggetto(RubricaUniversale $caller, ?int $stabileId = null): ?Soggetto
{ {
$queries = []; $queries = [];

View File

@ -23,10 +23,12 @@
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms; use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms; use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -56,6 +58,8 @@ public static function canAccess(): bool
public ?array $unitaPickerData = []; public ?array $unitaPickerData = [];
public string $unitaVisibilita = 'attive';
/** /**
* @var array<int, string> * @var array<int, string>
*/ */
@ -139,6 +143,8 @@ public function mount(): void
} }
$this->stabileId = (int) $stabileId; $this->stabileId = (int) $stabileId;
$this->unitaVisibilita = $this->normalizeUnitaVisibilita(request()->query('visibilita'));
$this->refreshUnitaOptions(); $this->refreshUnitaOptions();
$this->unitaId = $this->pickUnitaId(request()->integer('unita_id')); $this->unitaId = $this->pickUnitaId(request()->integer('unita_id'));
@ -240,6 +246,60 @@ protected function getHeaderActions(): array
$nextId = $this->getNextUnitaId(); $nextId = $this->getNextUnitaId();
return [ return [
Action::make('modifica_unita')
->label('Modifica unità')
->icon('heroicon-o-pencil-square')
->modalWidth('5xl')
->visible(fn(): bool => (bool) $this->unitaId && $this->unita instanceof UnitaImmobiliare)
->fillForm(fn(): array=> $this->getEditUnitaFormData())
->form([
TextInput::make('codice_unita')
->label('Codice unità')
->maxLength(100)
->columnSpan(3),
TextInput::make('denominazione')
->label('Denominazione')
->maxLength(255)
->columnSpan(5),
TextInput::make('tipo_unita')
->label('Tipologia')
->maxLength(100)
->columnSpan(2),
TextInput::make('categoria_catastale')
->label('Categoria catastale')
->maxLength(20)
->columnSpan(2),
TextInput::make('scala')
->label('Scala')
->maxLength(50)
->columnSpan(2),
TextInput::make('piano')
->label('Piano')
->maxLength(50)
->columnSpan(2),
TextInput::make('interno')
->label('Interno')
->maxLength(50)
->columnSpan(2),
TextInput::make('stato_occupazione')
->label('Stato occupazione')
->maxLength(100)
->columnSpan(3),
TextInput::make('utilizzo_attuale')
->label('Utilizzo attuale')
->maxLength(100)
->columnSpan(3),
Toggle::make('attiva')
->label('Unità attiva')
->columnSpan(2),
Textarea::make('note')
->label('Note')
->rows(4)
->columnSpanFull(),
])
->action(function (array $data): void {
$this->saveEditedUnita($data);
}),
Action::make('prev_unita') Action::make('prev_unita')
->label('Precedente') ->label('Precedente')
->icon('heroicon-o-arrow-left') ->icon('heroicon-o-arrow-left')
@ -529,15 +589,32 @@ public function setTab(string $tab): void
$this->tab = $tab; $this->tab = $tab;
} }
public function setUnitaVisibilita(string $visibilita): void
{
$this->unitaVisibilita = $this->normalizeUnitaVisibilita($visibilita);
$this->refreshUnitaOptions();
$this->unitaId = $this->pickUnitaId($this->unitaId);
$this->getSchema('unitaPicker')?->fill([
'unita_id' => $this->unitaId,
]);
if ($this->unitaId) {
$this->redirect($this->makeSelfUrl($this->unitaId));
return;
}
$this->loadUnita();
}
protected function refreshUnitaOptions(): void protected function refreshUnitaOptions(): void
{ {
$unita = UnitaImmobiliare::query() $unita = $this->buildUnitaIndexQuery()
->where('stabile_id', $this->stabileId)
->orderBy('palazzina_id') ->orderBy('palazzina_id')
->orderBy('scala') ->orderBy('scala')
->orderBy('piano') ->orderBy('piano')
->orderBy('id') ->orderBy('id')
->get(['id', 'palazzina_id', 'codice_unita', 'denominazione', 'scala', 'piano', 'interno']); ->get(['id', 'palazzina_id', 'codice_unita', 'denominazione', 'scala', 'piano', 'interno', 'deleted_at']);
$unita = $unita->filter(function (UnitaImmobiliare $u): bool { $unita = $unita->filter(function (UnitaImmobiliare $u): bool {
$denominazione = trim((string) ($u->denominazione ?? '')); $denominazione = trim((string) ($u->denominazione ?? ''));
@ -649,6 +726,9 @@ protected function refreshUnitaOptions(): void
if (is_string($owner) && trim($owner) !== '') { if (is_string($owner) && trim($owner) !== '') {
$label .= ' — ' . trim($owner); $label .= ' — ' . trim($owner);
} }
if ($u->trashed()) {
$label .= ' [archiviata]';
}
return [(int) $u->id => $label]; return [(int) $u->id => $label];
}) })
->all(); ->all();
@ -730,14 +810,15 @@ protected function loadUnita(): void
return; return;
} }
$this->unita = UnitaImmobiliare::with([ $this->unita = UnitaImmobiliare::withTrashed()
'palazzinaObj', ->with([
'soggetti', 'palazzinaObj',
'stabile', 'soggetti',
'dettagliMillesimi.tabellaMillesimale', 'stabile',
'relazioniPersoneAttive.persona.emailMultiple', 'dettagliMillesimi.tabellaMillesimale',
'unitaRecapitiServizio.persona', 'relazioniPersoneAttive.persona.emailMultiple',
]) 'unitaRecapitiServizio.persona',
])
->where('stabile_id', $this->stabileId) ->where('stabile_id', $this->stabileId)
->whereKey($this->unitaId) ->whereKey($this->unitaId)
->first(); ->first();
@ -758,6 +839,79 @@ protected function loadUnita(): void
$this->tab = 'riepilogo'; $this->tab = 'riepilogo';
} }
protected function buildUnitaIndexQuery()
{
$query = UnitaImmobiliare::query()
->where('stabile_id', $this->stabileId);
return match ($this->unitaVisibilita) {
'tutte' => $query->withTrashed(),
'archiviate' => $query->onlyTrashed(),
default => $query,
};
}
protected function normalizeUnitaVisibilita(mixed $value): string
{
$candidate = strtolower(trim((string) $value));
return in_array($candidate, ['attive', 'tutte', 'archiviate'], true)
? $candidate
: 'attive';
}
protected function getEditUnitaFormData(): array
{
if (! $this->unita) {
return [];
}
return [
'codice_unita' => (string) ($this->unita->codice_unita ?? ''),
'denominazione' => (string) ($this->unita->denominazione ?? ''),
'tipo_unita' => (string) ($this->unita->tipo_unita ?? ''),
'categoria_catastale' => (string) ($this->unita->categoria_catastale ?? ''),
'scala' => (string) ($this->unita->scala ?? ''),
'piano' => (string) ($this->unita->piano ?? ''),
'interno' => (string) ($this->unita->interno ?? ''),
'stato_occupazione' => (string) ($this->unita->stato_occupazione ?? ''),
'utilizzo_attuale' => (string) ($this->unita->utilizzo_attuale ?? ''),
'attiva' => (bool) ($this->unita->attiva ?? true),
'note' => (string) ($this->unita->note ?? ''),
];
}
protected function saveEditedUnita(array $data): void
{
if (! $this->unita) {
return;
}
$this->unita->fill([
'codice_unita' => $this->normalizeOptionalString($data['codice_unita'] ?? null),
'denominazione' => $this->normalizeOptionalString($data['denominazione'] ?? null),
'tipo_unita' => $this->normalizeOptionalString($data['tipo_unita'] ?? null),
'categoria_catastale' => $this->normalizeOptionalString($data['categoria_catastale'] ?? null),
'scala' => $this->normalizeOptionalString($data['scala'] ?? null),
'piano' => $this->normalizeOptionalString($data['piano'] ?? null),
'interno' => $this->normalizeOptionalString($data['interno'] ?? null),
'stato_occupazione' => $this->normalizeOptionalString($data['stato_occupazione'] ?? null),
'utilizzo_attuale' => $this->normalizeOptionalString($data['utilizzo_attuale'] ?? null),
'attiva' => (bool) ($data['attiva'] ?? false),
'note' => $this->normalizeOptionalString($data['note'] ?? null),
]);
$this->unita->updated_by = Auth::id();
$this->unita->save();
$this->refreshUnitaOptions();
$this->loadUnita();
Notification::make()
->title('Unità aggiornata')
->success()
->send();
}
public function hydrateNominativiStorici(): void public function hydrateNominativiStorici(): void
{ {
$this->nominativiStorici = []; $this->nominativiStorici = [];

View File

@ -65,10 +65,10 @@ public function mount(): void
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []); $google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$oauth = Arr::get($google, 'oauth', []); $oauth = Arr::get($google, 'oauth', []);
$this->googleConnected = (bool) Arr::get($oauth, 'connected', false); $this->googleConnected = (bool) Arr::get($oauth, 'connected', false);
$email = (string) Arr::get($oauth, 'email', ''); $email = (string) Arr::get($oauth, 'email', '');
$this->connectionLabel = $email !== '' ? $email : ($this->googleConnected ? 'Collegato' : 'Non collegato'); $this->connectionLabel = $email !== '' ? $email : ($this->googleConnected ? 'Collegato' : 'Non collegato');
$this->calendarId = (string) Arr::get($google, 'calendar_id', 'primary') ?: 'primary'; $this->calendarId = (string) Arr::get($google, 'calendar_id', 'primary') ?: 'primary';
$this->rubricaSyncCommandPreview = 'php artisan google:sync-rubrica --admin-id=' . (int) $amministratore->id . ' --max-pages=20'; $this->rubricaSyncCommandPreview = 'php artisan google:sync-rubrica --admin-id=' . (int) $amministratore->id . ' --max-pages=20';
if (! $this->googleConnected) { if (! $this->googleConnected) {
@ -144,7 +144,7 @@ private function runRubricaSync(bool $dryRun): void
} }
$params = [ $params = [
'--admin-id' => [(string) $amministratore->id], '--admin-id' => [(string) $amministratore->id],
'--max-pages' => 20, '--max-pages' => 20,
]; ];
@ -152,11 +152,11 @@ private function runRubricaSync(bool $dryRun): void
$params['--dry-run'] = true; $params['--dry-run'] = true;
} }
$exit = Artisan::call('google:sync-rubrica', $params); $exit = Artisan::call('google:sync-rubrica', $params);
$output = trim((string) Artisan::output()); $output = trim((string) Artisan::output());
$this->rubricaSyncLastRunLabel = now()->format('d/m/Y H:i:s') . ($dryRun ? ' · dry-run' : ' · apply'); $this->rubricaSyncLastRunLabel = now()->format('d/m/Y H:i:s') . ($dryRun ? ' · dry-run' : ' · apply');
$this->rubricaSyncLastOutput = $output !== '' ? $output : 'Nessun output disponibile.'; $this->rubricaSyncLastOutput = $output !== '' ? $output : 'Nessun output disponibile.';
$this->rubricaSyncCommandPreview = 'php artisan google:sync-rubrica --admin-id=' . (int) $amministratore->id . ' --max-pages=20' . ($dryRun ? ' --dry-run' : ''); $this->rubricaSyncCommandPreview = 'php artisan google:sync-rubrica --admin-id=' . (int) $amministratore->id . ' --max-pages=20' . ($dryRun ? ' --dry-run' : '');
if (! $dryRun) { if (! $dryRun) {
@ -165,8 +165,8 @@ private function runRubricaSync(bool $dryRun): void
Notification::make() Notification::make()
->title($exit === 0 ->title($exit === 0
? ($dryRun ? 'Preview import rubrica completata' : 'Import rubrica completato') ? ($dryRun ? 'Preview import rubrica completata' : 'Import rubrica completato')
: ($dryRun ? 'Preview import rubrica con errori' : 'Import rubrica con errori')) : ($dryRun ? 'Preview import rubrica con errori' : 'Import rubrica con errori'))
->body((string) collect(preg_split('/\R/', $this->rubricaSyncLastOutput) ?: [])->filter()->last()) ->body((string) collect(preg_split('/\R/', $this->rubricaSyncLastOutput) ?: [])->filter()->last())
->{$exit === 0 ? 'success' : 'warning'}() ->{$exit === 0 ? 'success' : 'warning'}()
->send(); ->send();

View File

@ -18,10 +18,10 @@ class TicketMobileOverview extends Widget
/** @var array<string,int> */ /** @var array<string,int> */
public array $ticketCounters = [ public array $ticketCounters = [
'open' => 0, 'open' => 0,
'urgent' => 0, 'urgent' => 0,
'closed' => 0, 'closed' => 0,
'all' => 0, 'all' => 0,
]; ];
public static function canView(): bool public static function canView(): bool
@ -29,7 +29,7 @@ public static function canView(): bool
$user = Auth::user(); $user = Auth::user();
return $user instanceof User return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
} }
public function mount(): void public function mount(): void
@ -53,10 +53,10 @@ public function mount(): void
$base = Ticket::query()->whereIn('stabile_id', $stabileIds); $base = Ticket::query()->whereIn('stabile_id', $stabileIds);
$this->ticketCounters = [ $this->ticketCounters = [
'open' => (clone $base)->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(), '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(), 'urgent' => (clone $base)->where('priorita', 'Urgente')->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
'closed' => (clone $base)->whereIn('stato', ['Risolto', 'Chiuso'])->count(), 'closed' => (clone $base)->whereIn('stato', ['Risolto', 'Chiuso'])->count(),
'all' => (clone $base)->count(), 'all' => (clone $base)->count(),
]; ];
} }
@ -99,4 +99,4 @@ public function getHelpUrl(): string
{ {
return TicketMobileHelp::getUrl(panel: 'admin-filament'); return TicketMobileHelp::getUrl(panel: 'admin-filament');
} }
} }

View File

@ -6,8 +6,8 @@
use App\Models\Fornitore; use App\Models\Fornitore;
use App\Models\FornitoreDipendente; use App\Models\FornitoreDipendente;
use App\Models\Stabile; use App\Models\Stabile;
use App\Support\GoogleAccountStore;
use App\Models\User; use App\Models\User;
use App\Support\GoogleAccountStore;
use App\Support\StabileContext; use App\Support\StabileContext;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -19,7 +19,7 @@
class GoogleOAuthController extends Controller class GoogleOAuthController extends Controller
{ {
private const GOOGLE_SCOPES = [ private const GOOGLE_SCOPES = [
...GoogleAccountStore::SCOPES, ...GoogleAccountStore::SCOPES,
]; ];
public function redirect(): RedirectResponse public function redirect(): RedirectResponse
@ -30,8 +30,8 @@ public function redirect(): RedirectResponse
abort(403, 'Amministratore non disponibile per questa sessione.'); abort(403, 'Amministratore non disponibile per questa sessione.');
} }
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []); $google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$store = app(GoogleAccountStore::class); $store = app(GoogleAccountStore::class);
$accountKey = $store->normalizeAccountKey((string) request()->query('account_key', '')); $accountKey = $store->normalizeAccountKey((string) request()->query('account_key', ''));
$accountLabel = trim((string) request()->query('label', '')); $accountLabel = trim((string) request()->query('label', ''));
$account = $store->get($amministratore, $accountKey !== '' ? $accountKey : null); $account = $store->get($amministratore, $accountKey !== '' ? $accountKey : null);
@ -86,11 +86,11 @@ public function callback(): RedirectResponse
abort(403, 'Amministratore non disponibile per questa sessione.'); abort(403, 'Amministratore non disponibile per questa sessione.');
} }
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []); $google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$store = app(GoogleAccountStore::class); $store = app(GoogleAccountStore::class);
$clientId = (string) ($google['client_id'] ?? config('services.google.client_id')); $clientId = (string) ($google['client_id'] ?? config('services.google.client_id'));
$clientSecret = (string) ($google['client_secret'] ?? config('services.google.client_secret')); $clientSecret = (string) ($google['client_secret'] ?? config('services.google.client_secret'));
$redirectUri = (string) ($google['redirect_uri'] ?? config('services.google.redirect')); $redirectUri = (string) ($google['redirect_uri'] ?? config('services.google.redirect'));
try { try {
$oauthContext = (array) session()->pull('google_oauth_context', []); $oauthContext = (array) session()->pull('google_oauth_context', []);
@ -124,7 +124,7 @@ public function callback(): RedirectResponse
$payload = $tokenResponse->json(); $payload = $tokenResponse->json();
$accessToken = (string) Arr::get($payload, 'access_token', ''); $accessToken = (string) Arr::get($payload, 'access_token', '');
$refreshToken = (string) Arr::get($payload, 'refresh_token', ''); $refreshToken = (string) Arr::get($payload, 'refresh_token', '');
$expiresIn = (int) Arr::get($payload, 'expires_in', 3600); $expiresIn = (int) Arr::get($payload, 'expires_in', 3600);
$profileResponse = Http::withToken($accessToken) $profileResponse = Http::withToken($accessToken)
->acceptJson() ->acceptJson()

View File

@ -102,7 +102,18 @@ protected static function booted(): void
return; return;
} }
foreach (['documenti', 'temp/upload', 'temp/processing', 'logs'] as $folder) { foreach ([
'documenti',
'rubrica',
'fe',
'prodotti',
'comunicazioni',
'tecnorepair/imports',
'tecnorepair/allegati',
'temp/upload',
'temp/processing',
'logs',
] as $folder) {
Storage::disk('local')->makeDirectory($base . '/' . $folder); Storage::disk('local')->makeDirectory($base . '/' . $folder);
} }
} catch (\Throwable) { } catch (\Throwable) {

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Services\Anagrafiche; namespace App\Services\Anagrafiche;
use App\Models\RubricaUniversale; use App\Models\RubricaUniversale;
@ -88,4 +87,4 @@ private function normalizedDigitsSql(string $column): string
{ {
return "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE({$column}, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '')"; return "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE({$column}, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '')";
} }
} }

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Services\Support; namespace App\Services\Support;
use App\Models\CommunicationMessage; use App\Models\CommunicationMessage;
@ -51,7 +50,7 @@ public function importMailbox(Stabile $stabile, array $mailbox, int $maxMessages
->get('https://gmail.googleapis.com/gmail/v1/users/me/messages', array_filter([ ->get('https://gmail.googleapis.com/gmail/v1/users/me/messages', array_filter([
'maxResults' => max(1, min($maxMessages, 50)), 'maxResults' => max(1, min($maxMessages, 50)),
'q' => $query !== '' ? $query : null, 'q' => $query !== '' ? $query : null,
], static fn ($value) => $value !== null)); ], static fn($value) => $value !== null));
if (! $response->successful()) { if (! $response->successful()) {
throw new RuntimeException('Gmail API non disponibile: ' . $response->body()); throw new RuntimeException('Gmail API non disponibile: ' . $response->body());
@ -86,15 +85,15 @@ public function importMailbox(Stabile $stabile, array $mailbox, int $maxMessages
$date = $headers['date'] ?? null; $date = $headers['date'] ?? null;
$ticket = Ticket::query()->create([ $ticket = Ticket::query()->create([
'stabile_id' => (int) $stabile->id, 'stabile_id' => (int) $stabile->id,
'aperto_da_user_id' => $openingUserId, 'aperto_da_user_id' => $openingUserId,
'titolo' => Str::limit($subject !== '' ? $subject : ('Email importata da ' . ($from !== '' ? $from : 'Gmail')), 255), 'titolo' => Str::limit($subject !== '' ? $subject : ('Email importata da ' . ($from !== '' ? $from : 'Gmail')), 255),
'descrizione' => $this->buildTicketDescription($stabile, $mailbox, $subject, $from, $to, $date, $bodyText), 'descrizione' => $this->buildTicketDescription($stabile, $mailbox, $subject, $from, $to, $date, $bodyText),
'categoria_ticket_id' => null, 'categoria_ticket_id' => null,
'luogo_intervento' => 'Casella email stabile', 'luogo_intervento' => 'Casella email stabile',
'data_apertura' => $this->normalizeDate($date) ?? now(), 'data_apertura' => $this->normalizeDate($date) ?? now(),
'stato' => 'Aperto', 'stato' => 'Aperto',
'priorita' => 'Media', 'priorita' => 'Media',
]); ]);
$rawMessage = $this->fetchMessage($token, $gmailId, 'raw'); $rawMessage = $this->fetchMessage($token, $gmailId, 'raw');
@ -113,10 +112,10 @@ public function importMailbox(Stabile $stabile, array $mailbox, int $maxMessages
'inviato_il' => $this->normalizeDate($date), 'inviato_il' => $this->normalizeDate($date),
'eml_documento_id' => $documento->id, 'eml_documento_id' => $documento->id,
'metadata' => [ 'metadata' => [
'gmail_message_id' => $gmailId, 'gmail_message_id' => $gmailId,
'gmail_thread_id' => Arr::get($message, 'threadId'), 'gmail_thread_id' => Arr::get($message, 'threadId'),
'gmail_mailbox_email' => $mailbox['email'] ?? null, 'gmail_mailbox_email' => $mailbox['email'] ?? null,
'google_account_key' => $mailbox['google_account_key'] ?? null, 'google_account_key' => $mailbox['google_account_key'] ?? null,
], ],
]); ]);
@ -131,13 +130,13 @@ public function importMailbox(Stabile $stabile, array $mailbox, int $maxMessages
'status' => 'linked_to_ticket', 'status' => 'linked_to_ticket',
'received_at' => $this->normalizeDate($date), 'received_at' => $this->normalizeDate($date),
'metadata' => [ 'metadata' => [
'gmail_message_id' => $gmailId, 'gmail_message_id' => $gmailId,
'gmail_thread_id' => Arr::get($message, 'threadId'), 'gmail_thread_id' => Arr::get($message, 'threadId'),
'gmail_mailbox_email' => $mailbox['email'] ?? null, 'gmail_mailbox_email' => $mailbox['email'] ?? null,
'google_account_key' => $mailbox['google_account_key'] ?? null, 'google_account_key' => $mailbox['google_account_key'] ?? null,
'subject' => $subject, 'subject' => $subject,
'to' => $to, 'to' => $to,
'documento_id' => (int) $documento->id, 'documento_id' => (int) $documento->id,
], ],
]); ]);
@ -251,10 +250,10 @@ private function storeAttachments(string $token, string $gmailId, Ticket $ticket
'ticket-email/' . $ticket->id, 'ticket-email/' . $ticket->id,
$fileName, $fileName,
[ [
'mime' => (string) ($part['mimeType'] ?? ''), 'mime' => (string) ($part['mimeType'] ?? ''),
'optimize_image' => false, 'optimize_image' => false,
'display_name' => $fileName, 'display_name' => $fileName,
'stored_basename' => pathinfo($fileName, PATHINFO_FILENAME), 'stored_basename' => pathinfo($fileName, PATHINFO_FILENAME),
] ]
); );
@ -414,4 +413,4 @@ private function normalizeDate(mixed $value): mixed
return null; return null;
} }
} }
} }

View File

@ -92,7 +92,7 @@ public function storeRawContents(string $contents, string $directory, string $or
Storage::disk('public')->put($path, $contents); Storage::disk('public')->put($path, $contents);
$mime = TicketAttachment::normalizeMimeType((string) ($options['mime'] ?? ''), $originalName, $path); $mime = TicketAttachment::normalizeMimeType((string) ($options['mime'] ?? ''), $originalName, $path);
$metadata = []; $metadata = [];
if (str_starts_with($mime, 'image/')) { if (str_starts_with($mime, 'image/')) {

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Support; namespace App\Support;
use App\Models\Amministratore; use App\Models\Amministratore;
@ -46,9 +45,9 @@ public function all(Amministratore $amministratore): array
if (is_array($legacyOauth) && $this->hasUsableOauthPayload($legacyOauth)) { if (is_array($legacyOauth) && $this->hasUsableOauthPayload($legacyOauth)) {
$legacyKey = $this->normalizeAccountKey( $legacyKey = $this->normalizeAccountKey(
(string) (Arr::get($google, 'default_account_key') (string) (Arr::get($google, 'default_account_key')
?: Arr::get($legacyOauth, 'google_user_id') ?: Arr::get($legacyOauth, 'google_user_id')
?: Arr::get($legacyOauth, 'email') ?: Arr::get($legacyOauth, 'email')
?: 'primary') ?: 'primary')
); );
if (! isset($accounts[$legacyKey])) { if (! isset($accounts[$legacyKey])) {
@ -143,7 +142,7 @@ public function upsertAccount(Amministratore $amministratore, string $accountKey
$account['scopes'] = self::SCOPES; $account['scopes'] = self::SCOPES;
} }
$accounts[$key] = $account; $accounts[$key] = $account;
$google['accounts'] = $accounts; $google['accounts'] = $accounts;
$defaultKey = $this->normalizeAccountKey((string) Arr::get($google, 'default_account_key', '')); $defaultKey = $this->normalizeAccountKey((string) Arr::get($google, 'default_account_key', ''));
@ -245,17 +244,17 @@ public function resolveAccessToken(Amministratore $amministratore, ?string $acco
} }
$this->upsertAccount($amministratore, (string) ($account['key'] ?? 'primary'), array_filter([ $this->upsertAccount($amministratore, (string) ($account['key'] ?? 'primary'), array_filter([
'connected' => true, 'connected' => true,
'label' => $account['label'] ?? null, 'label' => $account['label'] ?? null,
'email' => $account['email'] ?? null, 'email' => $account['email'] ?? null,
'name' => $account['name'] ?? null, 'name' => $account['name'] ?? null,
'google_user_id'=> $account['google_user_id'] ?? null, 'google_user_id' => $account['google_user_id'] ?? null,
'access_token' => $newAccessToken, 'access_token' => $newAccessToken,
'refresh_token' => trim((string) Arr::get($payload, 'refresh_token', '')) ?: $refreshToken, 'refresh_token' => trim((string) Arr::get($payload, 'refresh_token', '')) ?: $refreshToken,
'expires_in' => (int) Arr::get($payload, 'expires_in', $account['expires_in'] ?? 3600), 'expires_in' => (int) Arr::get($payload, 'expires_in', $account['expires_in'] ?? 3600),
'refreshed_at' => now()->toDateTimeString(), 'refreshed_at' => now()->toDateTimeString(),
'scopes' => $account['scopes'] ?? self::SCOPES, 'scopes' => $account['scopes'] ?? self::SCOPES,
], static fn ($value) => $value !== null), ! empty($account['is_default'])); ], static fn($value) => $value !== null), ! empty($account['is_default']));
return $newAccessToken; return $newAccessToken;
} }
@ -269,19 +268,19 @@ private function normalizeAccountPayload(array $account, string $key): array
$label = trim((string) ($account['label'] ?? $account['name'] ?? $account['email'] ?? $key)); $label = trim((string) ($account['label'] ?? $account['name'] ?? $account['email'] ?? $key));
return [ return [
'key' => $key, 'key' => $key,
'label' => $label, 'label' => $label,
'connected' => (bool) ($account['connected'] ?? false), 'connected' => (bool) ($account['connected'] ?? false),
'email' => trim((string) ($account['email'] ?? '')), 'email' => trim((string) ($account['email'] ?? '')),
'name' => trim((string) ($account['name'] ?? '')), 'name' => trim((string) ($account['name'] ?? '')),
'google_user_id' => trim((string) ($account['google_user_id'] ?? '')), 'google_user_id' => trim((string) ($account['google_user_id'] ?? '')),
'access_token' => trim((string) ($account['access_token'] ?? '')), 'access_token' => trim((string) ($account['access_token'] ?? '')),
'refresh_token' => trim((string) ($account['refresh_token'] ?? '')), 'refresh_token' => trim((string) ($account['refresh_token'] ?? '')),
'expires_in' => (int) ($account['expires_in'] ?? 0), 'expires_in' => (int) ($account['expires_in'] ?? 0),
'connected_at' => $account['connected_at'] ?? null, 'connected_at' => $account['connected_at'] ?? null,
'refreshed_at' => $account['refreshed_at'] ?? null, 'refreshed_at' => $account['refreshed_at'] ?? null,
'disconnected_at'=> $account['disconnected_at'] ?? null, 'disconnected_at' => $account['disconnected_at'] ?? null,
'scopes' => array_values(array_filter((array) ($account['scopes'] ?? []))), 'scopes' => array_values(array_filter((array) ($account['scopes'] ?? []))),
]; ];
} }
@ -330,7 +329,7 @@ private function isExpired(array $account): bool
private function hasUsableOauthPayload(array $oauth): bool private function hasUsableOauthPayload(array $oauth): bool
{ {
return (bool) ($oauth['connected'] ?? false) return (bool) ($oauth['connected'] ?? false)
|| trim((string) ($oauth['refresh_token'] ?? '')) !== '' || trim((string) ($oauth['refresh_token'] ?? '')) !== ''
|| trim((string) ($oauth['access_token'] ?? '')) !== ''; || trim((string) ($oauth['access_token'] ?? '')) !== '';
} }
} }

View File

@ -130,7 +130,7 @@ protected function searchRubricaLookupMatches(string $raw, ?int $selectedId = nu
$matches = $query->get(); $matches = $query->get();
foreach ($matches as $rubrica) { foreach ($matches as $rubrica) {
$row = $this->mapRubricaLookupRow($rubrica); $row = $this->mapRubricaLookupRow($rubrica);
$row['score'] = $this->scoreRubricaLookupMatch($row, $raw, $digits, $selectedId); $row['score'] = $this->scoreRubricaLookupMatch($row, $raw, $digits, $selectedId);
$rows[] = $row; $rows[] = $row;
} }

View File

@ -1,11 +1,11 @@
{ {
"resources/css/app.css": { "resources/css/app.css": {
"file": "assets/app-BGt-S40c.css", "file": "assets/app-tgbzBtFO.css",
"src": "resources/css/app.css", "src": "resources/css/app.css",
"isEntry": true "isEntry": true
}, },
"resources/css/filament/admin/theme.css": { "resources/css/filament/admin/theme.css": {
"file": "assets/theme-D-E95I9r.css", "file": "assets/theme-EkdZtrh_.css",
"src": "resources/css/filament/admin/theme.css", "src": "resources/css/filament/admin/theme.css",
"isEntry": true "isEntry": true
}, },

View File

@ -16,6 +16,7 @@
<a href="{{ $this->getTicketsUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Ticket operativi</a> <a href="{{ $this->getTicketsUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Ticket operativi</a>
<a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Collaboratori</a> <a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Collaboratori</a>
<a href="{{ $this->getRubricaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Rubrica clienti</a> <a href="{{ $this->getRubricaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Rubrica clienti</a>
<a href="{{ $this->getImpostazioniUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Impostazioni</a>
<a href="{{ $this->getProdottiUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Prodotti</a> <a href="{{ $this->getProdottiUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Prodotti</a>
</div> </div>
</div> </div>

View File

@ -15,6 +15,7 @@
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<a href="{{ \App\Filament\Pages\Fornitore\LavorazioniOperative::getUrl(['fornitore' => $this->fornitoreId], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Lavorazioni</a> <a href="{{ \App\Filament\Pages\Fornitore\LavorazioniOperative::getUrl(['fornitore' => $this->fornitoreId], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Lavorazioni</a>
<a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Collaboratori</a> <a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Collaboratori</a>
<a href="{{ $this->getImpostazioniUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Impostazioni</a>
<a href="{{ $this->getContabilitaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Contabilita</a> <a href="{{ $this->getContabilitaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Contabilita</a>
</div> </div>
</div> </div>
@ -48,10 +49,14 @@
<table class="min-w-full divide-y divide-gray-200 text-sm"> <table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-4 py-3 text-left font-medium text-gray-600">Ticket</th> <th class="px-4 py-3 text-left font-medium text-gray-600">Num</th>
<th class="px-4 py-3 text-left font-medium text-gray-600">Contatto</th> <th class="px-4 py-3 text-left font-medium text-gray-600">Ingresso</th>
<th class="px-4 py-3 text-left font-medium text-gray-600">Problema</th> <th class="px-4 py-3 text-left font-medium text-gray-600">Cliente</th>
<th class="px-4 py-3 text-left font-medium text-gray-600">Operatore</th> <th class="px-4 py-3 text-left font-medium text-gray-600">Tel</th>
<th class="px-4 py-3 text-left font-medium text-gray-600">Difetto</th>
<th class="px-4 py-3 text-left font-medium text-gray-600">Modello</th>
<th class="px-4 py-3 text-left font-medium text-gray-600">Seriale</th>
<th class="px-4 py-3 text-left font-medium text-gray-600">Cod. prod</th>
<th class="px-4 py-3 text-left font-medium text-gray-600">Stato</th> <th class="px-4 py-3 text-left font-medium text-gray-600">Stato</th>
<th class="px-4 py-3 text-left font-medium text-gray-600">Aggiornato</th> <th class="px-4 py-3 text-left font-medium text-gray-600">Aggiornato</th>
<th class="px-4 py-3"></th> <th class="px-4 py-3"></th>
@ -59,34 +64,46 @@
</thead> </thead>
<tbody class="divide-y divide-gray-100 bg-white"> <tbody class="divide-y divide-gray-100 bg-white">
@forelse($rows as $row) @forelse($rows as $row)
<tr> <tr class="{{ $row['row_class'] }}">
<td class="px-4 py-3 align-top"> <td class="px-4 py-3 align-top font-medium">
<div class="font-medium">#{{ $row['ticket_id'] }} - {{ $row['titolo'] }}</div> {{ $row['numero'] }}
<div class="text-xs text-gray-500">{{ $row['stabile'] }} · ingresso {{ $row['ingresso'] }}</div> <div class="mt-1 text-[11px] text-slate-500">{{ $row['origine'] }}</div>
</td> </td>
<td class="px-4 py-3 align-top text-gray-700">{{ $row['ingresso'] }}</td>
<td class="px-4 py-3 align-top"> <td class="px-4 py-3 align-top">
<div>{{ $row['contatto'] }}</div> <div class="font-medium">{{ $row['cliente'] }}</div>
<div class="text-xs text-gray-500">{{ $row['telefono'] !== '' ? $row['telefono'] : 'Telefono non disponibile' }}</div> <div class="text-[11px] text-slate-500">{{ $row['stabile'] }}</div>
</td> </td>
<td class="px-4 py-3 align-top">{{ $row['telefono'] !== '' ? $row['telefono'] : '-' }}</td>
<td class="px-4 py-3 align-top">{{ $row['difetto'] }}</td>
<td class="px-4 py-3 align-top"> <td class="px-4 py-3 align-top">
<div>{{ $row['problema'] }}</div> <div>{{ $row['modello'] }}</div>
<div class="text-xs text-gray-500">Apparato: {{ $row['apparato'] }}</div> @if(($row['titolo'] ?? '') !== '')
<div class="text-[11px] text-slate-500">{{ $row['titolo'] }}</div>
@endif
</td> </td>
<td class="px-4 py-3 align-top">{{ $row['operatore'] }}</td> <td class="px-4 py-3 align-top">{{ $row['seriale'] }}</td>
<td class="px-4 py-3 align-top">{{ $row['cod_prodotto'] }}</td>
<td class="px-4 py-3 align-top"> <td class="px-4 py-3 align-top">
<span class="inline-flex rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">{{ $row['stato'] }}</span> <span class="inline-flex rounded-full border px-2 py-1 text-xs font-medium {{ $row['badge_class'] }}">{{ $row['stato'] }}</span>
</td> </td>
<td class="px-4 py-3 align-top text-gray-600">{{ $row['updated_at'] }}</td> <td class="px-4 py-3 align-top text-gray-600">{{ $row['updated_at'] }}</td>
<td class="px-4 py-3 text-right align-top"> <td class="px-4 py-3 text-right align-top">
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button type="button" wire:click="openInterventoModal({{ (int) $row['id'] }})" class="inline-flex items-center rounded-md bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-200">Apri modal</button> @if($row['row_type'] === 'ticket')
<a href="{{ $row['url'] }}" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Apri scheda</a> <button type="button" wire:click="openInterventoModal({{ (int) $row['id'] }})" class="inline-flex items-center rounded-md bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-200">Apri modal</button>
@endif
@if($row['url'])
<a href="{{ $row['url'] }}" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Apri scheda</a>
@else
<a href="{{ $this->getImpostazioniUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-slate-600 ring-1 ring-inset ring-slate-300 hover:bg-slate-50">Gestisci import</a>
@endif
</div> </div>
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="7" class="px-4 py-10 text-center text-sm text-gray-500">Nessun intervento trovato per questo filtro.</td> <td colspan="11" class="px-4 py-10 text-center text-sm text-gray-500">Nessun intervento trovato per questo filtro.</td>
</tr> </tr>
@endforelse @endforelse
</tbody> </tbody>

View File

@ -29,8 +29,56 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
@php @php
$unitaOptions = $this->getUnitaOptions(); $unitaOptions = $this->getUnitaOptions();
$prevUrl = $this->getPrevUnitaUrl();
$nextUrl = $this->getNextUnitaUrl();
$visibilityOptions = [
'attive' => 'Attive',
'tutte' => 'Tutte',
'archiviate' => 'Archiviate',
];
@endphp @endphp
<x-filament::section class="p-4!">
<div class="flex flex-wrap items-end justify-between gap-4">
<div class="flex items-end gap-2">
<div class="flex flex-col items-center gap-1">
<span class="text-[10px] text-gray-500">Prec.</span>
@if($prevUrl)
<a class="fi-btn fi-btn-color-gray fi-btn-size-sm" href="{{ $prevUrl }}">&larr;</a>
@else
<span class="fi-btn fi-btn-color-gray fi-btn-size-sm opacity-40">&larr;</span>
@endif
</div>
<div class="min-w-72 max-w-xl flex flex-col items-start gap-1">
<span class="text-[10px] text-gray-500">Unità</span>
<div class="w-full">{{ $this->getSchema('unitaPicker') }}</div>
</div>
<div class="flex flex-col items-center gap-1">
<span class="text-[10px] text-gray-500">Succ.</span>
@if($nextUrl)
<a class="fi-btn fi-btn-color-gray fi-btn-size-sm" href="{{ $nextUrl }}">&rarr;</a>
@else
<span class="fi-btn fi-btn-color-gray fi-btn-size-sm opacity-40">&rarr;</span>
@endif
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="text-xs font-medium text-gray-500">Visibilità:</span>
@foreach($visibilityOptions as $key => $label)
<button
type="button"
wire:click="setUnitaVisibilita('{{ $key }}')"
class="rounded-lg border px-3 py-2 text-xs font-semibold transition {{ $this->unitaVisibilita === $key ? 'border-primary-300 bg-primary-50 text-primary-700 shadow-sm' : 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50' }}">
{{ $label }}
</button>
@endforeach
</div>
</div>
</x-filament::section>
@if(!$unita) @if(!$unita)
<div class="rounded-xl border border-dashed border-gray-200 bg-white p-6 text-center text-gray-500">Nessuna unità immobiliare trovata.</div> <div class="rounded-xl border border-dashed border-gray-200 bg-white p-6 text-center text-gray-500">Nessuna unità immobiliare trovata.</div>
@else @else
@ -38,8 +86,7 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
{{-- Header compatto --}} {{-- Header compatto --}}
<x-filament::section class="p-4!"> <x-filament::section class="p-4!">
@php @php
$prevUrl = $this->getPrevUnitaUrl(); $isArchivedUnita = method_exists($unita, 'trashed') && $unita->trashed();
$nextUrl = $this->getNextUnitaUrl();
@endphp @endphp
<div class="flex items-start justify-between gap-4 flex-wrap"> <div class="flex items-start justify-between gap-4 flex-wrap">
@ -48,31 +95,9 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
<div class="flex flex-wrap items-center gap-3 text-lg font-semibold text-gray-900"> <div class="flex flex-wrap items-center gap-3 text-lg font-semibold text-gray-900">
<x-filament::icon icon="heroicon-o-home-modern" class="h-5 w-5 text-primary-600" /> <x-filament::icon icon="heroicon-o-home-modern" class="h-5 w-5 text-primary-600" />
<span class="text-primary-700">Unità immobiliare</span> <span class="text-primary-700">Unità immobiliare</span>
@if($isArchivedUnita)
<div class="flex items-end gap-2"> <span class="rounded-full border border-amber-300 bg-amber-50 px-2 py-0.5 text-xs font-semibold text-amber-700">Archiviata</span>
<div class="flex flex-col items-center gap-1"> @endif
<span class="text-[10px] text-gray-500">Prec.</span>
@if($prevUrl)
<a class="fi-btn fi-btn-color-gray fi-btn-size-sm" href="{{ $prevUrl }}">&larr;</a>
@else
<span class="fi-btn fi-btn-color-gray fi-btn-size-sm opacity-40">&larr;</span>
@endif
</div>
<div class="min-w-72 max-w-xl flex flex-col items-center gap-1">
<span class="text-[10px] text-gray-500">Unità</span>
<div class="w-full">{{ $this->getSchema('unitaPicker') }}</div>
</div>
<div class="flex flex-col items-center gap-1">
<span class="text-[10px] text-gray-500">Succ.</span>
@if($nextUrl)
<a class="fi-btn fi-btn-color-gray fi-btn-size-sm" href="{{ $nextUrl }}">&rarr;</a>
@else
<span class="fi-btn fi-btn-color-gray fi-btn-size-sm opacity-40">&rarr;</span>
@endif
</div>
</div>
</div> </div>
</div> </div>
@php @php