operational_config ?? []); $path = trim((string) data_get($config, 'tecnorepair.mdb_path', '')); if ($path === '') { $path = trim((string) data_get($config, 'tecnorepair.local_fallback_path', '')); } } if ($path === '') { $path = self::DEFAULT_WINDOWS_UNC_PATH; } // If path is a local existing file, return realpath if (file_exists($path) && is_file($path)) { return realpath($path) ?: $path; } // Candidates for fallback $fallbackCandidates = [ 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)) { return realpath($cand) ?: $cand; } } throw new RuntimeException("Archivio TecnoRepair non trovato sul percorso specificato ($path) e nessuna replica locale trovata."); } /** * Import entire TecnoRepair archive (Schede, Clienti, Allegati, Ricambi/Prodotti). * * @return array */ public function importArchive( int $amministratoreId, int $fornitoreId, ?string $mdbPath = null, int $limit = 0, bool $dryRun = false ): array { $fornitore = Fornitore::query()->findOrFail($fornitoreId); $resolvedPath = $this->resolveMdbPath($mdbPath, $fornitore); $tables = $this->reader->listTables($resolvedPath); foreach (['TClienti', 'TApparecchi', 'TAllegati'] as $requiredTable) { if (! in_array($requiredTable, $tables, true)) { throw new RuntimeException("Tabella richiesta mancante nel file MDB: {$requiredTable}"); } } $clientiRows = $this->reader->exportTable($resolvedPath, 'TClienti'); $schedeRows = $this->reader->exportTable($resolvedPath, 'TApparecchi'); $allegatiRows = $this->reader->exportTable($resolvedPath, 'TAllegati'); $ricambiRows = in_array('TRicambiAnag', $tables, true) ? $this->reader->exportTable($resolvedPath, 'TRicambiAnag') : []; $ricambiSchedaRows = in_array('TRicambiScheda', $tables, true) ? $this->reader->exportTable($resolvedPath, 'TRicambiScheda') : []; $schedeRows = array_reverse($schedeRows); if ($limit > 0) { $schedeRows = array_slice($schedeRows, 0, $limit); } $clienti = []; foreach ($clientiRows as $clienteRow) { $cId = $this->toInt($clienteRow['ID'] ?? null); if ($cId !== null) { $clienti[$cId] = $clienteRow; } } $allegatiByScheda = []; foreach ($allegatiRows as $allegatoRow) { $sId = $this->toInt($allegatoRow['ID_Scheda'] ?? null); if ($sId !== null) { $allegatiByScheda[$sId][] = $allegatoRow; } } $ricambiByScheda = []; foreach ($ricambiSchedaRows as $rs) { $sId = $this->toInt($rs['ID_Scheda'] ?? null); if ($sId !== null) { $ricambiByScheda[$sId][] = $rs; } } $stats = [ 'mdb_path' => $resolvedPath, 'schede_lette' => count($schedeRows), 'schede_create' => 0, 'schede_aggiornate' => 0, 'schede_saltate' => 0, 'allegati_importati' => 0, 'seriali_allineati' => 0, 'ricambi_catalogati' => 0, 'dry_run' => $dryRun, ]; // 1. Process Ricambi / Spare parts as Products if (! empty($ricambiRows)) { foreach ($ricambiRows as $ricambio) { $codice = trim((string) ($ricambio['Codice'] ?? '')); $descrizione = trim((string) ($ricambio['Descrizione'] ?? '')); if ($codice === '' && $descrizione === '') { continue; } if ($dryRun) { $stats['ricambi_catalogati']++; continue; } $barcode = trim((string) ($ricambio['CodBarre'] ?? '')); $prezzoAcq = (float) str_replace(',', '.', (string) ($ricambio['PrezzoAcq'] ?? 0)); $prezzoUltAcq = (float) str_replace(',', '.', (string) ($ricambio['PrezzoUltAcq'] ?? 0)); $costo = $prezzoAcq > 0 ? $prezzoAcq : ($prezzoUltAcq > 0 ? $prezzoUltAcq : null); $przList1 = (float) str_replace(',', '.', (string) ($ricambio['PrzList1'] ?? 0)); $name = $descrizione !== '' ? $descrizione : ('Ricambio ' . $codice); $res = $this->catalogService->resolveOrCreateProduct($fornitore, [ 'name' => $name, 'internal_code' => $codice ?: ('RIC-' . ($ricambio['ID'] ?? uniqid())), 'type' => 'spare_part', 'brand' => 'TecnoRepair', 'description' => $descrizione, 'track_serials' => true, 'meta' => [ 'source' => 'tecnorepair_ricambi', 'raw_id' => $ricambio['ID'] ?? null, 'pos_magazzino' => $ricambio['PosizioneMagazzino'] ?? null, 'prz_list1' => $przList1, ], 'identifiers' => array_filter([ $codice !== '' ? [ 'fornitore_id' => $fornitore->id, 'code_type' => 'vendor_sku', 'code_role' => 'primary', 'code_value' => $codice, 'source' => 'tecnorepair_anag', ] : null, $barcode !== '' ? [ 'fornitore_id' => $fornitore->id, 'code_type' => 'barcode', 'code_role' => 'barcode', 'code_value' => $barcode, 'source' => 'tecnorepair_anag', ] : null, ]), ]); $product = $res['product']; if ($costo !== null && $costo > 0) { $this->productOfferService->syncInternalSupplierOffer($product, $fornitore, [ 'price_amount' => $costo, 'currency' => 'EUR', 'availability' => 'in_stock', 'meta' => [ 'prz_list1' => $przList1, 'source_type' => 'tecnorepair_ricambi', ], ]); } $stats['ricambi_catalogati']++; } } // 2. Process Schede / Repair cards $runner = function () use ( $amministratoreId, $fornitore, $resolvedPath, $schedeRows, $clienti, $allegatiByScheda, $ricambiByScheda, $dryRun, &$stats ): void { foreach ($schedeRows as $row) { $legacyId = $this->toInt($row['ID'] ?? null); if ($legacyId === null) { $stats['schede_saltate']++; continue; } $legacyClienteId = $this->toInt($row['ID_Cliente'] ?? null); $cliente = $legacyClienteId !== null ? ($clienti[$legacyClienteId] ?? []) : []; $statusLabel = $this->clean($row['StatoRiparazione'] ?? null) ?: $this->clean($row['ID_StatoRip'] ?? null); $payload = [ 'amministratore_id' => $amministratoreId, 'fornitore_id' => $fornitore->id, 'legacy_id' => $legacyId, 'legacy_cliente_id' => $legacyClienteId, 'legacy_centro_ass_id' => $this->toInt($row['ID_CentroAss'] ?? null), 'legacy_committente_codice' => $this->clean($row['Cod_Committente'] ?? null), 'legacy_numero_scheda' => $this->clean($row['NumeroScheda'] ?? null), 'customer_name' => $this->clean($cliente['NomeCognome'] ?? null), 'customer_phone' => $this->clean($cliente['NumeroTelefono'] ?? null), 'customer_phone_alt' => $this->clean($cliente['TelFisso'] ?? null), 'customer_email' => $this->clean($cliente['Email'] ?? null), 'product_model' => $this->clean($row['Modello'] ?? null), 'product_code' => $this->clean($row['CodiceProdotto'] ?? null), 'serial_number' => $this->clean($row['SerialNumber'] ?? null), 'serial_number_2' => $this->clean($row['SerialNumber2'] ?? null), 'status_code' => $this->clean($row['ID_StatoRip'] ?? null), 'status_label' => $statusLabel, 'status_bucket' => AssistenzaTecnorepairScheda::normalizeStatusBucket($statusLabel), 'defect_reported' => $this->clean($row['DifettoSegnalato'] ?? null), 'repair_description' => $this->clean($row['DescrizioneRiparazione'] ?? null), 'communications' => $this->clean($row['Comunicazioni'] ?? null), 'operator_name' => $this->clean($row['NomeOperatore'] ?? null), 'technician_name' => $this->clean($row['NomeTecnicoRiparatore'] ?? null), 'date_received' => $this->normalizeDate($row['DataIngresso'] ?? null), 'ordered_at' => $this->normalizeDate($row['DataOrdine'] ?? null), 'order_number' => $this->clean($row['NumOrdine'] ?? null), 'rma_code' => $this->clean($row['CodiceRMA'] ?? null), 'pin_code' => $this->clean($row['CodicePIN'] ?? null), 'unlock_code' => $this->clean($row['CodiceSblocco'] ?? null), 'legacy_attachment_path' => $this->clean($row['FileAllegato1'] ?? null), 'imported_from_path' => $resolvedPath, 'imported_at' => now(), 'metadata' => [ 'cliente' => $cliente, 'raw' => $row, 'ricambi_scheda' => $ricambiByScheda[$legacyId] ?? [], ], ]; $existing = AssistenzaTecnorepairScheda::query() ->where('amministratore_id', $amministratoreId) ->where('legacy_id', $legacyId) ->first(); if ($dryRun) { if ($existing) { $stats['schede_aggiornate']++; } else { $stats['schede_create']++; } continue; } if ($existing instanceof AssistenzaTecnorepairScheda) { $existing->fill($payload); $existing->save(); $scheda = $existing; $stats['schede_aggiornate']++; } else { $scheda = AssistenzaTecnorepairScheda::query()->create($payload); $stats['schede_create']++; } // Align ProductSerial $serial = ProductSerial::query()->updateOrCreate( ['legacy_scheda_id' => (int) $scheda->id], [ 'fornitore_id' => $fornitore->id, 'customer_name' => $scheda->customer_name, 'product_model' => $scheda->product_model, 'product_code' => $scheda->product_code, 'serial_number' => $scheda->serial_number, 'serial_number_2' => $scheda->serial_number_2, 'date_received' => $scheda->date_received, 'internal_notes' => $scheda->communications, 'source' => 'tecnorepair_mdb', 'source_reference' => (string) ($scheda->legacy_numero_scheda ?: $scheda->legacy_id), ] ); $stats['seriali_allineati']++; $this->catalogService->syncTecnorepairProduct($fornitore, $scheda, $serial); // Allegati AssistenzaTecnorepairAllegato::query()->where('scheda_id', (int) $scheda->id)->delete(); foreach ($allegatiByScheda[$legacyId] ?? [] as $allegatoRow) { AssistenzaTecnorepairAllegato::query()->create([ 'scheda_id' => (int) $scheda->id, 'legacy_id' => $this->toInt($allegatoRow['ID'] ?? null), 'legacy_scheda_id' => $legacyId, 'legacy_attachment_number' => $this->toInt($allegatoRow['NumAllegato'] ?? null), 'file_name' => basename((string) ($allegatoRow['FileAllegato'] ?? 'allegato')), 'file_path' => $this->clean($allegatoRow['FileAllegato'] ?? null), 'imported_from_path' => $resolvedPath, 'metadata' => ['raw' => $allegatoRow], ]); $stats['allegati_importati']++; } } }; if ($dryRun) { $runner(); } else { DB::transaction($runner); // Update Fornitore operational_config with last sync info $config = (array) ($fornitore->operational_config ?? []); $config['tecnorepair']['last_sync'] = [ 'timestamp' => now()->toIso8601String(), 'resolved_path' => $resolvedPath, 'schede_lette' => $stats['schede_lette'], 'schede_create' => $stats['schede_create'], 'schede_aggiornate' => $stats['schede_aggiornate'], 'ricambi_catalogati' => $stats['ricambi_catalogati'], 'seriali_allineati' => $stats['seriali_allineati'], ]; $fornitore->operational_config = $config; $fornitore->save(); } return $stats; } /** * Automated Workflow 1: Mark repair as completed (RIPARATO). */ public function chiudiComeRiparato( AssistenzaTecnorepairScheda $scheda, ?string $note = null, ?string $tecnico = null ): bool { $meta = (array) ($scheda->metadata ?? []); $meta['workflow_status'] = 'repaired'; $meta['repaired_at'] = now()->toIso8601String(); $scheda->status_code = '4'; $scheda->status_label = 'RIPARATO'; $scheda->status_bucket = 'completed'; if ($tecnico) { $scheda->technician_name = $tecnico; } if ($note) { $prevNotes = trim((string) $scheda->repair_description); $scheda->repair_description = $prevNotes !== '' ? ($prevNotes . "\n[" . now()->format('d/m/Y H:i') . "] " . $note) : $note; } $scheda->metadata = $meta; return $scheda->save(); } /** * Automated Workflow 2: Mark device as scrapped or used for spare parts (DISPOSITIVO IRRIPARABILE / PZ-RICAM). */ public function chiudiComeRottamato( AssistenzaTecnorepairScheda $scheda, ?string $motivo = null, bool $comeRicambi = false ): bool { $meta = (array) ($scheda->metadata ?? []); $meta['workflow_status'] = $comeRicambi ? 'spare_parts' : 'scrapped'; $meta['scrapped_at'] = now()->toIso8601String(); $meta['scrap_reason'] = $motivo; if ($comeRicambi) { $scheda->status_code = '23'; $scheda->status_label = 'PZ-RICAM - NOTEBOOK PC DA UTILIZZARE COME RICAMBI'; $scheda->status_bucket = 'scrapped'; } else { $scheda->status_code = '10'; $scheda->status_label = 'DISPOSITIVO IRRIPARABILE'; $scheda->status_bucket = 'cancelled'; } $desc = trim((string) $scheda->repair_description); $append = "Apparato contrassegnato per " . ($comeRicambi ? 'recupero ricambi' : 'rottamazione/smaltimento') . ". Motivo: " . ($motivo ?: 'Non riparabile economicamente'); $scheda->repair_description = $desc !== '' ? ($desc . "\n[" . now()->format('d/m/Y H:i') . "] " . $append) : $append; $scheda->metadata = $meta; return $scheda->save(); } /** * Automated Workflow 3: Return to vendor / Warranty RMA (MANDATO IN GARANZIA AL PRODUTTORE). */ public function rendiAFornitoreRma( AssistenzaTecnorepairScheda $scheda, ?string $rmaCode = null, ?int $fornitoreResoId = null, ?string $note = null ): bool { $meta = (array) ($scheda->metadata ?? []); $generatedRma = $rmaCode ?: ('RMA-' . date('Ymd') . '-' . ($scheda->legacy_numero_scheda ?: $scheda->id)); $meta['workflow_status'] = 'vendor_rma'; $meta['rma_initiated_at'] = now()->toIso8601String(); $meta['rma_fornitore_id'] = $fornitoreResoId; $scheda->status_code = '33'; $scheda->status_label = 'MANDATO IN GARANZIA AL PRODUTTORE'; $scheda->status_bucket = 'in_progress'; $scheda->rma_code = $generatedRma; $comm = trim((string) $scheda->communications); $append = "Inviato in RMA / Garanzia fornitore. Codice RMA: {$generatedRma}." . ($note ? " Note: {$note}" : ''); $scheda->communications = $comm !== '' ? ($comm . "\n[" . now()->format('d/m/Y H:i') . "] " . $append) : $append; $scheda->metadata = $meta; return $scheda->save(); } private function clean(mixed $value): ?string { if (! is_string($value) && ! is_numeric($value)) { return null; } $value = trim((string) $value); return $value !== '' ? $value : null; } private function toInt(mixed $value): ?int { if ($value === null || $value === '') { return null; } if (! is_numeric($value)) { return null; } return (int) $value; } private function normalizeDate(mixed $value): ?string { $value = $this->clean($value); if ($value === null) { return null; } try { return Carbon::parse($value)->toDateTimeString(); } catch (\Throwable) { return null; } } }