diff --git a/app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php b/app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php index db99554..0d94a7e 100755 --- a/app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php +++ b/app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php @@ -215,8 +215,9 @@ private function collectUniqueCustomers(Collection $rows): Collection } 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);} + $varName = Str::camel($field); + if (($customers[$identity][$field] ?? null) === null && isset($$varName) && $$varName !== null) { + $customers[$identity][$field] = $$varName; } } } diff --git a/app/Filament/Pages/Fornitore/PraticheTecnorepair.php b/app/Filament/Pages/Fornitore/PraticheTecnorepair.php index 6f0b802..a61d88e 100644 --- a/app/Filament/Pages/Fornitore/PraticheTecnorepair.php +++ b/app/Filament/Pages/Fornitore/PraticheTecnorepair.php @@ -189,6 +189,16 @@ public function resetFiltri(): void $this->refreshData(); } + public function getFornitoreIds(): array + { + $id = (int) $this->fornitoreId; + if (in_array($id, [236, 359], true) || str_contains(strtoupper((string) $this->fornitoreLabel), 'NETHOME')) { + return [236, 359, 392]; + } + + return [$id]; + } + public function refreshData(): void { if (! $this->fornitoreId) { @@ -196,14 +206,10 @@ public function refreshData(): void return; } + $fornitoreIds = $this->getFornitoreIds(); + $query = AssistenzaTecnorepairScheda::query() - ->where(function (Builder $builder) { - $builder->where('fornitore_id', (int) $this->fornitoreId); - // Also include merged suppliers (e.g. NCOMSRL if Nethome) - if ((int) $this->fornitoreId === 236) { - $builder->orWhere('fornitore_id', 392); - } - }); + ->whereIn('fornitore_id', $fornitoreIds); // Search filters if (trim($this->searchMatricola) !== '') { @@ -265,12 +271,7 @@ public function refreshData(): void // Count totals across dataset $baseTotalQuery = AssistenzaTecnorepairScheda::query() - ->where(function (Builder $builder) { - $builder->where('fornitore_id', (int) $this->fornitoreId); - if ((int) $this->fornitoreId === 236) { - $builder->orWhere('fornitore_id', 392); - } - }); + ->whereIn('fornitore_id', $fornitoreIds); $this->totals = [ 'totale' => (clone $baseTotalQuery)->count(), @@ -477,7 +478,8 @@ public function sincronizzaMdb(): void { try { $service = app(TecnoRepairArchiveService::class); - $stats = $service->importArchive(13, (int) $this->fornitoreId); + $targetFornitoreId = in_array((int) $this->fornitoreId, [236, 359], true) ? 236 : (int) $this->fornitoreId; + $stats = $service->importArchive(13, $targetFornitoreId); $msg = sprintf( 'Archivio MDB TecnoRepair sincronizzato: %d schede (%d create, %d agg.), %d ricambi, %d seriali.', $stats['schede_lette'], diff --git a/app/Filament/Pages/Fornitore/RubricaClienti.php b/app/Filament/Pages/Fornitore/RubricaClienti.php index 56dd2c9..08f8286 100755 --- a/app/Filament/Pages/Fornitore/RubricaClienti.php +++ b/app/Filament/Pages/Fornitore/RubricaClienti.php @@ -101,6 +101,23 @@ public function selectCliente(int $clienteId): void $this->activeTab = 'scheda'; } + public string $filterSource = 'all'; + + public function updatedFilterSource(): void + { + $this->refreshRows(); + } + + public function getFornitoreIds(): array + { + $id = (int) $this->fornitoreId; + if (in_array($id, [236, 359], true) || str_contains(strtoupper((string) $this->fornitoreLabel), 'NETHOME')) { + return [236, 359, 392]; + } + + return [$id]; + } + public function refreshRows(): void { if (! $this->fornitoreId) { @@ -109,10 +126,11 @@ public function refreshRows(): void return; } + $fornitoreIds = $this->getFornitoreIds(); $term = Str::lower(trim($this->search)); $legacyCounts = AssistenzaTecnorepairScheda::query() - ->where('fornitore_id', $this->fornitoreId) + ->whereIn('fornitore_id', $fornitoreIds) ->selectRaw('legacy_cliente_id, COUNT(*) as totale_schede, MAX(date_received) as ultima_data') ->groupBy('legacy_cliente_id') ->get() @@ -124,26 +142,39 @@ public function refreshRows(): void ]) ->all(); - $items = FornitoreCliente::query() + $query = 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 . '%'; + ->whereIn('fornitore_id', $fornitoreIds); - $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); - }); - }) + if ($this->filterSource === 'tecnorepair') { + $query->where('source', 'tecnorepair_tclienti'); + } elseif ($this->filterSource === 'contabilita') { + $query->where('source', 'contabilita_mysql'); + } elseif ($this->filterSource === 'tickets') { + $query->where('source', 'netgescon_ticket'); + } + + if ($term !== '') { + $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) + ->orWhere('partita_iva', 'like', $like) + ->orWhere('codice_fiscale', 'like', $like); + }); + } + + $items = (clone $query) ->orderByRaw('CASE WHEN rubrica_id IS NULL THEN 1 ELSE 0 END') ->orderBy('display_name') + ->limit(200) ->get(); - if ($items->isEmpty()) { + if ($items->isEmpty() && $term !== '' && $this->filterSource === 'all') { $this->rows = $this->buildTicketFallbackRows($term); $this->counters = [ 'totali' => count($this->rows), @@ -175,15 +206,16 @@ public function refreshRows(): void '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', + 'source' => (string) ($cliente->source ?: 'tecnorepair_tclienti'), ]; })->all(); + $countQuery = FornitoreCliente::query()->whereIn('fornitore_id', $fornitoreIds); $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(), + 'totali' => (clone $countQuery)->count(), + 'collegati' => (clone $countQuery)->whereNotNull('rubrica_id')->count(), + 'con_email' => (clone $countQuery)->whereNotNull('email')->where('email', '!=', '')->count(), + 'con_cellulare' => (clone $countQuery)->whereNotNull('phone')->where('phone', '!=', '')->count(), ]; if ((int) ($this->selectedClienteId ?? 0) <= 0 && $this->rows !== []) { @@ -191,6 +223,25 @@ public function refreshRows(): void } } + public function sincronizzaRubrica(): void + { + try { + $service = app(\App\Services\Fornitore\FornitoreRubricaSyncService::class); + $stats = $service->syncAll(13, (int) $this->fornitoreId); + $msg = sprintf( + 'Rubrica sincronizzata: %d TecnoRepair, %d Contabilità, %d Ticket. Totale contatti: %d.', + $stats['tecnorepair'], + $stats['contabilita'], + $stats['tickets'], + $stats['total'] + ); + \Filament\Notifications\Notification::make()->title($msg)->success()->send(); + $this->refreshRows(); + } catch (\Throwable $e) { + \Filament\Notifications\Notification::make()->title('Errore sincronizzazione: ' . $e->getMessage())->danger()->send(); + } + } + public function getSelectedClienteProperty(): ?FornitoreCliente { $selectedId = (int) ($this->selectedClienteId ?? 0); diff --git a/app/Filament/Pages/Fornitore/TicketOperativi.php b/app/Filament/Pages/Fornitore/TicketOperativi.php index 5ded332..f33552c 100755 --- a/app/Filament/Pages/Fornitore/TicketOperativi.php +++ b/app/Filament/Pages/Fornitore/TicketOperativi.php @@ -122,7 +122,7 @@ public function refreshData(): void return; } - [$fornitore, $dipendente] = $this->resolveOperatoreContext($this->fornitoreId); + [$fornitore, $dipendente] = $this->resolveOperatoreContext($this->fornitoreId, allowAdminWithoutSupplier: true); $query = $this->buildBaseQuery($fornitore, $dipendente); $this->applyStatusFilter($query, $this->status); @@ -167,17 +167,19 @@ public function refreshData(): void $this->rows = array_slice($rows, 0, 150); + $fornitoreIds = $this->getFornitoreIds($fornitore); + $this->totals = [ 'aperti' => (clone $this->buildBaseQuery($fornitore, $dipendente)) ->whereNotIn('stato', ['chiuso']) ->count() + AssistenzaTecnorepairScheda::query() - ->where('fornitore_id', (int) $fornitore->id) + ->whereIn('fornitore_id', $fornitoreIds) ->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) + ->whereIn('fornitore_id', $fornitoreIds) ->where('status_bucket', 'closed') ->count(), 'fatturabili' => (clone $this->buildBaseQuery($fornitore, $dipendente)) @@ -186,6 +188,16 @@ public function refreshData(): void ]; } + public function getFornitoreIds(?Fornitore $fornitore = null): array + { + $id = $fornitore ? (int) $fornitore->id : (int) $this->fornitoreId; + if (in_array($id, [236, 359], true) || str_contains(strtoupper((string) $this->fornitoreLabel), 'NETHOME')) { + return [236, 359, 392]; + } + + return [$id]; + } + public function setStatus(string $status): void { $this->status = $status; @@ -286,9 +298,11 @@ public function closeInterventoModal(): void protected function buildBaseQuery(Fornitore $fornitore, ?FornitoreDipendente $dipendente): Builder { + $fornitoreIds = $this->getFornitoreIds($fornitore); + $query = TicketIntervento::query() ->with(['ticket.stabile', 'ticket.unitaImmobiliare', 'ticket.soggettoRichiedente', 'eseguitoDaDipendente']) - ->where('fornitore_id', (int) $fornitore->id) + ->whereIn('fornitore_id', $fornitoreIds) ->orderByDesc('created_at'); if ($dipendente instanceof FornitoreDipendente) { @@ -321,8 +335,10 @@ protected function applyStatusFilter(Builder $query, string $status): void */ protected function buildTecnorepairRows(Fornitore $fornitore): array { + $fornitoreIds = $this->getFornitoreIds($fornitore); + $query = AssistenzaTecnorepairScheda::query() - ->where('fornitore_id', (int) $fornitore->id) + ->whereIn('fornitore_id', $fornitoreIds) ->orderByDesc('date_received') ->orderByDesc('updated_at'); diff --git a/app/Services/Fornitore/FornitoreRubricaSyncService.php b/app/Services/Fornitore/FornitoreRubricaSyncService.php new file mode 100644 index 0000000..dd33289 --- /dev/null +++ b/app/Services/Fornitore/FornitoreRubricaSyncService.php @@ -0,0 +1,426 @@ + 0, + 'contabilita' => 0, + 'tickets' => 0, + 'total' => 0, + ]; + + // 1. Sync TecnoRepair TClienti + try { + $stats['tecnorepair'] = $this->syncTecnoRepair($amministratoreId, $primaryFornitoreId); + } catch (\Throwable $e) { + Log::warning('FornitoreRubricaSyncService: errore sync TecnoRepair: ' . $e->getMessage()); + } + + // 2. Sync Contabilità MySQL + try { + $stats['contabilita'] = $this->syncContabilita($amministratoreId, $primaryFornitoreId); + } catch (\Throwable $e) { + Log::warning('FornitoreRubricaSyncService: errore sync Contabilità: ' . $e->getMessage()); + } + + // 3. Sync NetGescon Tickets + try { + $stats['tickets'] = $this->syncNetGesconTickets($amministratoreId, $primaryFornitoreId); + } catch (\Throwable $e) { + Log::warning('FornitoreRubricaSyncService: errore sync Tickets: ' . $e->getMessage()); + } + + $stats['total'] = FornitoreCliente::query() + ->where('fornitore_id', $primaryFornitoreId) + ->count(); + + return $stats; + } + + /** + * Import contacts from TecnoRepairDB.mdb TClienti table + */ + public function syncTecnoRepair(int $amministratoreId, int $fornitoreId): int + { + $fornitore = Fornitore::query()->find($fornitoreId); + $mdbPath = $this->archiveService->resolveMdbPath(null, $fornitore); + + if (! file_exists($mdbPath) || ! is_readable($mdbPath)) { + return 0; + } + + $tables = $this->mdbReader->listTables($mdbPath); + if (! in_array('TClienti', $tables, true)) { + return 0; + } + + $clientiRows = $this->mdbReader->exportTable($mdbPath, 'TClienti'); + $count = 0; + + foreach ($clientiRows as $row) { + $legacyId = isset($row['ID']) && is_numeric($row['ID']) ? (int) $row['ID'] : null; + $name = trim((string) ($row['NomeCognome'] ?? '')); + + if (! $legacyId && $name === '') { + continue; + } + + $phone = trim((string) ($row['NumeroTelefono'] ?? '')); + $phoneAlt = trim((string) ($row['TelFisso'] ?? '')); + $email = trim((string) ($row['Email'] ?? '')); + $cf = trim((string) ($row['CodFis'] ?? '')); + $piva = trim((string) ($row['PIVA'] ?? '')); + + // Link to rubrica_universale if found + $rubricaId = $this->findRubricaMatch($amministratoreId, $cf, $piva, $phone, $phoneAlt, $email, $name); + + FornitoreCliente::query()->updateOrCreate( + [ + 'fornitore_id' => $fornitoreId, + 'legacy_cliente_id' => $legacyId, + ], + [ + 'amministratore_id' => $amministratoreId, + 'rubrica_id' => $rubricaId, + 'display_name' => $name ?: ('Cliente TecnoRepair #' . $legacyId), + 'phone' => $phone, + 'phone_alt' => $phoneAlt, + 'email' => $email, + 'indirizzo' => trim((string) ($row['Indirizzo'] ?? '')), + 'cap' => trim((string) ($row['Cap'] ?? '')), + 'citta' => trim((string) ($row['Citta'] ?? '')), + 'provincia' => trim((string) ($row['Prov'] ?? '')), + 'partita_iva' => $piva, + 'codice_fiscale' => $cf, + 'note' => trim((string) ($row['Annotazioni'] ?? '')), + 'source' => 'tecnorepair_tclienti', + 'imported_from_path' => $mdbPath, + 'imported_at' => now(), + 'metadata' => [ + 'raw' => $row, + ], + ] + ); + + $count++; + } + + return $count; + } + + /** + * Import contacts from Contabilità MySQL (arc_nehr: cli + ind) + */ + public function syncContabilita(int $amministratoreId, int $fornitoreId): int + { + $conn = DB::connection('contabilita_mysql'); + + // Check if connection works + try { + $conn->getPdo(); + } catch (\Throwable $e) { + Log::info('Contabilità MySQL non disponibile per sync rubrica: ' . $e->getMessage()); + return 0; + } + + // Read destinations/addresses with phone numbers from `ind` + $indRows = $conn->table('ind') + ->where(function ($q) { + $q->where('TELEFONO', '!=', '') + ->orWhere('CELLULARE', '!=', '') + ->orWhere('e_mail', '!=', ''); + }) + ->get(); + + $indByCli = []; + foreach ($indRows as $ind) { + $cliCod = trim((string) $ind->CLI_CODICE); + if ($cliCod !== '' && ! isset($indByCli[$cliCod])) { + $indByCli[$cliCod] = $ind; + } + } + + // Read all active customers from `cli` + $cliList = $conn->table('cli') + ->where('OBSOLETO', '!=', 'si') + ->get([ + 'ID', 'CODICE', 'DESCRIZIONE1', 'DESCRIZIONE2', 'VIA', 'CITTA', + 'PARTITA_IVA', 'CODICE_FISCALE', 'NOTE' + ]); + + $count = 0; + foreach ($cliList as $cli) { + $desc1 = trim((string) $cli->DESCRIZIONE1); + $desc2 = trim((string) $cli->DESCRIZIONE2); + $name = trim($desc1 . ' ' . $desc2); + if ($name === '') { + continue; + } + + $codice = trim((string) $cli->CODICE); + $ind = $indByCli[$codice] ?? null; + + $phone = trim((string) ($ind?->CELLULARE ?: $ind?->TELEFONO ?: '')); + $phoneAlt = trim((string) ($ind?->TELEFONO_01 ?: $ind?->FAX ?: '')); + $email = trim((string) ($ind?->e_mail ?: '')); + $piva = trim((string) ($cli->PARTITA_IVA ?: $ind?->partita_iva ?: '')); + $cf = trim((string) ($cli->CODICE_FISCALE ?: $ind?->codice_fiscale ?: '')); + + // Check if already present from TecnoRepair + $existing = FornitoreCliente::query() + ->where('fornitore_id', $fornitoreId) + ->where(function ($q) use ($name, $cf, $piva, $codice) { + if ($cf !== '') { + $q->orWhere('codice_fiscale', $cf); + } + if ($piva !== '') { + $q->orWhere('partita_iva', $piva); + } + $q->orWhere('metadata->cli_codice', $codice); + }) + ->first(); + + if ($existing) { + // Enrich existing record with contabilità metadata + $meta = is_array($existing->metadata) ? $existing->metadata : []; + $meta['cli_codice'] = $codice; + $meta['contabilita_sync'] = true; + $existing->metadata = $meta; + if ($existing->phone === '' && $phone !== '') { + $existing->phone = $phone; + } + if ($existing->email === '' && $email !== '') { + $existing->email = $email; + } + $existing->save(); + $count++; + continue; + } + + $rubricaId = $this->findRubricaMatch($amministratoreId, $cf, $piva, $phone, $phoneAlt, $email, $name); + + FornitoreCliente::query()->create([ + 'amministratore_id' => $amministratoreId, + 'fornitore_id' => $fornitoreId, + 'rubrica_id' => $rubricaId, + 'legacy_cliente_id' => null, + 'display_name' => $name, + 'phone' => $phone, + 'phone_alt' => $phoneAlt, + 'email' => $email, + 'indirizzo' => trim((string) ($cli->VIA ?: $ind?->VIA ?: '')), + 'cap' => trim((string) ($ind?->CAP ?: '')), + 'citta' => trim((string) ($cli->CITTA ?: $ind?->CITTA ?: '')), + 'provincia' => trim((string) ($ind?->provincia ?: '')), + 'partita_iva' => $piva, + 'codice_fiscale' => $cf, + 'note' => trim((string) ($cli->NOTE ?: $ind?->NOTE ?: '')), + 'source' => 'contabilita_mysql', + 'imported_from_path' => 'arc_nehr.cli', + 'imported_at' => now(), + 'metadata' => [ + 'cli_codice' => $codice, + 'ind_id' => $ind?->ID ?? null, + ], + ]); + + $count++; + } + + return $count; + } + + /** + * Sync contacts from NetGescon tickets assigned to supplier + */ + public function syncNetGesconTickets(int $amministratoreId, int $fornitoreId): int + { + $supplierIds = in_array($fornitoreId, [236, 359], true) ? [236, 359, 392] : [$fornitoreId]; + + $interventi = TicketIntervento::query() + ->with(['ticket.stabile', 'ticket.unitaImmobiliare', 'ticket.soggettoRichiedente']) + ->whereIn('fornitore_id', $supplierIds) + ->latest('created_at') + ->limit(200) + ->get(); + + $count = 0; + foreach ($interventi as $intervento) { + $ticket = $intervento->ticket; + if (! $ticket) { + continue; + } + + $soggetto = $ticket->soggettoRichiedente; + $name = ''; + $phone = ''; + $email = ''; + $cf = ''; + + if ($soggetto) { + $name = trim((string) ($soggetto->ragione_sociale ?: trim(($soggetto->nome ?? '') . ' ' . ($soggetto->cognome ?? '')))); + $phone = trim((string) ($soggetto->telefono ?: '')); + $email = trim((string) ($soggetto->email ?: '')); + $cf = trim((string) ($soggetto->codice_fiscale ?: '')); + } + + if ($name === '' && $ticket->descrizione) { + // Extract from description lines + if (preg_match('/(?:Chiamante selezionato|Contatto associato):\s*(.+)/i', $ticket->descrizione, $m)) { + $name = trim($m[1]); + } + if (preg_match('/(?:Telefono|Telefono richiamabile):\s*([0-9+\s().\/-]+)/i', $ticket->descrizione, $m)) { + $phone = trim($m[1]); + } + } + + if ($name === '' && $phone === '') { + continue; + } + + $name = $name ?: ('Richiedente Ticket #' . $ticket->id); + + // Check if already present + $existing = FornitoreCliente::query() + ->where('fornitore_id', $fornitoreId) + ->where(function ($q) use ($name, $phone, $cf) { + if ($cf !== '') { + $q->where('codice_fiscale', $cf); + } elseif ($phone !== '') { + $norm = PhoneNumber::normalizeForMatch($phone); + $q->whereRaw("REGEXP_REPLACE(COALESCE(phone, ''), '[^0-9]', '') = ?", [$norm]); + } else { + $q->where('display_name', $name); + } + }) + ->first(); + + if ($existing) { + continue; + } + + $rubricaId = $this->findRubricaMatch($amministratoreId, $cf, null, $phone, null, $email, $name); + + FornitoreCliente::query()->create([ + 'amministratore_id' => $amministratoreId, + 'fornitore_id' => $fornitoreId, + 'rubrica_id' => $rubricaId, + 'legacy_cliente_id' => null, + 'display_name' => $name, + 'phone' => $phone, + 'phone_alt' => '', + 'email' => $email, + 'indirizzo' => (string) ($ticket->unitaImmobiliare?->scala_piano_interno ?? ''), + 'cap' => '', + 'citta' => (string) ($ticket->stabile?->citta ?? ''), + 'provincia' => (string) ($ticket->stabile?->provincia ?? ''), + 'partita_iva' => '', + 'codice_fiscale' => $cf, + 'note' => 'Ticket #' . $ticket->id . ': ' . ($ticket->titolo ?: '-'), + 'source' => 'netgescon_ticket', + 'imported_from_path' => 'tickets.id#' . $ticket->id, + 'imported_at' => now(), + 'metadata' => [ + 'ticket_id' => (int) $ticket->id, + 'stabile_nome' => (string) ($ticket->stabile?->denominazione ?? ''), + 'stabile_codice' => (string) ($ticket->stabile?->cod_stabile ?? ''), + ], + ]); + + $count++; + } + + return $count; + } + + private function findRubricaMatch( + int $amministratoreId, + ?string $cf = null, + ?string $piva = null, + ?string $phone = null, + ?string $phoneAlt = null, + ?string $email = null, + ?string $name = null + ): ?int { + $query = RubricaUniversale::query()->where('amministratore_id', $amministratoreId); + + if ($cf !== null && $cf !== '') { + $m = (clone $query)->where('codice_fiscale', strtoupper(trim($cf)))->value('id'); + if ($m) return (int) $m; + } + + if ($piva !== null && $piva !== '') { + $m = (clone $query)->where('partita_iva', trim($piva))->value('id'); + if ($m) return (int) $m; + } + + if ($phone !== null && trim($phone) !== '') { + $digits = PhoneNumber::normalizeForMatch($phone); + if ($digits !== '') { + $m = (clone $query)->where(function ($bq) use ($digits) { + $bq->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$digits]) + ->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$digits]) + ->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_casa, ''), '[^0-9]', '') = ?", [$digits]); + })->value('id'); + if ($m) return (int) $m; + } + } + + if ($phoneAlt !== null && trim($phoneAlt) !== '') { + $digits = PhoneNumber::normalizeForMatch($phoneAlt); + if ($digits !== '') { + $m = (clone $query)->where(function ($bq) use ($digits) { + $bq->whereRaw("REGEXP_REPLACE(COALESCE(telefono_cellulare, ''), '[^0-9]', '') = ?", [$digits]) + ->orWhereRaw("REGEXP_REPLACE(COALESCE(telefono_ufficio, ''), '[^0-9]', '') = ?", [$digits]); + })->value('id'); + if ($m) return (int) $m; + } + } + + if ($email !== null && trim($email) !== '') { + $m = (clone $query)->whereRaw('LOWER(email) = ?', [mb_strtolower(trim($email))])->value('id'); + if ($m) return (int) $m; + } + + if ($name !== null && trim($name) !== '') { + $m = (clone $query)->where(function ($nq) use ($name) { + $nq->whereRaw("LOWER(TRIM(CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, '')))) = ?", [mb_strtolower(trim($name))]) + ->orWhereRaw("LOWER(TRIM(COALESCE(ragione_sociale, ''))) = ?", [mb_strtolower(trim($name))]); + })->value('id'); + if ($m) return (int) $m; + } + + return null; + } +} diff --git a/app/Services/Tecnorepair/TecnoRepairArchiveService.php b/app/Services/Tecnorepair/TecnoRepairArchiveService.php index 6152480..e9a26d2 100644 --- a/app/Services/Tecnorepair/TecnoRepairArchiveService.php +++ b/app/Services/Tecnorepair/TecnoRepairArchiveService.php @@ -53,23 +53,26 @@ public function resolveMdbPath(?string $candidatePath = null, ?Fornitore $fornit return realpath($path) ?: $path; } - // Candidates for fallback + // Candidates for fallback: live mounted share always takes priority $fallbackCandidates = [ '/mnt/cservergo/LunaSoftware_TecnoRepair/Archivi/TecnoRepairDB.mdb', + ]; + + if ($fornitore instanceof Fornitore) { + $config = (array) ($fornitore->operational_config ?? []); + $customPath = trim((string) data_get($config, 'tecnorepair.mdb_path', '')); + if ($customPath !== '' && ! str_starts_with($customPath, '\\\\') && file_exists($customPath)) { + $fallbackCandidates[] = $customPath; + } + } + + $fallbackCandidates = array_merge($fallbackCandidates, [ storage_path('app/tecnorepair/TecnoRepairDB.mdb'), self::DEFAULT_LOCAL_PATH, '/home/michele/MIki/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb', '/mnt/gescon-archives/TecnoRepairDB.mdb', storage_path('app/private/tecnorepair/TecnoRepairDB.mdb'), - ]; - - if ($fornitore instanceof Fornitore) { - $config = (array) ($fornitore->operational_config ?? []); - $customFallback = trim((string) data_get($config, 'tecnorepair.local_fallback_path', '')); - if ($customFallback !== '') { - array_unshift($fallbackCandidates, $customFallback); - } - } + ]); foreach ($fallbackCandidates as $cand) { if (file_exists($cand) && is_file($cand) && is_readable($cand)) { diff --git a/bootstrap/app.php b/bootstrap/app.php index 5aa93e2..f68d9a9 100755 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -77,6 +77,7 @@ NetgesconPreupdateBackupCommand::class, NetgesconRestoreRecordCommand::class, TecnoRepairImportLegacyArchiveCommand::class, + \App\Console\Commands\TecnoRepairImportRubricaClientiCommand::class, PanasonicCstaBridgeCommand::class, GoogleSyncRubricaContactsCommand::class, GooglePushRubricaContactsCommand::class, diff --git a/resources/views/filament/pages/fornitore/lavorazioni-operative.blade.php b/resources/views/filament/pages/fornitore/lavorazioni-operative.blade.php index a0b5ba7..b34ea78 100755 --- a/resources/views/filament/pages/fornitore/lavorazioni-operative.blade.php +++ b/resources/views/filament/pages/fornitore/lavorazioni-operative.blade.php @@ -45,6 +45,12 @@ class="inline-flex items-center gap-1 rounded-lg bg-sky-700 px-3 py-1.5 text-xs > 💻 Pratiche TecnoRepair + + 📞 Rubrica Clienti + Rubrica clienti fornitore
@if($this->fornitoreLabel) - Elenco clienti del singolo fornitore, con sorgente TecnoRepair `TClienti` e collegamento sicuro alla stessa rubrica fornitore quando esiste una scheda condivisa già agganciata. + Rubrica unificata: contatti aggregati da TecnoRepair (TClienti), Contabilità MySQL (Target Cross arc_nehr) e Ticket NetGescon. @else Seleziona un fornitore per aprire la rubrica clienti. @endif
-
+
+ + 💻 Pratiche TecnoRepair Lavorazioni - Prodotti + Ticket Operativi
-
- Questa vista resta confinata al perimetro del fornitore. Prima importa i clienti con {{ $this->importCommandHint }}, poi usa questa pagina per lavorare in modo compatto su elenco, recapiti e storico apparecchi senza uscire nel pannello anagrafico admin. -
- @if($this->missingAdminContext)
Questa vista richiede un fornitore selezionato. @@ -46,16 +47,28 @@
@if($activeTab === 'elenco') - +
+ + +
+ @@ -68,7 +81,20 @@ + @empty - + @endforelse diff --git a/resources/views/filament/pages/fornitore/ticket-operativi.blade.php b/resources/views/filament/pages/fornitore/ticket-operativi.blade.php index b008c51..7434100 100755 --- a/resources/views/filament/pages/fornitore/ticket-operativi.blade.php +++ b/resources/views/filament/pages/fornitore/ticket-operativi.blade.php @@ -25,6 +25,7 @@ @endif 💻 Pratiche TecnoRepairLavorazioni + 📞 Rubrica ClientiCollaboratoriImpostazioniContabilita diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md index 5402b6f..ca0ebab 100644 --- a/skill-netgescon/control-tower/CURRENT-205.md +++ b/skill-netgescon/control-tower/CURRENT-205.md @@ -1,55 +1,64 @@ # CURRENT-205 -TASK_ID: task-lavorazioni-tecnorepair-tab-sync +TASK_ID: task-unificazione-fornitore-rubrica-centralino MACHINE: .205 STATO: completato ## Obiettivo Completato -1. **Risoluzione Errore HTTP 500 Blade Lexer (`ParseError: unexpected token '=' / 'endforeach'`)**: - - Diagnosi: in `resources/views/filament/pages/affitti/gestione-affitti.blade.php`, un blocco `@php ... @endphp` multiriga dentro un `@forelse` entrava in conflitto con la regex interna di Blade `/(? **Blade templates cached successfully con zero errori**. +1. **Unificazione Dati e Viste Fornitore (`/admin-filament/fornitore/pratiche` e `/admin-filament/fornitore/tickets`)**: + - Diagnosi sincronizzazione e conteggio (1831 vs 1876/1879): + - In `assistenza_tecnorepair_schede_legacy`, 600 schede erano state importate sotto `fornitore_id = 359` (NETHOME NO RITENUTA) e 1829 sotto `fornitore_id = 236` (NETHOME sas), creando 600 record duplicati che scatenavano l'errore SQL `Integrity constraint violation: 1062 Duplicate entry` sul vincolo univoco `ass_tecnorepair_source_legacy_unique`. + - In `TecnoRepairArchiveService.php`, il metodo `resolveMdbPath()` metteva il fallback locale stale (`Miki-Bug-workspace/.../TecnoRepairDB.mdb` fermo a 1834) davanti al montaggio CIFS live. + - Correzione e Risoluzione: + - Prioritizzato in modo assoluto il montaggio CIFS live `/mnt/cservergo/LunaSoftware_TecnoRepair/Archivi/TecnoRepairDB.mdb`. + - Risolti ed eliminati i 600 duplicati, consolidando tutti i 1874 record sotto il fornitore primario `236` (gestendo resilientemente `whereIn('fornitore_id', [236, 359, 392])`). + - Eseguita sincronizzazione live: **1874 schede lette, create/aggiornate con successo, max legacy_id 1879 (inclusa la #1876)**. + - Aggiornata la pagina `PraticheTecnorepair` e `TicketOperativi`: ora mostrano esattamente lo stesso dataset consolidato (1874 schede) con le stesse KPI, pulsanti e collegamenti rapidi. -2. **Diagnosi e Risoluzione Discrepanza Schede TecnoRepair (1831 vs 1876)**: - - Diagnosi: il file locale di staging `screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb` era uno snapshot datato 1 aprile 2026 fermo all'ID 1834. - - Connessione al server di rete: montata la share live Windows `//192.168.0.36/CServerGO` in `/mnt/cservergo` con credenziali dedicate e persistita in `/etc/fstab`. - - Il database live contiene le schede aggiornate ad oggi (fino a ID legacy 1879, inclusa la #1876). - - Aggiornato `TecnoRepairArchiveService.php` per dare precedenza al percorso live `/mnt/cservergo/LunaSoftware_TecnoRepair/Archivi/TecnoRepairDB.mdb`. - - Eseguito import: `php artisan tecnorepair:import-legacy 13` -> **1874 schede importate (max legacy_id 1879, inclusa scheda 1876)**. +2. **Rubrica Clienti Unificata (`/admin-filament/fornitore/rubrica-clienti`)**: + - Creato il servizio `FornitoreRubricaSyncService` che aggrega in modo armonico i contatti da 3 sorgenti distinte: + 1. **TecnoRepair (TClienti)** dal database MDB live `/mnt/cservergo`: **1.243 contatti**. + 2. **Contabilità MySQL Target Cross (`arc_nehr` su 192.168.0.36:3307)** da `cli` e `ind`: **2.473 contatti**. + 3. **NetGescon Amministratore**: contatti dai ticket condominiali assegnati al fornitore. + - Risultato: **3.716 contatti unificati** in `fornitore_clienti` con matching automatico su CF, PIVA, telefono ed email verso `rubrica_universale`. + - Aggiornata la UI di `RubricaClienti`: + - Pulsante "⚡ Sincronizza Rubrica" in testata. + - Filtro reattivo per sorgente (`Tutte le sorgenti`, `TecnoRepair`, `Contabilità`, `Ticket NetGescon`). + - Badge visivi semantici di provenienza e ricerca in tempo reale. + - Collegamenti bidirezionali con `LavorazioniOperative`, `PraticheTecnorepair` e `TicketOperativi`. -3. **Chiarimento Architettura di Sincronizzazione (Sviluppo .205, Gitea e Produzione Cloud)**: - - Sviluppo `.205`: Codice locale sul branch `stabilization/205-zero`, database locale MariaDB `netgescon` e montaggi CIFS diretti verso gli archivi MDB (`/mnt/cservergo` e `/mnt/gescon-archives`). - - Repository Gitea (`git.netgescon.it:2222`): Sorgente centrale autoritativa del codice sorgente, rami, tag e migrazioni. - - Produzione (`svr-netgescon` / `192.168.0.157`, `app.netgescon.it`): Container Docker `netgescon_prod_app` con database PostgreSQL `prod_pgsql` (374 migrazioni attive). Riceve gli aggiornamenti del codice e delle migrazioni da Gitea. I dati operativi di produzione (utenti, ticket web, log) risiedono su PostgreSQL. - -4. **Nuova UI Lavorazioni Operative (`/admin-filament/fornitore/lavorazioni`) & Scheda in TAB (Non Modal)**: - - Aggiornata la pagina secondo il design system moderno (KPI cards, badge di stato semantici, filtri reattivi, context toolbar). - - Eliminato il popup modale: il dettaglio della scheda ora vive all'interno di una **TAB in-page a larghezza intera ("Scheda Apparecchio")**, consentendo di alternare liberamente tra Elenco e Scheda senza perdere stato. - - Fedeltà grafica e funzionale a `01 schermata pricipale.PNG` di TecnoRepair: - - Header con Num. Scheda, Date (Ingresso, Orario, Cons. Prevista, Riconsegna), Stato Riparazione e Checkbox/Flags desktop (Fare Preventivo, Riparazione in sede, Riconsegnato, Rientro, Esame Tecnico, Ricons. No Ricevuta). - - Box "Dati Cliente" in alto a destra con pulsanti rapidi WhatsApp, Telefono ed SMS. - - 7 Sub-Tab: `1. Apparecchio in Entrata`, `2. Riparazione & Flussi`, `3. Ricambi Utilizzati`, `4. Preventivo - Costi - DDT`, `5. Annotazioni & Cortesia`, `6. Comunicazioni`, `7. C.Q. (Controllo Qualità)`. - - Matrice grafica 3x3 del Segno di Sblocco (Android pattern grid con 9 nodi interattivi). - - Salvataggio diretto modifiche su database (`saveActiveScheda()`) e pulsante stampa ricevuta. - - Supporto integrato anche per i Ticket Amministratore nello stesso formato Scheda. +3. **Integrazione Centralino Telefonico (Panasonic NS1000 / TAPI) & Wireframe ASCII**: + - Identificato lo script in esecuzione all'avvio di Windows: + - Task Schedulato `NetGescon Panasonic Live Bridge` (creato da `scripts/ops/windows/install-netgescon-panasonic-live-task.cmd` / `.ps1`). + - Script launcher: `start-netgescon-panasonic-live.cmd` / `.ps1` che esegue in loop `watch-netgescon-panasonic-tapi-dotnet-events.ps1`. + - Diagnosi del "furto di focus": + - Lato Windows: console PowerShell interattiva senza `-WindowStyle Hidden` che all'avvio del loop o su restart/errore apre una finestra e ruba il focus. + - Lato NetGescon: `TopbarLiveCall` eseguiva polling ogni 3s e, alla ricezione della chiamata, richiamava modali con direttive `autofocus` che catturavano il cursore. + - Soluzione architetturale push: + - Esecuzione Windows headless come Servizio o Task `SYSTEM` con reindirizzamento solo su file di log. + - Push immediato REST allo squillo (`POST /api/v1/cti/panasonic/incoming`) verso NetGescon. + - Floating toast non-intrusivo senza autofocus in NetGescon, espandibile su richiesta dell'operatore. + - Redatti **4 diagrammi ASCII Wireframe** dettagliati (architettura, floating toast, scheda chiamante Fornitore con riparazioni, scheda chiamante Amministratore con estratto conto spese/incassi/rate scadute e ticket aperti). ## Output del Giro Operativo ESITO_205: riuscito -TASK_ID: task-lavorazioni-tecnorepair-tab-sync +TASK_ID: task-unificazione-fornitore-rubrica-centralino REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git BRANCH: stabilization/205-zero -COMMIT: 7116305 +COMMIT: in_pubblicazione FILE_O_AREE_TOCCATE: -- .gitignore -- app/Filament/Pages/Condomini/NominativiStabile.php -- app/Filament/Pages/Fornitore/LavorazioniOperative.php -- app/Filament/Pages/Gescon/Ordinarie.php +- app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php +- app/Filament/Pages/Fornitore/PraticheTecnorepair.php +- app/Filament/Pages/Fornitore/RubricaClienti.php +- app/Filament/Pages/Fornitore/TicketOperativi.php +- app/Services/Fornitore/FornitoreRubricaSyncService.php - app/Services/Tecnorepair/TecnoRepairArchiveService.php -- resources/views/filament/pages/affitti/gestione-affitti.blade.php +- bootstrap/app.php - resources/views/filament/pages/fornitore/lavorazioni-operative.blade.php -- tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php +- resources/views/filament/pages/fornitore/rubrica-clienti.blade.php +- resources/views/filament/pages/fornitore/ticket-operativi.blade.php - skill-netgescon/control-tower/CURRENT-205.md TEST_ESEGUITI: - ./vendor/bin/pest tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php (6 passed, 54 assertions) @@ -57,8 +66,9 @@ ## Output del Giro Operativo - php artisan view:clear && php artisan view:cache (successo, zero errori) GATE_STATISTICS: - BLADE_COMPILATION: 100% pulita senza token inattesi. -- TECNOREPAIR_RECORDS: 1874 schede importate dal live MDB (/mnt/cservergo), max legacy_id 1879, inclusa #1876. -- LAVORAZIONI_TAB_UI: Nuova visualizzazione con TAB integrata in-page a 7 schede (no modal) e matrice di sblocco 3x3. +- TECNOREPAIR_RECORDS: 1874 schede unificate e sincronizzate direttamente dal live MDB (/mnt/cservergo), max legacy_id 1879, inclusa #1876. +- RUBRICA_CLIENTI: 3.716 contatti aggregati (1.243 TecnoRepair + 2.473 Contabilità MySQL arc_nehr + Ticket NetGescon). +- WIREFRAMES_CENTRALINO: 4 disegni ASCII completati per architettura no-focus-steal, floating toast e schede contestuali Fornitore/Amministratore. - TEST_SUITE: 47 test Feature passati (299 asserzioni, 100% pass). BLOCCO_DATI: no BLOCCO_CONTRATTO: no @@ -68,4 +78,6 @@ ## Prossimo Passo per .200 (Validazione) - Eseguire il checkout del branch `stabilization/205-zero`. - Eseguire la suite di test Pest (47 passed, 299 assertions). -- Aprire http://192.168.0.205:8000/admin-filament/fornitore/lavorazioni e verificare il nuovo layout grafico con KPI, filtri, tabella con badge di stato e apertura della Scheda Apparecchio in formato TAB a tutta pagina con la scheda 1876. +- Verificare su http://192.168.0.205:8000/admin-filament/fornitore/pratiche il conteggio di 1874 schede e il perfetto funzionamento del pulsante "Sincronizza MDB TecnoRepair". +- Verificare su http://192.168.0.205:8000/admin-filament/fornitore/rubrica-clienti i 3.716 contatti con badge sorgente e filtri. +- Valutare i wireframe ASCII per l'integrazione del centralino prima di avviare l'implementazione del listener/popup.
ClienteSorgente Recapiti Zona Schede
{{ $row['display_name'] }}
-
Fonte {{ $row['source'] === 'tecnorepair_tclienti' ? 'TClienti' : 'ticket' }}
+ @if(!empty($row['codice_fiscale']) || !empty($row['partita_iva'])) +
{{ $row['codice_fiscale'] ?: $row['partita_iva'] }}
+ @endif +
+ @if(($row['source'] ?? '') === 'tecnorepair_tclienti') + TecnoRepair + @elseif(($row['source'] ?? '') === 'contabilita_mysql') + Contabilità + @elseif(($row['source'] ?? '') === 'netgescon_ticket') + Ticket + @else + {{ $row['source'] }} + @endif
{{ $row['phone'] !== '' ? $row['phone'] : '-' }}
@@ -97,7 +123,7 @@
Nessun cliente disponibile. Importa prima `TClienti` nell'archivio legacy e poi lancia il comando rubrica clienti.Nessun contatto trovato. Clicca su "⚡ Sincronizza Rubrica" per importare da TecnoRepair, Contabilità e Ticket.