From 2810d2c70ef147e78f2a0d0a394f93e95a9370fe Mon Sep 17 00:00:00 2001 From: michele Date: Wed, 15 Apr 2026 06:05:10 +0000 Subject: [PATCH] Import supplier customer directory from TecnoRepair legacy --- ...TecnoRepairImportRubricaClientiCommand.php | 441 ++++++++++++++++++ .../Pages/Fornitore/RubricaClienti.php | 217 +++++++++ app/Models/FornitoreCliente.php | 58 +++ ..._190000_create_fornitore_clienti_table.php | 48 ++ .../pages/fornitore/rubrica-clienti.blade.php | 240 +++++++--- 5 files changed, 947 insertions(+), 57 deletions(-) create mode 100644 app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php create mode 100644 app/Models/FornitoreCliente.php create mode 100644 database/migrations/2026_04_14_190000_create_fornitore_clienti_table.php diff --git a/app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php b/app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php new file mode 100644 index 0000000..dbf77e8 --- /dev/null +++ b/app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php @@ -0,0 +1,441 @@ +resolveAdmin((string) $this->argument('amministratore')); + if (! $admin instanceof Amministratore) { + $this->error('Amministratore non trovato.'); + + return self::FAILURE; + } + + $fornitore = $this->resolveFornitore($admin, (string) $this->option('fornitore-id'), (string) $this->option('fornitore-term')); + if (! $fornitore instanceof Fornitore) { + $this->error('Fornitore non trovato per questo amministratore.'); + + return self::FAILURE; + } + + $dryRun = (bool) $this->option('dry-run'); + + $candidates = AssistenzaTecnorepairScheda::query() + ->where('amministratore_id', (int) $admin->id) + ->where('fornitore_id', (int) $fornitore->id) + ->orderByDesc('date_received') + ->orderByDesc('id') + ->get(); + + $customers = $this->collectUniqueCustomers($candidates); + + $stats = [ + 'legacy_rows' => $candidates->count(), + 'clienti_distinti' => $customers->count(), + 'rubrica_create' => 0, + 'rubrica_match' => 0, + 'rubrica_update' => 0, + 'fornitore_rows' => 0, + ]; + + $runner = function () use ($customers, $fornitore, $admin, &$stats): void { + foreach ($customers as $customer) { + $rubrica = $this->resolveOrCreateRubrica($admin, $customer, $stats); + + FornitoreCliente::query()->updateOrCreate( + [ + 'fornitore_id' => (int) $fornitore->id, + 'legacy_cliente_id' => $customer['legacy_cliente_id'], + ], + [ + 'amministratore_id' => (int) $admin->id, + 'rubrica_id' => $rubrica?->id, + 'display_name' => $customer['display_name'], + 'phone' => $customer['phone'], + 'phone_alt' => $customer['phone_alt'], + 'email' => $customer['email'], + 'indirizzo' => $customer['indirizzo'], + 'cap' => $customer['cap'], + 'citta' => $customer['citta'], + 'provincia' => $customer['provincia'], + 'partita_iva' => $customer['partita_iva'], + 'codice_fiscale' => $customer['codice_fiscale'], + 'note' => $customer['note'], + 'source' => 'tecnorepair_tclienti', + 'imported_from_path' => $customer['imported_from_path'], + 'imported_at' => now(), + 'metadata' => $customer['metadata'], + ] + ); + + $stats['fornitore_rows']++; + } + }; + + if ($dryRun) { + foreach ($customers as $customer) { + $this->resolveOrCreateRubrica($admin, $customer, $stats, true); + $stats['fornitore_rows']++; + } + } else { + DB::transaction($runner); + } + + $this->info('Import rubrica clienti completato.'); + $this->table(['Campo', 'Valore'], [ + ['Amministratore', (string) ($admin->denominazione_studio ?: trim((string) ($admin->nome ?? '') . ' ' . (string) ($admin->cognome ?? '')))], + ['Fornitore', (string) ($fornitore->ragione_sociale ?: ('Fornitore #' . $fornitore->id))], + ['Dry run', $dryRun ? 'si' : 'no'], + ['Schede legacy lette', (string) $stats['legacy_rows']], + ['Clienti distinti', (string) $stats['clienti_distinti']], + ['Rubrica nuova', (string) $stats['rubrica_create']], + ['Rubrica match', (string) $stats['rubrica_match']], + ['Rubrica aggiornata', (string) $stats['rubrica_update']], + ['Righe fornitore_clienti', (string) $stats['fornitore_rows']], + ]); + + return self::SUCCESS; + } + + private function resolveAdmin(string $value): ?Amministratore + { + $value = trim($value); + if ($value === '') { + return null; + } + + return Amministratore::query() + ->when(is_numeric($value), fn($query) => $query->orWhere('id', (int) $value)) + ->orWhere('codice_amministratore', $value) + ->orWhere('codice_univoco', $value) + ->first(); + } + + private function resolveFornitore(Amministratore $admin, string $fornitoreIdOption, string $fornitoreTermOption): ?Fornitore + { + $fornitoreId = (int) $fornitoreIdOption; + if ($fornitoreId > 0) { + return Fornitore::query() + ->where('amministratore_id', (int) $admin->id) + ->whereKey($fornitoreId) + ->first(); + } + + $term = trim($fornitoreTermOption); + if ($term === '') { + return null; + } + + return Fornitore::query() + ->where('amministratore_id', (int) $admin->id) + ->where(function ($query) use ($term): void { + $query->where('ragione_sociale', 'like', '%' . $term . '%') + ->orWhere('nome', 'like', '%' . $term . '%') + ->orWhere('cognome', 'like', '%' . $term . '%'); + }) + ->orderByDesc('is_tecnorepair_primary') + ->orderBy('ragione_sociale') + ->first(); + } + + /** + * @param Collection $rows + * @return Collection> + */ + private function collectUniqueCustomers(Collection $rows): Collection + { + $customers = []; + + foreach ($rows as $scheda) { + $cliente = is_array($scheda->metadata['cliente'] ?? null) ? $scheda->metadata['cliente'] : []; + + $legacyClienteId = $this->toInt($cliente['ID'] ?? $scheda->legacy_cliente_id); + $displayName = $this->cleanString($cliente['NomeCognome'] ?? $scheda->customer_name); + $phone = $this->cleanString($cliente['NumeroTelefono'] ?? $scheda->customer_phone); + $phoneAlt = $this->cleanString($cliente['TelFisso'] ?? $scheda->customer_phone_alt); + $email = $this->cleanString($cliente['Email'] ?? $scheda->customer_email); + $indirizzo = $this->cleanString($cliente['Indirizzo'] ?? null); + $cap = $this->cleanString($cliente['Cap'] ?? null); + $citta = $this->cleanString($cliente['Citta'] ?? null); + $provincia = $this->cleanString($cliente['Prov'] ?? null); + $partitaIva = $this->cleanString($cliente['PIVA'] ?? null); + $codiceFiscale = $this->cleanString($cliente['CodFis'] ?? null); + $note = $this->cleanString($cliente['Annotazioni'] ?? null); + + $identity = $legacyClienteId !== null + ? 'legacy-' . $legacyClienteId + : $this->buildIdentityKey($displayName, $email, $phone, $phoneAlt); + + if ($identity === '' || ($displayName === null && $phone === null && $email === null)) { + continue; + } + + if (! isset($customers[$identity])) { + $customers[$identity] = [ + 'legacy_cliente_id' => $legacyClienteId, + 'display_name' => $displayName, + 'phone' => $phone, + 'phone_alt' => $phoneAlt, + 'email' => $email, + 'indirizzo' => $indirizzo, + 'cap' => $cap, + 'citta' => $citta, + 'provincia' => $provincia, + 'partita_iva' => $partitaIva, + 'codice_fiscale' => $codiceFiscale, + 'note' => $note, + 'imported_from_path'=> $this->cleanString($scheda->imported_from_path), + 'metadata' => [ + 'cliente' => $cliente, + 'ultima_scheda_id' => (int) $scheda->id, + 'ultima_scheda_num' => (string) ($scheda->legacy_numero_scheda ?? ''), + ], + ]; + + continue; + } + + foreach (['display_name', 'phone', 'phone_alt', 'email', 'indirizzo', 'cap', 'citta', 'provincia', 'partita_iva', 'codice_fiscale', 'note', 'imported_from_path'] as $field) { + if (($customers[$identity][$field] ?? null) === null && ${Str::camel($field)} !== null) { + $customers[$identity][$field] = ${Str::camel($field)}; + } + } + } + + return collect(array_values($customers)); + } + + /** + * @param array $customer + * @param array $stats + */ + private function resolveOrCreateRubrica(Amministratore $admin, array $customer, array &$stats, bool $dryRun = false): ?RubricaUniversale + { + $query = RubricaUniversale::query()->where('amministratore_id', (int) $admin->id); + + $email = $this->cleanString($customer['email'] ?? null); + $phone = $this->normalizePhone($customer['phone'] ?? null); + $phoneAlt = $this->normalizePhone($customer['phone_alt'] ?? null); + $phoneForRubrica = $this->normalizePhoneField($customer['phone'] ?? null); + $phoneAltForRubrica = $this->normalizePhoneField($customer['phone_alt'] ?? null); + $cf = $this->cleanString($customer['codice_fiscale'] ?? null); + $piva = $this->cleanString($customer['partita_iva'] ?? null); + $name = $this->cleanString($customer['display_name'] ?? null); + $provincia = $this->normalizeProvince($customer['provincia'] ?? null); + + $rubrica = null; + + if ($cf !== null) { + $rubrica = (clone $query)->where('codice_fiscale', $cf)->first(); + } + if (! $rubrica && $piva !== null) { + $rubrica = (clone $query)->where('partita_iva', $piva)->first(); + } + if (! $rubrica && $email !== null) { + $rubrica = (clone $query)->where('email', $email)->first(); + } + if (! $rubrica && $phone !== null) { + $rubrica = (clone $query)->where(function ($phoneQuery) use ($phone): void { + $phoneQuery->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$phone]) + ->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$phone]) + ->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_casa, ''), '[^0-9]', '') = ?", [$phone]); + })->first(); + } + if (! $rubrica && $phoneAlt !== null) { + $rubrica = (clone $query)->where(function ($phoneQuery) use ($phoneAlt): void { + $phoneQuery->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$phoneAlt]) + ->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$phoneAlt]) + ->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_casa, ''), '[^0-9]', '') = ?", [$phoneAlt]); + })->first(); + } + if (! $rubrica && $name !== null) { + $rubrica = (clone $query)->where(function ($nameQuery) use ($name): void { + $nameQuery->whereRaw("LOWER(TRIM(CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, '')))) = ?", [Str::lower($name)]) + ->orWhereRaw("LOWER(TRIM(COALESCE(ragione_sociale, ''))) = ?", [Str::lower($name)]); + })->first(); + } + + if ($rubrica instanceof RubricaUniversale) { + $stats['rubrica_match']++; + if (! $dryRun) { + $dirty = false; + + foreach ([ + 'email' => $email, + 'telefono_cellulare' => $phoneForRubrica, + 'telefono_ufficio' => $phoneAltForRubrica, + 'indirizzo' => $this->cleanString($customer['indirizzo'] ?? null), + 'cap' => $this->cleanString($customer['cap'] ?? null), + 'citta' => $this->cleanString($customer['citta'] ?? null), + 'provincia' => $provincia, + 'partita_iva' => $piva, + 'codice_fiscale' => $cf, + ] as $field => $value) { + if ($value !== null && ! filled($rubrica->{$field})) { + $rubrica->{$field} = $value; + $dirty = true; + } + } + + if ($dirty) { + $rubrica->save(); + $stats['rubrica_update']++; + } + } + + return $rubrica; + } + + $stats['rubrica_create']++; + if ($dryRun) { + return null; + } + + [$nome, $cognome] = $this->splitDisplayName($name); + + return RubricaUniversale::query()->create([ + 'amministratore_id' => (int) $admin->id, + 'nome' => $nome, + 'cognome' => $cognome, + 'tipo_contatto' => 'persona_fisica', + 'codice_fiscale' => $cf, + 'partita_iva' => $piva, + 'indirizzo' => $this->cleanString($customer['indirizzo'] ?? null), + 'cap' => $this->cleanString($customer['cap'] ?? null), + 'citta' => $this->cleanString($customer['citta'] ?? null), + 'provincia' => $provincia, + 'telefono_cellulare' => $phoneForRubrica, + 'telefono_ufficio' => $phoneAltForRubrica, + 'email' => $email, + 'note' => $this->buildRubricaNote($customer), + 'categoria' => 'cliente', + 'stato' => 'attivo', + 'data_inserimento' => now(), + 'data_ultima_modifica' => now(), + 'creato_da' => auth()->id(), + 'modificato_da' => auth()->id(), + ]); + } + + private function splitDisplayName(?string $name): array + { + $name = trim((string) $name); + if ($name === '') { + return ['Cliente', 'TecnoRepair']; + } + + $parts = preg_split('/\s+/', $name) ?: []; + if (count($parts) <= 1) { + return [$name, '']; + } + + return [trim((string) array_shift($parts)), trim(implode(' ', $parts))]; + } + + private function buildRubricaNote(array $customer): string + { + $lines = array_filter([ + 'Import TecnoRepair TClienti per rubrica fornitore.', + isset($customer['legacy_cliente_id']) && $customer['legacy_cliente_id'] !== null ? 'Legacy cliente ID: ' . $customer['legacy_cliente_id'] : null, + $this->cleanString($customer['note'] ?? null), + ]); + + return implode("\n", $lines); + } + + private function buildIdentityKey(?string $displayName, ?string $email, ?string $phone, ?string $phoneAlt): string + { + if ($email !== null) { + return 'email-' . Str::lower($email); + } + + if ($phone !== null) { + return 'phone-' . preg_replace('/\D+/', '', $phone); + } + + if ($phoneAlt !== null) { + return 'phone-' . preg_replace('/\D+/', '', $phoneAlt); + } + + if ($displayName !== null) { + return 'name-' . Str::slug(Str::lower($displayName)); + } + + return ''; + } + + private function normalizePhone(mixed $value): ?string + { + $value = preg_replace('/\D+/', '', (string) $value) ?? ''; + + return $value !== '' ? $value : null; + } + + private function normalizePhoneField(mixed $value): ?string + { + $value = trim((string) $value); + if ($value === '') { + return null; + } + + if (preg_match('/\+?[0-9][0-9\s().\/-]{4,}/', $value, $matches) !== 1) { + return null; + } + + $normalized = preg_replace('/[^0-9+]/', '', (string) $matches[0]) ?? ''; + $normalized = ltrim($normalized, '+') === '' ? '' : $normalized; + + return $normalized !== '' ? Str::limit($normalized, 30, '') : null; + } + + private function normalizeProvince(mixed $value): ?string + { + $value = Str::upper(trim((string) $value)); + if ($value === '') { + return null; + } + + $value = preg_replace('/[^A-Z]/', '', $value) ?? ''; + if ($value === '') { + return null; + } + + return Str::limit($value, 2, ''); + } + + private function cleanString(mixed $value): ?string + { + $value = trim((string) $value); + + return $value !== '' ? $value : null; + } + + private function toInt(mixed $value): ?int + { + if ($value === null || $value === '' || ! is_numeric($value)) { + return null; + } + + return (int) $value; + } +} \ No newline at end of file diff --git a/app/Filament/Pages/Fornitore/RubricaClienti.php b/app/Filament/Pages/Fornitore/RubricaClienti.php index dfbf240..ac9e1b2 100644 --- a/app/Filament/Pages/Fornitore/RubricaClienti.php +++ b/app/Filament/Pages/Fornitore/RubricaClienti.php @@ -2,7 +2,10 @@ namespace App\Filament\Pages\Fornitore; use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext; +use App\Filament\Pages\Gescon\RubricaUniversaleScheda; +use App\Models\AssistenzaTecnorepairScheda; use App\Models\Fornitore; +use App\Models\FornitoreCliente; use App\Models\TicketIntervento; use App\Models\User; use BackedEnum; @@ -37,9 +40,21 @@ class RubricaClienti extends Page public string $search = ''; + public string $activeTab = 'elenco'; + + public ?int $selectedClienteId = null; + /** @var array> */ public array $rows = []; + /** @var array */ + public array $counters = [ + 'totali' => 0, + 'collegati' => 0, + 'con_email' => 0, + 'con_cellulare' => 0, + ]; + /** @var array|null */ public ?array $googleWorkspaceStatus = null; @@ -71,15 +86,151 @@ public function updatedSearch(): void $this->refreshRows(); } + public function selectTab(string $tab): void + { + if (! in_array($tab, ['elenco', 'scheda'], true)) { + return; + } + + $this->activeTab = $tab; + } + + public function selectCliente(int $clienteId): void + { + $this->selectedClienteId = $clienteId > 0 ? $clienteId : null; + $this->activeTab = 'scheda'; + } + public function refreshRows(): void { if (! $this->fornitoreId) { $this->rows = []; + $this->counters = ['totali' => 0, 'collegati' => 0, 'con_email' => 0, 'con_cellulare' => 0]; return; } $term = Str::lower(trim($this->search)); + $legacyCounts = AssistenzaTecnorepairScheda::query() + ->where('fornitore_id', $this->fornitoreId) + ->selectRaw('legacy_cliente_id, COUNT(*) as totale_schede, MAX(date_received) as ultima_data') + ->groupBy('legacy_cliente_id') + ->get() + ->mapWithKeys(fn(AssistenzaTecnorepairScheda $row): array => [ + (int) ($row->legacy_cliente_id ?? 0) => [ + 'totale_schede' => (int) ($row->totale_schede ?? 0), + 'ultima_data' => optional($row->ultima_data)->format('d/m/Y') ?: '-', + ], + ]) + ->all(); + + $items = FornitoreCliente::query() + ->with('rubrica:id,nome,cognome,ragione_sociale,email,telefono_cellulare,telefono_ufficio,categoria') + ->where('fornitore_id', $this->fornitoreId) + ->when($term !== '', function ($query) use ($term): void { + $like = '%' . $term . '%'; + + $query->where(function ($inner) use ($like): void { + $inner->where('display_name', 'like', $like) + ->orWhere('phone', 'like', $like) + ->orWhere('phone_alt', 'like', $like) + ->orWhere('email', 'like', $like) + ->orWhere('citta', 'like', $like) + ->orWhere('indirizzo', 'like', $like); + }); + }) + ->orderByRaw('CASE WHEN rubrica_id IS NULL THEN 1 ELSE 0 END') + ->orderBy('display_name') + ->limit(500) + ->get(); + + if ($items->isEmpty()) { + $this->rows = $this->buildTicketFallbackRows($term); + $this->counters = [ + 'totali' => count($this->rows), + 'collegati' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['rubrica_id'] ?? null))->count(), + 'con_email' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['email'] ?? null))->count(), + 'con_cellulare' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['phone'] ?? null))->count(), + ]; + + $this->selectedClienteId = null; + return; + } + + $this->rows = $items->map(function (FornitoreCliente $cliente) use ($legacyCounts): array { + $legacy = $legacyCounts[(int) ($cliente->legacy_cliente_id ?? 0)] ?? ['totale_schede' => 0, 'ultima_data' => '-']; + + return [ + 'id' => (int) $cliente->id, + 'legacy_cliente_id'=> (int) ($cliente->legacy_cliente_id ?? 0), + 'display_name' => (string) ($cliente->display_name ?: 'Cliente TecnoRepair'), + 'phone' => (string) ($cliente->phone ?? ''), + 'phone_alt' => (string) ($cliente->phone_alt ?? ''), + 'email' => (string) ($cliente->email ?? ''), + 'indirizzo' => (string) ($cliente->indirizzo ?? ''), + 'citta' => (string) ($cliente->citta ?? ''), + 'provincia' => (string) ($cliente->provincia ?? ''), + 'rubrica_id' => (int) ($cliente->rubrica_id ?? 0), + 'rubrica_url' => $cliente->rubrica_id ? RubricaUniversaleScheda::getUrl(['record' => (int) $cliente->rubrica_id], panel: 'admin-filament') : null, + 'rubrica_label' => (string) ($cliente->rubrica?->categoria ?? ''), + 'totale_schede' => (int) ($legacy['totale_schede'] ?? 0), + 'ultima_data' => (string) ($legacy['ultima_data'] ?? '-'), + 'updated_at' => optional($cliente->updated_at)->format('d/m/Y H:i') ?: '-', + 'source' => 'tecnorepair_tclienti', + ]; + })->all(); + + $this->counters = [ + 'totali' => count($this->rows), + 'collegati' => (int) collect($this->rows)->filter(fn(array $row): bool => (int) ($row['rubrica_id'] ?? 0) > 0)->count(), + 'con_email' => (int) collect($this->rows)->filter(fn(array $row): bool => trim((string) ($row['email'] ?? '')) !== '')->count(), + 'con_cellulare' => (int) collect($this->rows)->filter(fn(array $row): bool => trim((string) ($row['phone'] ?? '')) !== '')->count(), + ]; + + if ((int) ($this->selectedClienteId ?? 0) <= 0 && $this->rows !== []) { + $this->selectedClienteId = (int) $this->rows[0]['id']; + } + } + + public function getSelectedClienteProperty(): ?FornitoreCliente + { + $selectedId = (int) ($this->selectedClienteId ?? 0); + if ($selectedId <= 0 || ! $this->fornitoreId) { + return null; + } + + return FornitoreCliente::query() + ->with('rubrica') + ->where('fornitore_id', $this->fornitoreId) + ->find($selectedId); + } + + /** @return array> */ + public function getSelectedClienteSchedeProperty(): array + { + $cliente = $this->selectedCliente; + if (! $cliente instanceof FornitoreCliente || ! $this->fornitoreId) { + return []; + } + + return AssistenzaTecnorepairScheda::query() + ->where('fornitore_id', $this->fornitoreId) + ->when((int) ($cliente->legacy_cliente_id ?? 0) > 0, fn($query) => $query->where('legacy_cliente_id', (int) $cliente->legacy_cliente_id)) + ->orderByDesc('date_received') + ->orderByDesc('id') + ->limit(30) + ->get() + ->map(fn(AssistenzaTecnorepairScheda $scheda): array => [ + 'id' => (int) $scheda->id, + 'numero' => (string) ($scheda->legacy_numero_scheda ?: ('#' . $scheda->id)), + 'prodotto' => (string) ($scheda->product_model ?: '-'), + 'codice' => (string) ($scheda->product_code ?: ''), + 'seriali' => (string) $scheda->serial_label, + 'stato' => (string) ($scheda->status_label ?: '-'), + 'status_badge' => (string) $scheda->status_badge_classes, + 'ingresso' => optional($scheda->date_received)->format('d/m/Y') ?: '-', + ])->all(); + $items = TicketIntervento::query() ->with(['ticket.stabile', 'ticket.soggettoRichiedente', 'ticket.apertoDaUser']) ->where('fornitore_id', $this->fornitoreId) @@ -144,6 +295,19 @@ public function getProdottiUrl(): string return ProdottiCatalogo::getUrl(['fornitore' => (int) $this->fornitoreId], panel: 'admin-filament'); } + public function getImportCommandHintProperty(): string + { + $fornitore = (int) ($this->fornitoreId ?? 0) > 0 + ? Fornitore::query()->select(['id', 'amministratore_id'])->find((int) $this->fornitoreId) + : null; + + if (! $fornitore instanceof Fornitore) { + return 'php artisan tecnorepair:import-rubrica-clienti {amministratore} --fornitore-id={id}'; + } + + return 'php artisan tecnorepair:import-rubrica-clienti ' . (int) ($fornitore->amministratore_id ?? 0) . ' --fornitore-id=' . (int) $fornitore->id; + } + public function getGoogleConnectUrl(): string { return route('oauth.google.redirect', [ @@ -226,6 +390,59 @@ protected function buildRubricaUrlForCurrentUser(int $rubricaId): ?string return null; } + /** + * @return array> + */ + private function buildTicketFallbackRows(string $term = ''): array + { + $items = TicketIntervento::query() + ->with(['ticket.stabile', 'ticket.soggettoRichiedente', 'ticket.apertoDaUser']) + ->where('fornitore_id', $this->fornitoreId) + ->latest('updated_at') + ->limit(250) + ->get(); + + $grouped = []; + + foreach ($items as $intervento) { + $contact = $this->resolveCallerDataForRubrica($intervento); + $key = $contact['identity_key'] !== '' ? $contact['identity_key'] : ('ticket-' . (int) $intervento->ticket_id); + + if ($term !== '') { + $haystack = Str::lower(implode(' ', [$contact['contatto'], $contact['telefono'], $contact['email'], $contact['stabile'], $contact['origine']])); + if (! Str::contains($haystack, $term)) { + continue; + } + } + + if (! isset($grouped[$key])) { + $grouped[$key] = [ + 'id' => 0, + 'legacy_cliente_id' => 0, + 'display_name' => $contact['contatto'], + 'phone' => $contact['telefono'], + 'phone_alt' => '', + 'email' => $contact['email'], + 'indirizzo' => '', + 'citta' => '', + 'provincia' => '', + 'rubrica_id' => 0, + 'rubrica_url' => $contact['rubrica_url'], + 'rubrica_label' => $contact['rubrica_label'], + 'totale_schede' => 1, + 'ultima_data' => optional($intervento->updated_at)->format('d/m/Y') ?: '-', + 'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-', + 'source' => 'ticket_fallback', + ]; + continue; + } + + $grouped[$key]['totale_schede']++; + } + + return array_values($grouped); + } + /** * @return array|null */ diff --git a/app/Models/FornitoreCliente.php b/app/Models/FornitoreCliente.php new file mode 100644 index 0000000..95be34d --- /dev/null +++ b/app/Models/FornitoreCliente.php @@ -0,0 +1,58 @@ + 'datetime', + 'metadata' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public function amministratore(): BelongsTo + { + return $this->belongsTo(Amministratore::class, 'amministratore_id'); + } + + public function fornitore(): BelongsTo + { + return $this->belongsTo(Fornitore::class, 'fornitore_id'); + } + + public function rubrica(): BelongsTo + { + return $this->belongsTo(RubricaUniversale::class, 'rubrica_id'); + } +} \ No newline at end of file diff --git a/database/migrations/2026_04_14_190000_create_fornitore_clienti_table.php b/database/migrations/2026_04_14_190000_create_fornitore_clienti_table.php new file mode 100644 index 0000000..594f450 --- /dev/null +++ b/database/migrations/2026_04_14_190000_create_fornitore_clienti_table.php @@ -0,0 +1,48 @@ +id(); + $table->foreignId('amministratore_id')->nullable()->constrained('amministratori')->nullOnDelete(); + $table->foreignId('fornitore_id')->constrained('fornitori')->cascadeOnDelete(); + $table->foreignId('rubrica_id')->nullable()->constrained('rubrica_universale')->nullOnDelete(); + $table->unsignedBigInteger('legacy_cliente_id')->nullable(); + $table->string('display_name')->nullable(); + $table->string('phone')->nullable(); + $table->string('phone_alt')->nullable(); + $table->string('email')->nullable(); + $table->string('indirizzo')->nullable(); + $table->string('cap')->nullable(); + $table->string('citta')->nullable(); + $table->string('provincia')->nullable(); + $table->string('partita_iva')->nullable(); + $table->string('codice_fiscale')->nullable(); + $table->text('note')->nullable(); + $table->string('source')->nullable(); + $table->string('imported_from_path')->nullable(); + $table->timestamp('imported_at')->nullable(); + $table->json('metadata')->nullable(); + $table->timestamps(); + + $table->unique(['fornitore_id', 'legacy_cliente_id'], 'fornitore_clienti_fornitore_legacy_unique'); + $table->index(['fornitore_id', 'display_name'], 'fornitore_clienti_fornitore_name_idx'); + $table->index(['fornitore_id', 'rubrica_id'], 'fornitore_clienti_fornitore_rubrica_idx'); + }); + } + + public function down(): void + { + Schema::dropIfExists('fornitore_clienti'); + } +}; \ No newline at end of file diff --git a/resources/views/filament/pages/fornitore/rubrica-clienti.blade.php b/resources/views/filament/pages/fornitore/rubrica-clienti.blade.php index 4a75c32..52d5851 100644 --- a/resources/views/filament/pages/fornitore/rubrica-clienti.blade.php +++ b/resources/views/filament/pages/fornitore/rubrica-clienti.blade.php @@ -6,7 +6,7 @@
Rubrica clienti fornitore
@if($this->fornitoreLabel) - Contatti emersi dai ticket e dalle lavorazioni di {{ $this->fornitoreLabel }}, agganciati quando possibile all'anagrafica unica. + Elenco clienti del singolo fornitore, con sorgente TecnoRepair `TClienti` e collegamento alla scheda condivisa di anagrafica unica quando disponibile. @else Seleziona un fornitore per aprire la rubrica clienti. @endif @@ -20,7 +20,7 @@
- Questa vista resta stretta sui contatti del singolo fornitore, ma il punto di verità anagrafico rimane la scheda dell'anagrafica unica. Da qui puoi filtrare i contatti operativi e saltare direttamente sulla scheda condivisa quando il legame esiste gia. + Questa vista resta stretta sul fornitore, ma la scheda definitiva resta quella condivisa di anagrafica unica. Prima importa i clienti con {{ $this->importCommandHint }}, poi usa questa pagina per lavorare in modo compatto su elenco, recapiti e storico apparecchi.
@if($this->missingAdminContext) @@ -28,67 +28,193 @@ Questa vista richiede un fornitore selezionato. @else -
-
- +
+
+
+ + +
-
- @forelse($rows as $row) -
-
-
-
{{ $row['contatto'] }}
-
Origine: {{ $row['origine'] }} · Interventi: {{ $row['totale_interventi'] }}
- @if($row['rubrica_url']) -
Anagrafica unica collegata{{ $row['rubrica_label'] !== '' ? ' · ' . $row['rubrica_label'] : '' }}
- @endif -
Stabile: {{ $row['stabile'] }}
-
Ultimo aggiornamento: {{ $row['updated_at'] }}
-
-
- @if($row['rubrica_url']) - Scheda anagrafica - @endif - @if($row['telefono'] !== '') - Chiama - @endif - @if($row['email'] !== '') - Email - @endif - Ultima scheda -
-
-
- @empty -
Nessun contatto disponibile per il filtro corrente.
- @endforelse +
+
Totali {{ $counters['totali'] ?? 0 }}
+
Collegati {{ $counters['collegati'] ?? 0 }}
+
Con email {{ $counters['con_email'] ?? 0 }}
+
Con cellulare {{ $counters['con_cellulare'] ?? 0 }}
-
-
Google workspace
- @if($googleWorkspaceStatus) -
-
Stato: {{ $googleWorkspaceStatus['connected'] ? 'Collegato' : 'Non collegato' }}
-
Account: {{ $googleWorkspaceStatus['email'] !== '' ? $googleWorkspaceStatus['email'] : '-' }}
-
Nome: {{ $googleWorkspaceStatus['name'] !== '' ? $googleWorkspaceStatus['name'] : '-' }}
-
Ultimo collegamento: {{ $googleWorkspaceStatus['connected_at'] !== '' ? $googleWorkspaceStatus['connected_at'] : '-' }}
-
-
- Collega / aggiorna Google - @if($googleWorkspaceStatus['connected']) -
- @csrf - -
+
+
+ @if($activeTab === 'elenco') + + +
+ + + + + + + + + + + + + @forelse($rows as $row) + + + + + + + + + @empty + + + + @endforelse + +
ClienteRecapitiZonaSchedeRubricaAzioni
+
{{ $row['display_name'] }}
+
Fonte {{ $row['source'] === 'tecnorepair_tclienti' ? 'TClienti' : 'ticket' }}
+
+
{{ $row['phone'] !== '' ? $row['phone'] : '-' }}
+
{{ $row['phone_alt'] !== '' ? $row['phone_alt'] : ($row['email'] !== '' ? $row['email'] : 'nessun recapito') }}
+
+
{{ $row['citta'] !== '' ? $row['citta'] : '-' }}
+
{{ $row['indirizzo'] !== '' ? $row['indirizzo'] : '' }}
+
+
{{ $row['totale_schede'] ?? 0 }}
+
Ultima {{ $row['ultima_data'] ?? '-' }}
+
+ @if($row['rubrica_url']) + Scheda condivisa + @else + Non collegato + @endif + + @if((int) ($row['id'] ?? 0) > 0) + + @endif +
Nessun cliente disponibile. Importa prima `TClienti` nell'archivio legacy e poi lancia il comando rubrica clienti.
+
+ @else + @php + $selected = $this->selectedCliente; + @endphp + @if(! $selected) +
+ Seleziona un cliente dall'elenco compatto per aprire la scheda fornitore limitata. +
+ @else +
+
+
+
Scheda cliente
+
{{ $selected->display_name ?: 'Cliente TecnoRepair' }}
+
Legacy cliente ID {{ $selected->legacy_cliente_id ?: '-' }} · aggiornato {{ optional($selected->updated_at)->format('d/m/Y H:i') ?: '-' }}
+
+
+
Cellulare
+
{{ $selected->phone ?: '-' }}
+
+
+
Fisso / email
+
{{ $selected->phone_alt ?: ($selected->email ?: '-') }}
+
+
+
Indirizzo
+
{{ trim(implode(', ', array_filter([$selected->indirizzo, $selected->cap, $selected->citta, $selected->provincia]))) !== '' ? trim(implode(', ', array_filter([$selected->indirizzo, $selected->cap, $selected->citta, $selected->provincia]))) : '-' }}
+
+
+
Codice fiscale
+
{{ $selected->codice_fiscale ?: '-' }}
+
+
+
P. IVA
+
{{ $selected->partita_iva ?: '-' }}
+
+
+ @if($selected->rubrica_id) + + @endif +
+ +
+
Storico apparecchi / seriali
+
+ + + + + + + + + + + @forelse($this->selectedClienteSchede as $scheda) + + + + + + + @empty + + + + @endforelse + +
SchedaProdottoSerialiStato
+
{{ $scheda['numero'] }}
+
{{ $scheda['ingresso'] }}
+
+
{{ $scheda['prodotto'] }}
+
{{ $scheda['codice'] !== '' ? $scheda['codice'] : 'senza codice' }}
+
{{ $scheda['seriali'] }}{{ $scheda['stato'] }}
Nessuna scheda legacy collegata a questo cliente.
+
+
+
+ +
+
+
Google workspace
+ @if($googleWorkspaceStatus) +
+
Stato: {{ $googleWorkspaceStatus['connected'] ? 'Collegato' : 'Non collegato' }}
+
Account: {{ $googleWorkspaceStatus['email'] !== '' ? $googleWorkspaceStatus['email'] : '-' }}
+
Nome: {{ $googleWorkspaceStatus['name'] !== '' ? $googleWorkspaceStatus['name'] : '-' }}
+
Ultimo collegamento: {{ $googleWorkspaceStatus['connected_at'] !== '' ? $googleWorkspaceStatus['connected_at'] : '-' }}
+
+
+ Collega / aggiorna Google + @if($googleWorkspaceStatus['connected']) +
+ @csrf + +
+ @endif +
+ @else +
Fornitore senza amministratore collegato: stato Google non disponibile.
+ @endif +
+ +
+ Il modello dati prodotti e seriali è già separato: un prodotto unico può avere più seriali collegati. Nella fase successiva si può rifinire la UI della scheda intervento in stile TecnoRepair e riusare la pipeline FE lato fornitore senza auto-import. +
+
+
@endif
-
Questo blocco prepara l'uso fornitore di rubrica, calendario e Drive sullo stesso backend Google gia presente nel progetto.
- @else -
Fornitore senza amministratore collegato: stato Google non disponibile.
@endif