508 lines
21 KiB
PHP
508 lines
21 KiB
PHP
<?php
|
|
namespace App\Services\Tecnorepair;
|
|
|
|
use App\Models\Amministratore;
|
|
use App\Models\AssistenzaTecnorepairAllegato;
|
|
use App\Models\AssistenzaTecnorepairScheda;
|
|
use App\Models\Fornitore;
|
|
use App\Models\Product;
|
|
use App\Models\ProductIdentifier;
|
|
use App\Models\ProductOffer;
|
|
use App\Models\ProductSerial;
|
|
use App\Services\Catalog\FornitoreProductCatalogService;
|
|
use App\Services\Catalog\ProductOfferService;
|
|
use App\Support\TecnoRepairMdbReader;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use RuntimeException;
|
|
|
|
class TecnoRepairArchiveService
|
|
{
|
|
public const DEFAULT_WINDOWS_UNC_PATH = '\\\\192.168.0.36\\CServerGO\\LunaSoftware_TecnoRepair\\Archivi\\TecnoRepairDB.mdb';
|
|
public const DEFAULT_LOCAL_PATH = '/home/michele/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb';
|
|
|
|
public function __construct(
|
|
private readonly TecnoRepairMdbReader $reader,
|
|
private readonly FornitoreProductCatalogService $catalogService,
|
|
private readonly ProductOfferService $productOfferService,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Resolve the readable local path for TecnoRepairDB.mdb, handling Windows UNC paths and local replicas.
|
|
*/
|
|
public function resolveMdbPath(?string $candidatePath = null, ?Fornitore $fornitore = null): string
|
|
{
|
|
$path = trim((string) $candidatePath);
|
|
|
|
if ($path === '' && $fornitore instanceof Fornitore) {
|
|
$config = (array) ($fornitore->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<string, mixed>
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
}
|