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; } }