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

View File

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

View File

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

View File

@ -424,16 +424,16 @@ private function syncAdditionalGoogleChannels(RubricaUniversale $rubrica, array
$tipo = str_contains($value, '@cert.') ? 'pec' : 'email';
RubricaContattoCanale::query()->updateOrCreate(
[
'rubrica_id' => (int) $rubrica->id,
'tipo' => $tipo,
'valore' => $value,
'stabile_id' => null,
'rubrica_id' => (int) $rubrica->id,
'tipo' => $tipo,
'valore' => $value,
'stabile_id' => null,
'unita_immobiliare_id' => null,
],
[
'etichetta' => $tipo === 'pec' ? 'PEC Google' : 'Email Google',
'etichetta' => $tipo === 'pec' ? 'PEC Google' : 'Email Google',
'is_principale' => $index === 0,
'meta' => ['source' => 'google_workspace'],
'meta' => ['source' => 'google_workspace'],
]
);
}
@ -450,21 +450,21 @@ private function syncAdditionalGoogleChannels(RubricaUniversale $rubrica, array
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;
RubricaContattoCanale::query()->updateOrCreate(
[
'rubrica_id' => (int) $rubrica->id,
'tipo' => $tipo,
'valore' => $value,
'stabile_id' => null,
'rubrica_id' => (int) $rubrica->id,
'tipo' => $tipo,
'valore' => $value,
'stabile_id' => null,
'unita_immobiliare_id' => null,
],
[
'etichetta' => $tipo === 'cellulare' ? 'Cellulare Google' : 'Telefono Google',
'etichetta' => $tipo === 'cellulare' ? 'Cellulare Google' : 'Telefono Google',
'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 !== ''
&& $this->normalizeComparableString((string) ($fornitore->ragione_sociale ?? '')) === $normalizedRagione;
&& $this->normalizeComparableString((string) ($fornitore->ragione_sociale ?? '')) === $normalizedRagione;
})->values();
if ($filtered->count() === 1) {

View File

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

View File

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

View File

@ -162,6 +162,15 @@ public function getProdottiUrl(): string
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
{
$query = TicketIntervento::query()

View File

@ -2,6 +2,7 @@
namespace App\Filament\Pages\Fornitore;
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
use App\Models\AssistenzaTecnorepairScheda;
use App\Models\Fornitore;
use App\Models\FornitoreDipendente;
use App\Models\TicketAttachment;
@ -95,13 +96,22 @@ public function refreshData(): void
$query = $this->buildBaseQuery($fornitore, $dipendente);
$this->applyStatusFilter($query, $this->status);
$this->rows = $query
$ticketRows = $query
->limit(100)
->get()
->map(function (TicketIntervento $intervento): array {
$base = $this->buildInterventoRow($intervento);
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,
'ticket_id' => (int) $intervento->ticket_id,
'titolo' => (string) ($intervento->ticket->titolo ?? '-'),
@ -110,17 +120,34 @@ public function refreshData(): void
'operatore' => (string) $intervento->operatore_assegnato_label,
'tempo_minuti' => (int) ($intervento->tempo_minuti ?? 0),
'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'),
]);
})
->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 = [
'aperti' => (clone $this->buildBaseQuery($fornitore, $dipendente))
->whereNotIn('stato', ['chiuso'])
->count() + AssistenzaTecnorepairScheda::query()
->where('fornitore_id', (int) $fornitore->id)
->whereIn('status_bucket', ['open', 'waiting'])
->count(),
'chiusi' => (clone $this->buildBaseQuery($fornitore, $dipendente))
->whereIn('stato', ['chiuso', 'fatturato'])
->count() + AssistenzaTecnorepairScheda::query()
->where('fornitore_id', (int) $fornitore->id)
->where('status_bucket', 'closed')
->count(),
'fatturabili' => (clone $this->buildBaseQuery($fornitore, $dipendente))
->whereIn('stato', ['fatturabile', 'fatturato'])
@ -152,6 +179,15 @@ public function getContabilitaUrl(): string
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
{
if (! $this->fornitoreId) {
@ -248,4 +284,79 @@ protected function applyStatusFilter(Builder $query, string $status): void
$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->rubrica_id = $rubricaId;
$dipendente->nome = (string) ($rubrica->nome ?: $rubrica->ragione_sociale ?: 'Dipendente');
$dipendente->cognome = trim((string) ($rubrica->cognome ?? '')) !== '' ? (string) $rubrica->cognome : null;
$dipendente->email = $email !== '' ? $email : null;
$dipendente->telefono = trim((string) ($rubrica->telefono_cellulare ?: $rubrica->telefono_ufficio ?: $rubrica->telefono_casa)) ?: null;
$dipendente->attivo = true;
$dipendente->updated_by_user_id = Auth::id();
$dipendente->rubrica_id = $rubricaId;
$dipendente->nome = (string) ($rubrica->nome ?: $rubrica->ragione_sociale ?: 'Dipendente');
$dipendente->cognome = trim((string) ($rubrica->cognome ?? '')) !== '' ? (string) $rubrica->cognome : null;
$dipendente->email = $email !== '' ? $email : null;
$dipendente->telefono = trim((string) ($rubrica->telefono_cellulare ?: $rubrica->telefono_ufficio ?: $rubrica->telefono_casa)) ?: null;
$dipendente->attivo = true;
$dipendente->updated_by_user_id = Auth::id();
$dipendente->save();
$this->resetDipendenteRubricaSelection();

View File

@ -28,6 +28,6 @@ public static function canAccess(): bool
$user = Auth::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();
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\Fornitore;
use App\Models\RubricaUniversale;
use App\Services\Anagrafiche\RubricaPhoneLookupService;
use App\Models\Stabile;
use App\Models\Ticket;
use App\Models\User;
use App\Services\Anagrafiche\RubricaPhoneLookupService;
use App\Support\PhoneNumber;
use App\Support\StabileContext;
use BackedEnum;

View File

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

View File

@ -1087,17 +1087,17 @@ private function loadGitWorkspaceStatus(): void
{
$repoPath = $this->resolveGitWorkspacePath();
$this->gitWorkspacePath = $repoPath;
$this->gitCurrentBranch = $this->runGitProcessAt($repoPath, ['rev-parse', '--abbrev-ref', '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->gitCurrentCommit = $this->gitSourceCommit;
$this->gitCurrentCommitDate = $this->gitSourceCommitDate;
$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->gitWorkingTreeDirty = trim((string) ($this->runGitProcessAt($repoPath, ['status', '--porcelain']) ?? '')) !== '';
$this->gitAheadCount = 0;
$this->gitBehindCount = 0;
$this->gitWorkspacePath = $repoPath;
$this->gitCurrentBranch = $this->runGitProcessAt($repoPath, ['rev-parse', '--abbrev-ref', '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->gitCurrentCommit = $this->gitSourceCommit;
$this->gitCurrentCommitDate = $this->gitSourceCommitDate;
$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->gitWorkingTreeDirty = trim((string) ($this->runGitProcessAt($repoPath, ['status', '--porcelain']) ?? '')) !== '';
$this->gitAheadCount = 0;
$this->gitBehindCount = 0;
$this->gitStatusUsesSyncSummary = false;
$currentRef = 'HEAD';
@ -1106,11 +1106,11 @@ private function loadGitWorkspaceStatus(): void
if (is_file($summaryPath)) {
$summary = json_decode((string) @file_get_contents($summaryPath), true);
if (is_array($summary)) {
$summaryMode = (string) ($summary['mode'] ?? '');
$summaryStatus = (string) ($summary['status'] ?? '');
$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'] : '';
$syncDate = isset($summary['synced_at']) ? (string) $summary['synced_at'] : null;
$summaryMode = (string) ($summary['mode'] ?? '');
$summaryStatus = (string) ($summary['status'] ?? '');
$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'] : '';
$syncDate = isset($summary['synced_at']) ? (string) $summary['synced_at'] : 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
? $selected
: $group
->sortByDesc(fn(RubricaUniversale $match): array => [
(int) (! empty($match->amministratore_id)),
(int) (! empty($match->riferimento_stabile)),
(int) (! empty($match->riferimento_unita)),
(int) (($match->categoria ?? '') === 'condomino'),
-1 * (int) $match->id,
])
->first();
->sortByDesc(fn(RubricaUniversale $match): array=> [
(int) (! empty($match->amministratore_id)),
(int) (! empty($match->riferimento_stabile)),
(int) (! empty($match->riferimento_unita)),
(int) (($match->categoria ?? '') === 'condomino'),
-1 * (int) $match->id,
])
->first();
$duplicateCategories = $group
->pluck('categoria')
@ -1274,7 +1274,6 @@ private function resolveCallerStabile(RubricaUniversale $caller, ?int $fallbackS
return $fallbackStabileId ? Stabile::query()->find($fallbackStabileId) : null;
}
private function resolveCallerSoggetto(RubricaUniversale $caller, ?int $stabileId = null): ?Soggetto
{
$queries = [];

View File

@ -23,10 +23,12 @@
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Auth;
@ -56,6 +58,8 @@ public static function canAccess(): bool
public ?array $unitaPickerData = [];
public string $unitaVisibilita = 'attive';
/**
* @var array<int, string>
*/
@ -139,6 +143,8 @@ public function mount(): void
}
$this->stabileId = (int) $stabileId;
$this->unitaVisibilita = $this->normalizeUnitaVisibilita(request()->query('visibilita'));
$this->refreshUnitaOptions();
$this->unitaId = $this->pickUnitaId(request()->integer('unita_id'));
@ -240,6 +246,60 @@ protected function getHeaderActions(): array
$nextId = $this->getNextUnitaId();
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')
->label('Precedente')
->icon('heroicon-o-arrow-left')
@ -529,15 +589,32 @@ public function setTab(string $tab): void
$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
{
$unita = UnitaImmobiliare::query()
->where('stabile_id', $this->stabileId)
$unita = $this->buildUnitaIndexQuery()
->orderBy('palazzina_id')
->orderBy('scala')
->orderBy('piano')
->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 {
$denominazione = trim((string) ($u->denominazione ?? ''));
@ -649,6 +726,9 @@ protected function refreshUnitaOptions(): void
if (is_string($owner) && trim($owner) !== '') {
$label .= ' — ' . trim($owner);
}
if ($u->trashed()) {
$label .= ' [archiviata]';
}
return [(int) $u->id => $label];
})
->all();
@ -730,14 +810,15 @@ protected function loadUnita(): void
return;
}
$this->unita = UnitaImmobiliare::with([
'palazzinaObj',
'soggetti',
'stabile',
'dettagliMillesimi.tabellaMillesimale',
'relazioniPersoneAttive.persona.emailMultiple',
'unitaRecapitiServizio.persona',
])
$this->unita = UnitaImmobiliare::withTrashed()
->with([
'palazzinaObj',
'soggetti',
'stabile',
'dettagliMillesimi.tabellaMillesimale',
'relazioniPersoneAttive.persona.emailMultiple',
'unitaRecapitiServizio.persona',
])
->where('stabile_id', $this->stabileId)
->whereKey($this->unitaId)
->first();
@ -758,6 +839,79 @@ protected function loadUnita(): void
$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
{
$this->nominativiStorici = [];

View File

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

View File

@ -18,10 +18,10 @@ class TicketMobileOverview extends Widget
/** @var array<string,int> */
public array $ticketCounters = [
'open' => 0,
'open' => 0,
'urgent' => 0,
'closed' => 0,
'all' => 0,
'all' => 0,
];
public static function canView(): bool
@ -29,7 +29,7 @@ public static function canView(): bool
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}
public function mount(): void
@ -53,10 +53,10 @@ public function mount(): void
$base = Ticket::query()->whereIn('stabile_id', $stabileIds);
$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(),
'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');
}
}
}

View File

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

View File

@ -102,7 +102,18 @@ protected static function booted(): void
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);
}
} catch (\Throwable) {

View File

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

View File

@ -1,5 +1,4 @@
<?php
namespace App\Services\Support;
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([
'maxResults' => max(1, min($maxMessages, 50)),
'q' => $query !== '' ? $query : null,
], static fn ($value) => $value !== null));
], static fn($value) => $value !== null));
if (! $response->successful()) {
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;
$ticket = Ticket::query()->create([
'stabile_id' => (int) $stabile->id,
'aperto_da_user_id' => $openingUserId,
'titolo' => Str::limit($subject !== '' ? $subject : ('Email importata da ' . ($from !== '' ? $from : 'Gmail')), 255),
'descrizione' => $this->buildTicketDescription($stabile, $mailbox, $subject, $from, $to, $date, $bodyText),
'categoria_ticket_id' => null,
'luogo_intervento' => 'Casella email stabile',
'data_apertura' => $this->normalizeDate($date) ?? now(),
'stato' => 'Aperto',
'priorita' => 'Media',
'stabile_id' => (int) $stabile->id,
'aperto_da_user_id' => $openingUserId,
'titolo' => Str::limit($subject !== '' ? $subject : ('Email importata da ' . ($from !== '' ? $from : 'Gmail')), 255),
'descrizione' => $this->buildTicketDescription($stabile, $mailbox, $subject, $from, $to, $date, $bodyText),
'categoria_ticket_id' => null,
'luogo_intervento' => 'Casella email stabile',
'data_apertura' => $this->normalizeDate($date) ?? now(),
'stato' => 'Aperto',
'priorita' => 'Media',
]);
$rawMessage = $this->fetchMessage($token, $gmailId, 'raw');
@ -113,10 +112,10 @@ public function importMailbox(Stabile $stabile, array $mailbox, int $maxMessages
'inviato_il' => $this->normalizeDate($date),
'eml_documento_id' => $documento->id,
'metadata' => [
'gmail_message_id' => $gmailId,
'gmail_thread_id' => Arr::get($message, 'threadId'),
'gmail_mailbox_email' => $mailbox['email'] ?? null,
'google_account_key' => $mailbox['google_account_key'] ?? null,
'gmail_message_id' => $gmailId,
'gmail_thread_id' => Arr::get($message, 'threadId'),
'gmail_mailbox_email' => $mailbox['email'] ?? 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',
'received_at' => $this->normalizeDate($date),
'metadata' => [
'gmail_message_id' => $gmailId,
'gmail_thread_id' => Arr::get($message, 'threadId'),
'gmail_mailbox_email' => $mailbox['email'] ?? null,
'google_account_key' => $mailbox['google_account_key'] ?? null,
'subject' => $subject,
'to' => $to,
'documento_id' => (int) $documento->id,
'gmail_message_id' => $gmailId,
'gmail_thread_id' => Arr::get($message, 'threadId'),
'gmail_mailbox_email' => $mailbox['email'] ?? null,
'google_account_key' => $mailbox['google_account_key'] ?? null,
'subject' => $subject,
'to' => $to,
'documento_id' => (int) $documento->id,
],
]);
@ -251,10 +250,10 @@ private function storeAttachments(string $token, string $gmailId, Ticket $ticket
'ticket-email/' . $ticket->id,
$fileName,
[
'mime' => (string) ($part['mimeType'] ?? ''),
'optimize_image' => false,
'display_name' => $fileName,
'stored_basename' => pathinfo($fileName, PATHINFO_FILENAME),
'mime' => (string) ($part['mimeType'] ?? ''),
'optimize_image' => false,
'display_name' => $fileName,
'stored_basename' => pathinfo($fileName, PATHINFO_FILENAME),
]
);
@ -414,4 +413,4 @@ private function normalizeDate(mixed $value): mixed
return null;
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
{
"resources/css/app.css": {
"file": "assets/app-BGt-S40c.css",
"file": "assets/app-tgbzBtFO.css",
"src": "resources/css/app.css",
"isEntry": true
},
"resources/css/filament/admin/theme.css": {
"file": "assets/theme-D-E95I9r.css",
"file": "assets/theme-EkdZtrh_.css",
"src": "resources/css/filament/admin/theme.css",
"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->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->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>
</div>
</div>

View File

@ -15,6 +15,7 @@
<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="{{ $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>
</div>
</div>
@ -48,10 +49,14 @@
<table class="min-w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-50">
<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">Contatto</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">Operatore</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">Ingresso</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">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">Aggiornato</th>
<th class="px-4 py-3"></th>
@ -59,34 +64,46 @@
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
@forelse($rows as $row)
<tr>
<td class="px-4 py-3 align-top">
<div class="font-medium">#{{ $row['ticket_id'] }} - {{ $row['titolo'] }}</div>
<div class="text-xs text-gray-500">{{ $row['stabile'] }} · ingresso {{ $row['ingresso'] }}</div>
<tr class="{{ $row['row_class'] }}">
<td class="px-4 py-3 align-top font-medium">
{{ $row['numero'] }}
<div class="mt-1 text-[11px] text-slate-500">{{ $row['origine'] }}</div>
</td>
<td class="px-4 py-3 align-top text-gray-700">{{ $row['ingresso'] }}</td>
<td class="px-4 py-3 align-top">
<div>{{ $row['contatto'] }}</div>
<div class="text-xs text-gray-500">{{ $row['telefono'] !== '' ? $row['telefono'] : 'Telefono non disponibile' }}</div>
<div class="font-medium">{{ $row['cliente'] }}</div>
<div class="text-[11px] text-slate-500">{{ $row['stabile'] }}</div>
</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">
<div>{{ $row['problema'] }}</div>
<div class="text-xs text-gray-500">Apparato: {{ $row['apparato'] }}</div>
<div>{{ $row['modello'] }}</div>
@if(($row['titolo'] ?? '') !== '')
<div class="text-[11px] text-slate-500">{{ $row['titolo'] }}</div>
@endif
</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">
<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 class="px-4 py-3 align-top text-gray-600">{{ $row['updated_at'] }}</td>
<td class="px-4 py-3 text-right align-top">
<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>
<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>
@if($row['row_type'] === 'ticket')
<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>
</td>
</tr>
@empty
<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>
@endforelse
</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
$unitaOptions = $this->getUnitaOptions();
$prevUrl = $this->getPrevUnitaUrl();
$nextUrl = $this->getNextUnitaUrl();
$visibilityOptions = [
'attive' => 'Attive',
'tutte' => 'Tutte',
'archiviate' => 'Archiviate',
];
@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)
<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
@ -38,8 +86,7 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
{{-- Header compatto --}}
<x-filament::section class="p-4!">
@php
$prevUrl = $this->getPrevUnitaUrl();
$nextUrl = $this->getNextUnitaUrl();
$isArchivedUnita = method_exists($unita, 'trashed') && $unita->trashed();
@endphp
<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">
<x-filament::icon icon="heroicon-o-home-modern" class="h-5 w-5 text-primary-600" />
<span class="text-primary-700">Unità immobiliare</span>
<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-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>
@if($isArchivedUnita)
<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>
@endif
</div>
</div>
@php