Google rubrica import preview and command unification

This commit is contained in:
michele 2026-04-08 19:17:16 +00:00
parent 478c0a120b
commit dba907a85f
2 changed files with 60 additions and 137 deletions

View File

@ -15,6 +15,7 @@
use Filament\Widgets\Widget;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
@ -46,6 +47,12 @@ class GoogleWorkspaceOverview extends Widget
public bool $showTechnicalBoxes = true;
public ?string $rubricaSyncCommandPreview = null;
public ?string $rubricaSyncLastOutput = null;
public ?string $rubricaSyncLastRunLabel = null;
public function mount(): void
{
$amministratore = $this->resolveAmministratore();
@ -62,6 +69,7 @@ public function mount(): void
$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) {
$this->errorMessage = 'Google non e collegato. Vai su Scheda amministratore per completare il collegamento.';
@ -99,7 +107,17 @@ public function getTicketGestioneUrl(int $ticketId): string
return TicketGestione::getUrl(['ticket' => $ticketId, 'tab' => 'scheda'], panel: 'admin-filament');
}
public function previewImportContactsToRubrica(): void
{
$this->runRubricaSync(true);
}
public function importContactsToRubrica(): void
{
$this->runRubricaSync(false);
}
private function runRubricaSync(bool $dryRun): void
{
$amministratore = $this->resolveAmministratore();
@ -118,154 +136,39 @@ public function importContactsToRubrica(): void
if ($token === null) {
Notification::make()
->title('Import rubrica non riuscito')
->title($dryRun ? 'Preview rubrica non disponibile' : 'Import rubrica non riuscito')
->warning()
->body($this->errorMessage ?? 'Token Google non disponibile.')
->send();
return;
}
$nextPageToken = null;
$created = 0;
$updated = 0;
$params = [
'--admin-id' => [(string) $amministratore->id],
'--max-pages' => 20,
];
// Import limitato a 5 pagine per evitare timeout dashboard.
for ($page = 0; $page < 5; $page++) {
$response = Http::withToken($token)
->acceptJson()
->get('https://people.googleapis.com/v1/people/me/connections', [
'personFields' => 'names,emailAddresses,phoneNumbers',
'pageSize' => 200,
'pageToken' => $nextPageToken,
]);
if ($response->status() === 401) {
$token = $this->resolveAccessToken($amministratore, $google, $oauth, true) ?? $token;
$response = Http::withToken($token)
->acceptJson()
->get('https://people.googleapis.com/v1/people/me/connections', [
'personFields' => 'names,emailAddresses,phoneNumbers',
'pageSize' => 200,
'pageToken' => $nextPageToken,
]);
}
if (! $response->successful()) {
Notification::make()
->title('Import rubrica interrotto')
->warning()
->body($this->googleErrorBody($response, 'Google People API ha restituito un errore durante l import.'))
->send();
return;
}
$connections = $response->json('connections');
if (! is_array($connections) || count($connections) === 0) {
break;
}
foreach ($connections as $person) {
$name = trim((string) Arr::get($person, 'names.0.displayName', ''));
$email = trim((string) Arr::get($person, 'emailAddresses.0.value', ''));
$phone = trim((string) Arr::get($person, 'phoneNumbers.0.value', ''));
if ($name === '' && $email === '' && $phone === '') {
continue;
}
[$nome, $cognome] = $this->splitDisplayName($name, $email);
$categoria = $this->inferCategoria($name, $email);
$tipoContatto = $this->inferTipoContatto($name, $email);
$today = now()->toDateString();
$existing = RubricaUniversale::withTrashed()
->where(function ($q) use ($email, $phone): void {
if ($email !== '') {
$q->orWhere('email', $email);
}
if ($phone !== '') {
$q->orWhere('telefono_cellulare', $phone)
->orWhere('telefono_ufficio', $phone)
->orWhere('telefono_casa', $phone);
}
})
->first();
if ($existing instanceof RubricaUniversale) {
if (method_exists($existing, 'trashed') && $existing->trashed()) {
$existing->restore();
}
$changed = false;
if (($existing->nome ?? '') === '' && $nome !== '') {
$existing->nome = $nome;
$changed = true;
}
if (($existing->cognome ?? '') === '' && $cognome !== '') {
$existing->cognome = $cognome;
$changed = true;
}
if (($existing->email ?? '') === '' && $email !== '') {
$existing->email = $email;
$changed = true;
}
if (($existing->telefono_cellulare ?? '') === '' && $phone !== '') {
$existing->telefono_cellulare = $phone;
$changed = true;
}
if (($existing->categoria ?? '') === '' || $existing->categoria === 'altro') {
$existing->categoria = $categoria;
$changed = true;
}
if (($existing->tipo_contatto ?? '') === '') {
$existing->tipo_contatto = $tipoContatto;
$changed = true;
}
if (($existing->note ?? '') === '') {
$existing->note = 'Origine contatto: Google Workspace';
$changed = true;
}
if ($changed) {
$existing->data_ultima_modifica = $today;
$existing->modificato_da = Auth::id();
$existing->save();
$updated++;
}
continue;
}
RubricaUniversale::create([
'nome' => $nome,
'cognome' => $cognome,
'tipo_contatto' => $tipoContatto,
'telefono_cellulare' => $phone !== '' ? $phone : null,
'email' => $email !== '' ? $email : null,
'categoria' => $categoria,
'stato' => 'attivo',
'note' => 'Origine contatto: Google Workspace',
'data_inserimento' => $today,
'data_ultima_modifica' => $today,
'creato_da' => Auth::id(),
'modificato_da' => Auth::id(),
]);
$created++;
}
$nextPageToken = $response->json('nextPageToken');
if (! is_string($nextPageToken) || $nextPageToken === '') {
break;
}
if ($dryRun) {
$params['--dry-run'] = true;
}
$this->loadContactsPreview($token);
$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->rubricaSyncCommandPreview = 'php artisan google:sync-rubrica --admin-id=' . (int) $amministratore->id . ' --max-pages=20' . ($dryRun ? ' --dry-run' : '');
if (! $dryRun) {
$this->loadContactsPreview($token);
}
Notification::make()
->title('Import rubrica completato')
->success()
->body("Creati {$created} contatti, aggiornati {$updated} contatti.")
->title($exit === 0
? ($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

@ -17,6 +17,9 @@
<a href="{{ $this->getRubricaUrl() }}" class="inline-flex items-center rounded-md bg-gray-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600">
Apri rubrica interna
</a>
<button type="button" wire:click="previewImportContactsToRubrica" class="inline-flex items-center rounded-md bg-slate-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-600">
Preview import rubrica
</button>
<button type="button" wire:click="importContactsToRubrica" class="inline-flex items-center rounded-md bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-500">
Importa in Rubrica NetGescon
</button>
@ -95,6 +98,23 @@
@endif
</div>
@if(($showTechnicalBoxes ?? true) === true)
<div class="rounded-lg border border-gray-200 p-3">
<div class="mb-2 text-sm font-semibold text-gray-800">Import rubrica operativo</div>
<div class="text-xs text-gray-500">Usa `Preview` per controllare cosa entrerebbe in rubrica, poi `Importa` per applicare davvero usando il comando unificato lato Laravel.</div>
<pre class="mt-3 overflow-x-auto rounded-lg bg-slate-900 p-3 text-xs text-slate-100">{{ $rubricaSyncCommandPreview ?: 'php artisan google:sync-rubrica --admin-id=... --max-pages=20 --dry-run' }}</pre>
@if($rubricaSyncLastRunLabel)
<div class="mt-3 text-[11px] font-semibold uppercase tracking-wide text-slate-500">Ultima esecuzione</div>
<div class="mt-1 text-xs text-slate-700">{{ $rubricaSyncLastRunLabel }}</div>
@endif
@if($rubricaSyncLastOutput)
<pre class="mt-3 max-h-72 overflow-auto rounded-lg bg-slate-50 p-3 text-xs text-slate-700">{{ $rubricaSyncLastOutput }}</pre>
@endif
</div>
@endif
<div class="rounded-lg border border-gray-200 p-3">
<div class="mb-2 text-sm font-semibold text-gray-800">Ticket aperti (operativi)</div>