feat(fornitore): riproduzione web tecnorepair pratiche e schede, automazioni rma ricambi e sync contabilita nethome
This commit is contained in:
parent
6d0d474041
commit
00d4417dc7
|
|
@ -35,12 +35,11 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ
|
|||
}
|
||||
|
||||
$rawMdbPath = trim((string) $this->option('mdb'));
|
||||
$mdbPath = $rawMdbPath !== '' && file_exists($rawMdbPath)
|
||||
? (realpath($rawMdbPath) ?: $rawMdbPath)
|
||||
: $rawMdbPath;
|
||||
|
||||
if ($mdbPath === '' || ! is_file($mdbPath)) {
|
||||
$this->error('Archivio MDB non trovato: ' . $mdbPath);
|
||||
$service = app(\App\Services\Tecnorepair\TecnoRepairArchiveService::class);
|
||||
try {
|
||||
$mdbPath = $service->resolveMdbPath($rawMdbPath);
|
||||
} catch (\Throwable $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
|
|
|||
527
app/Filament/Pages/Fornitore/PraticheTecnorepair.php
Normal file
527
app/Filament/Pages/Fornitore/PraticheTecnorepair.php
Normal file
|
|
@ -0,0 +1,527 @@
|
|||
<?php
|
||||
namespace App\Filament\Pages\Fornitore;
|
||||
|
||||
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
||||
use App\Models\AssistenzaTecnorepairAllegato;
|
||||
use App\Models\AssistenzaTecnorepairScheda;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\User;
|
||||
use App\Services\Catalog\ContabilitaMysqlSerialImportService;
|
||||
use App\Services\Tecnorepair\TecnoRepairArchiveService;
|
||||
use BackedEnum;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use UnitEnum;
|
||||
|
||||
class PraticheTecnorepair extends Page
|
||||
{
|
||||
use ResolvesOperatoreContext;
|
||||
|
||||
protected static ?string $navigationLabel = 'Pratiche TecnoRepair';
|
||||
|
||||
protected static ?string $title = 'Pratiche & Riparazioni TecnoRepair';
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-computer-desktop';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Fornitore';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
protected static ?string $slug = 'fornitore/pratiche';
|
||||
|
||||
protected string $view = 'filament.pages.fornitore.pratiche-tecnorepair';
|
||||
|
||||
public ?int $fornitoreId = null;
|
||||
|
||||
public ?string $fornitoreLabel = null;
|
||||
|
||||
public bool $missingAdminContext = false;
|
||||
|
||||
/** @var array<int, string> */
|
||||
public array $fornitoriOptions = [];
|
||||
|
||||
// Filter properties
|
||||
public string $searchMatricola = '';
|
||||
|
||||
public string $searchNumScheda = '';
|
||||
|
||||
public string $searchCliente = '';
|
||||
|
||||
public bool $searchClienteParziale = true;
|
||||
|
||||
public string $searchNumOrdine = '';
|
||||
|
||||
public string $filterStato = '0';
|
||||
|
||||
public bool $escludiRiconsegnati = true;
|
||||
|
||||
public bool $soloRiconsegnati = false;
|
||||
|
||||
public ?int $nonLavorateDaGiorni = null;
|
||||
|
||||
public string $dataIngressoDal = '';
|
||||
|
||||
public string $dataIngressoAl = '';
|
||||
|
||||
// Active Scheda modal state
|
||||
public ?int $selectedSchedaId = null;
|
||||
|
||||
public string $schedaTab = 'entrata';
|
||||
|
||||
/** @var array<string, mixed>|null */
|
||||
public ?array $activeScheda = null;
|
||||
|
||||
// Workflow inputs for active scheda
|
||||
public string $workflowNote = '';
|
||||
|
||||
public string $workflowMotivoRottamazione = '';
|
||||
|
||||
public bool $workflowComeRicambi = false;
|
||||
|
||||
public string $workflowRmaCode = '';
|
||||
|
||||
public ?int $workflowFornitoreResoId = null;
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
public array $rows = [];
|
||||
|
||||
/** @var array<string, int> */
|
||||
public array $totals = [
|
||||
'totale' => 0,
|
||||
'aperte' => 0,
|
||||
'riparate' => 0,
|
||||
'rottamate' => 0,
|
||||
'rma_garanzia' => 0,
|
||||
];
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore']);
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
[$fornitore] = $this->resolveOperatoreContext(allowAdminWithoutSupplier: true);
|
||||
|
||||
if (! $fornitore instanceof Fornitore) {
|
||||
$this->missingAdminContext = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->fornitoreId = (int) $fornitore->id;
|
||||
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
||||
|
||||
$user = Auth::user();
|
||||
if ($this->isInternalOperator($user)) {
|
||||
$this->fornitoriOptions = Fornitore::query()
|
||||
->where(function ($q) {
|
||||
$q->whereHas('productSerials')
|
||||
->orWhere('partita_iva', '10055221005')
|
||||
->orWhere('partita_iva', '14001151001')
|
||||
->orWhereIn('id', [236, 392]);
|
||||
})
|
||||
->orderBy('ragione_sociale')
|
||||
->get()
|
||||
->mapWithKeys(fn(Fornitore $f): array => [
|
||||
(int) $f->id => trim((string) ($f->ragione_sociale ?: ('Fornitore #' . $f->id))) . ($f->partita_iva ? ' (' . $f->partita_iva . ')' : ''),
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
// Optional date range
|
||||
$this->dataIngressoDal = trim((string) request()->query('dal', ''));
|
||||
$this->dataIngressoAl = trim((string) request()->query('al', ''));
|
||||
|
||||
// Optional query params
|
||||
if (request()->has('scheda')) {
|
||||
$this->searchNumScheda = trim((string) request()->query('scheda'));
|
||||
}
|
||||
if (request()->has('q')) {
|
||||
$this->searchMatricola = trim((string) request()->query('q'));
|
||||
}
|
||||
|
||||
$this->refreshData();
|
||||
|
||||
if (request()->has('open')) {
|
||||
$openId = (int) request()->query('open');
|
||||
if ($openId > 0) {
|
||||
$this->openScheda($openId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedFornitoreId(): void
|
||||
{
|
||||
$fornitore = Fornitore::query()->find((int) $this->fornitoreId);
|
||||
if ($fornitore instanceof Fornitore) {
|
||||
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
||||
$this->selectedSchedaId = null;
|
||||
$this->activeScheda = null;
|
||||
$this->refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedSearchMatricola(): void { $this->refreshData(); }
|
||||
public function updatedSearchNumScheda(): void { $this->refreshData(); }
|
||||
public function updatedSearchCliente(): void { $this->refreshData(); }
|
||||
public function updatedFilterStato(): void { $this->refreshData(); }
|
||||
public function updatedEscludiRiconsegnati(): void { $this->refreshData(); }
|
||||
public function updatedSoloRiconsegnati(): void { $this->refreshData(); }
|
||||
|
||||
public function resetFiltri(): void
|
||||
{
|
||||
$this->searchMatricola = '';
|
||||
$this->searchNumScheda = '';
|
||||
$this->searchCliente = '';
|
||||
$this->searchNumOrdine = '';
|
||||
$this->filterStato = '0';
|
||||
$this->escludiRiconsegnati = true;
|
||||
$this->soloRiconsegnati = false;
|
||||
$this->nonLavorateDaGiorni = null;
|
||||
$this->dataIngressoDal = now()->subYears(2)->startOfYear()->format('Y-m-d');
|
||||
$this->dataIngressoAl = now()->addYear()->endOfYear()->format('Y-m-d');
|
||||
$this->refreshData();
|
||||
}
|
||||
|
||||
public function refreshData(): void
|
||||
{
|
||||
if (! $this->fornitoreId) {
|
||||
$this->rows = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
});
|
||||
|
||||
// Search filters
|
||||
if (trim($this->searchMatricola) !== '') {
|
||||
$val = trim($this->searchMatricola);
|
||||
$query->where(function (Builder $b) use ($val) {
|
||||
$b->where('serial_number', 'like', "%{$val}%")
|
||||
->orWhere('serial_number_2', 'like', "%{$val}%")
|
||||
->orWhere('product_code', 'like', "%{$val}%");
|
||||
});
|
||||
}
|
||||
|
||||
if (trim($this->searchNumScheda) !== '') {
|
||||
$val = trim($this->searchNumScheda);
|
||||
$query->where(function (Builder $b) use ($val) {
|
||||
$b->where('legacy_numero_scheda', 'like', "%{$val}%")
|
||||
->orWhere('legacy_id', $val);
|
||||
});
|
||||
}
|
||||
|
||||
if (trim($this->searchCliente) !== '') {
|
||||
$val = trim($this->searchCliente);
|
||||
$query->where('customer_name', 'like', "%{$val}%");
|
||||
}
|
||||
|
||||
if (trim($this->searchNumOrdine) !== '') {
|
||||
$val = trim($this->searchNumOrdine);
|
||||
$query->where('order_number', 'like', "%{$val}%");
|
||||
}
|
||||
|
||||
if ($this->filterStato !== '0' && trim($this->filterStato) !== '') {
|
||||
$st = trim($this->filterStato);
|
||||
$query->where(function (Builder $b) use ($st) {
|
||||
$b->where('status_code', $st)
|
||||
->orWhere('status_label', 'like', "%{$st}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->escludiRiconsegnati) {
|
||||
$query->where(function (Builder $b) {
|
||||
$b->whereNull('metadata->raw->isRiconsegnato')
|
||||
->orWhere('metadata->raw->isRiconsegnato', 0)
|
||||
->orWhere('metadata->raw->isRiconsegnato', '0')
|
||||
->orWhere('metadata->raw->isRiconsegnato', false);
|
||||
});
|
||||
} elseif ($this->soloRiconsegnati) {
|
||||
$query->where(function (Builder $b) {
|
||||
$b->where('metadata->raw->isRiconsegnato', 1)
|
||||
->orWhere('metadata->raw->isRiconsegnato', '1')
|
||||
->orWhere('metadata->raw->isRiconsegnato', true);
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->dataIngressoDal !== '') {
|
||||
$query->where('date_received', '>=', Carbon::parse($this->dataIngressoDal)->startOfDay());
|
||||
}
|
||||
if ($this->dataIngressoAl !== '') {
|
||||
$query->where('date_received', '<=', Carbon::parse($this->dataIngressoAl)->endOfDay());
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
$this->totals = [
|
||||
'totale' => (clone $baseTotalQuery)->count(),
|
||||
'riparate' => (clone $baseTotalQuery)->where('status_code', '4')->count(),
|
||||
'rottamate' => (clone $baseTotalQuery)->whereIn('status_code', ['10', '23', '37'])->count(),
|
||||
'rma_garanzia' => (clone $baseTotalQuery)->where('status_code', '33')->count(),
|
||||
'aperte' => (clone $baseTotalQuery)->whereNotIn('status_code', ['4', '10', '23', '37'])->count(),
|
||||
];
|
||||
|
||||
$records = $query->orderByDesc('legacy_id')->limit(120)->get();
|
||||
|
||||
$this->rows = $records->map(function (AssistenzaTecnorepairScheda $s): array {
|
||||
$raw = (array) data_get($s->metadata, 'raw', []);
|
||||
$isRiconsegnato = (bool) ($raw['isRiconsegnato'] ?? false);
|
||||
$statusCode = (string) ($s->status_code ?: '');
|
||||
$statusLabel = strtoupper((string) ($s->status_label ?: 'IN LAVORAZIONE'));
|
||||
|
||||
// Row style logic matching TecnoRepair screenshots:
|
||||
// Green: RIPARATO (4) / ACQUISTO RICONDIZIONATO (31)
|
||||
// Red: DISPOSITIVO IRRIPARABILE (10) / PZ-RICAM (23) / SMALTITO (37)
|
||||
// Blue: INGRESSO ACCETTAZIONE (21)
|
||||
// Amber: ATTESA RICAMBIO (5) / LABORATORIO ESTERNO (27)
|
||||
$rowColor = 'white';
|
||||
if (in_array($statusCode, ['4', '31'], true) || str_contains($statusLabel, 'RIPARATO') || str_contains($statusLabel, 'ACQUISTO RICONDIZIONATO')) {
|
||||
$rowColor = 'green';
|
||||
} elseif (in_array($statusCode, ['10', '23', '37'], true) || str_contains($statusLabel, 'IRRIPARABILE') || str_contains($statusLabel, 'RICAMBI') || str_contains($statusLabel, 'SMALTITO')) {
|
||||
$rowColor = 'red';
|
||||
} elseif ($statusCode === '21' || str_contains($statusLabel, 'INGRESSO') || str_contains($statusLabel, 'ACCETTAZIONE')) {
|
||||
$rowColor = 'blue';
|
||||
} elseif (in_array($statusCode, ['5', '27', '30'], true) || str_contains($statusLabel, 'ATTESA') || str_contains($statusLabel, 'URGENTE')) {
|
||||
$rowColor = 'amber';
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $s->id,
|
||||
'legacy_id' => (int) ($s->legacy_id ?: $s->id),
|
||||
'numero_scheda' => (string) ($s->legacy_numero_scheda ?: $s->legacy_id ?: $s->id),
|
||||
'ingresso' => optional($s->date_received)->format('d/m/Y') ?: '-',
|
||||
'cliente' => (string) ($s->customer_name ?: '-'),
|
||||
'telefono' => (string) ($s->customer_phone ?: $s->customer_phone_alt ?: '-'),
|
||||
'difetto' => (string) ($s->defect_reported ?: '-'),
|
||||
'marca' => (string) ($raw['Cod_Marca'] ?? $this->extractBrand($s->product_model)),
|
||||
'modello' => (string) ($s->product_model ?: '-'),
|
||||
'seriale' => (string) ($s->serial_number ?: '-'),
|
||||
'cod_prodotto' => (string) ($s->product_code ?: '-'),
|
||||
'riconsegnato' => $isRiconsegnato,
|
||||
'data_ricons' => ! empty($raw['DataRiconsegna']) ? Carbon::parse($raw['DataRiconsegna'])->format('d/m/Y') : '-',
|
||||
'num_ordine' => (string) ($s->order_number ?: '0'),
|
||||
'stato_rip' => $statusLabel,
|
||||
'status_code' => $statusCode,
|
||||
'operatore' => (string) ($s->operator_name ?: $s->technician_name ?: 'MICHELE BARONE'),
|
||||
'row_color' => $rowColor,
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
public function openScheda(int $schedaId): void
|
||||
{
|
||||
$scheda = AssistenzaTecnorepairScheda::query()
|
||||
->with(['allegati'])
|
||||
->find($schedaId);
|
||||
|
||||
if (! $scheda instanceof AssistenzaTecnorepairScheda) {
|
||||
return;
|
||||
}
|
||||
|
||||
$raw = (array) data_get($scheda->metadata, 'raw', []);
|
||||
$cliente = (array) data_get($scheda->metadata, 'cliente', []);
|
||||
$ricambi = (array) data_get($scheda->metadata, 'ricambi_scheda', []);
|
||||
|
||||
$this->selectedSchedaId = (int) $scheda->id;
|
||||
$this->schedaTab = 'entrata';
|
||||
$this->workflowNote = '';
|
||||
$this->workflowRmaCode = (string) ($scheda->rma_code ?: ('RMA-' . date('Ymd') . '-' . ($scheda->legacy_numero_scheda ?: $scheda->id)));
|
||||
|
||||
$this->activeScheda = [
|
||||
'id' => (int) $scheda->id,
|
||||
'numero_scheda' => (string) ($scheda->legacy_numero_scheda ?: $scheda->legacy_id ?: $scheda->id),
|
||||
'data_ingresso' => optional($scheda->date_received)->format('d/m/Y H:i') ?: '-',
|
||||
'data_riparazione' => ! empty($raw['DataRiparazione']) ? Carbon::parse($raw['DataRiparazione'])->format('d/m/Y H:i') : '-',
|
||||
'data_riconsegna' => ! empty($raw['DataRiconsegna']) ? Carbon::parse($raw['DataRiconsegna'])->format('d/m/Y') : '-',
|
||||
'stato_rip' => (string) ($scheda->status_label ?: 'INGRESSO ACCETTAZIONE'),
|
||||
'status_code' => (string) ($scheda->status_code ?: '21'),
|
||||
'rma_code' => (string) ($scheda->rma_code ?: ''),
|
||||
'num_ordine' => (string) ($scheda->order_number ?: '0'),
|
||||
// Cliente
|
||||
'cliente_nome' => (string) ($scheda->customer_name ?: '-'),
|
||||
'cliente_email' => (string) ($scheda->customer_email ?: '-'),
|
||||
'cliente_telefono' => (string) ($scheda->customer_phone ?: '-'),
|
||||
'cliente_fisso' => (string) ($scheda->customer_phone_alt ?: '-'),
|
||||
'cliente_indirizzo' => (string) ($cliente['Indirizzo'] ?? '-'),
|
||||
'cliente_citta' => (string) ($cliente['Citta'] ?? '-'),
|
||||
// Apparecchio
|
||||
'tipo_apparecchio' => (string) ($raw['Cod_TipoApparecchio'] ?? 'AIO / PC / Dispositivo'),
|
||||
'marca' => (string) ($raw['Cod_Marca'] ?? $this->extractBrand($scheda->product_model)),
|
||||
'modello' => (string) ($scheda->product_model ?: '-'),
|
||||
'codice_prodotto' => (string) ($scheda->product_code ?: '-'),
|
||||
'seriale' => (string) ($scheda->serial_number ?: '-'),
|
||||
'seriale_2' => (string) ($scheda->serial_number_2 ?: '-'),
|
||||
'accessori' => (string) ($raw['AccessoriConsegnati'] ?? 'NESSUNO'),
|
||||
'difetto' => (string) ($scheda->defect_reported ?: '-'),
|
||||
'richieste_cliente' => (string) ($raw['RiparazioneRichiesta'] ?? '-'),
|
||||
'stato_generale' => (string) ($raw['StatoGenerale'] ?? '-'),
|
||||
'codice_pin' => (string) ($scheda->pin_code ?: '-'),
|
||||
'codice_sblocco' => (string) ($scheda->unlock_code ?: '-'),
|
||||
'tecnico' => (string) ($scheda->technician_name ?: $scheda->operator_name ?: 'MICHELE BARONE'),
|
||||
// Riparazione
|
||||
'descrizione_lavorazione'=> (string) ($scheda->repair_description ?: '-'),
|
||||
'comunicazioni' => (string) ($scheda->communications ?: '-'),
|
||||
// Economico
|
||||
'costo_subito' => (float) str_replace(',', '.', (string) ($raw['CostoSubito'] ?? 0)),
|
||||
'costo_addebitato' => (float) str_replace(',', '.', (string) ($raw['CostoAddebitato'] ?? 0)),
|
||||
'acconto' => (float) str_replace(',', '.', (string) ($raw['AccontoRiparazione'] ?? 0)),
|
||||
'preventivo' => (float) str_replace(',', '.', (string) ($raw['CostoPreventivo'] ?? 0)),
|
||||
// Ricambi
|
||||
'ricambi' => $ricambi,
|
||||
'allegati' => $scheda->allegati->map(fn(AssistenzaTecnorepairAllegato $a) => [
|
||||
'nome' => $a->file_name,
|
||||
'path' => $a->file_path,
|
||||
])->all(),
|
||||
];
|
||||
}
|
||||
|
||||
public function closeScheda(): void
|
||||
{
|
||||
$this->selectedSchedaId = null;
|
||||
$this->activeScheda = null;
|
||||
}
|
||||
|
||||
public function setSchedaTab(string $tab): void
|
||||
{
|
||||
$this->schedaTab = $tab;
|
||||
}
|
||||
|
||||
// Automated Workflow Actions
|
||||
public function eseguiChiusuraRiparato(): void
|
||||
{
|
||||
if (! $this->selectedSchedaId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scheda = AssistenzaTecnorepairScheda::query()->find($this->selectedSchedaId);
|
||||
if (! $scheda instanceof AssistenzaTecnorepairScheda) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(TecnoRepairArchiveService::class);
|
||||
$service->chiudiComeRiparato($scheda, $this->workflowNote ?: 'Intervento concluso e collaudato con successo.');
|
||||
|
||||
Notification::make()->title('Pratica segnata come RIPARATO con successo')->success()->send();
|
||||
$this->openScheda($this->selectedSchedaId);
|
||||
$this->refreshData();
|
||||
}
|
||||
|
||||
public function eseguiChiusuraRottamazione(): void
|
||||
{
|
||||
if (! $this->selectedSchedaId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scheda = AssistenzaTecnorepairScheda::query()->find($this->selectedSchedaId);
|
||||
if (! $scheda instanceof AssistenzaTecnorepairScheda) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(TecnoRepairArchiveService::class);
|
||||
$service->chiudiComeRottamato(
|
||||
$scheda,
|
||||
$this->workflowMotivoRottamazione ?: 'Apparato non riparabile convenientemente',
|
||||
$this->workflowComeRicambi
|
||||
);
|
||||
|
||||
$label = $this->workflowComeRicambi ? 'destinato a recupero ricambi' : 'contrassegnato per rottamazione';
|
||||
Notification::make()->title("Apparato {$label}")->warning()->send();
|
||||
$this->openScheda($this->selectedSchedaId);
|
||||
$this->refreshData();
|
||||
}
|
||||
|
||||
public function eseguiResoFornitoreRma(): void
|
||||
{
|
||||
if (! $this->selectedSchedaId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scheda = AssistenzaTecnorepairScheda::query()->find($this->selectedSchedaId);
|
||||
if (! $scheda instanceof AssistenzaTecnorepairScheda) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = app(TecnoRepairArchiveService::class);
|
||||
$service->rendiAFornitoreRma(
|
||||
$scheda,
|
||||
$this->workflowRmaCode,
|
||||
$this->workflowFornitoreResoId ?: 392,
|
||||
$this->workflowNote ?: 'Reso in garanzia al produttore / fornitore'
|
||||
);
|
||||
|
||||
Notification::make()->title("Pratica inviata in RMA/Garanzia (RMA: {$scheda->rma_code})")->info()->send();
|
||||
$this->openScheda($this->selectedSchedaId);
|
||||
$this->refreshData();
|
||||
}
|
||||
|
||||
public function sincronizzaMdb(): void
|
||||
{
|
||||
try {
|
||||
$service = app(TecnoRepairArchiveService::class);
|
||||
$stats = $service->importArchive(13, (int) $this->fornitoreId);
|
||||
$msg = sprintf(
|
||||
'Archivio MDB TecnoRepair sincronizzato: %d schede (%d create, %d agg.), %d ricambi, %d seriali.',
|
||||
$stats['schede_lette'],
|
||||
$stats['schede_create'],
|
||||
$stats['schede_aggiornate'],
|
||||
$stats['ricambi_catalogati'],
|
||||
$stats['seriali_allineati']
|
||||
);
|
||||
Notification::make()->title($msg)->success()->send();
|
||||
$this->refreshData();
|
||||
} catch (\Throwable $e) {
|
||||
Notification::make()->title('Errore sincronizzazione TecnoRepair: ' . $e->getMessage())->danger()->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function sincronizzaContabilita(): void
|
||||
{
|
||||
try {
|
||||
$service = app(ContabilitaMysqlSerialImportService::class);
|
||||
$stats = $service->importForSupplier('NCOMSRL');
|
||||
$msg = sprintf(
|
||||
'Contabilità MySQL sincronizzata: %d fatture lette, %d seriali aggiornati.',
|
||||
$stats['invoices_count'],
|
||||
$stats['serials_created'] + $stats['serials_updated']
|
||||
);
|
||||
Notification::make()->title($msg)->success()->send();
|
||||
$this->refreshData();
|
||||
} catch (\Throwable $e) {
|
||||
Notification::make()->title('Errore sincronizzazione Contabilità: ' . $e->getMessage())->danger()->send();
|
||||
}
|
||||
}
|
||||
|
||||
private function extractBrand(?string $model): string
|
||||
{
|
||||
$model = trim((string) $model);
|
||||
if ($model === '') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
$firstWord = strtoupper(explode(' ', $model)[0] ?? '');
|
||||
if (in_array($firstWord, ['HP', 'LENOVO', 'DELL', 'ASUS', 'ACER', 'APPLE', 'TOSHIBA', 'MSI', 'SAMSUNG', 'HANSUNG'], true)) {
|
||||
return $firstWord;
|
||||
}
|
||||
|
||||
return $firstWord ?: '-';
|
||||
}
|
||||
}
|
||||
|
|
@ -142,6 +142,34 @@ public function mount(): void
|
|||
if (($this->selectedProductId ?? 0) > 0) {
|
||||
$this->openProductDetail((int) $this->selectedProductId);
|
||||
}
|
||||
|
||||
if (request()->has('sync')) {
|
||||
$this->syncContabilitaProdotti();
|
||||
}
|
||||
}
|
||||
|
||||
public function syncContabilitaProdotti(): void
|
||||
{
|
||||
try {
|
||||
$service = app(\App\Services\Catalog\ContabilitaMysqlSerialImportService::class);
|
||||
$res = $service->importForSupplier('NCOMSRL');
|
||||
$msg = sprintf(
|
||||
'Contabilità sincronizzata: %d fatture, %d prodotti aggiornati, %d seriali (%d nuovi).',
|
||||
$res['invoices_count'],
|
||||
$res['products_created'],
|
||||
$res['serials_created'] + $res['serials_updated'],
|
||||
$res['serials_created']
|
||||
);
|
||||
\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 getPraticheUrl(): string
|
||||
{
|
||||
return PraticheTecnorepair::getUrl(['fornitore' => (int) ($this->fornitoreId ?? 0)], panel: 'admin-filament');
|
||||
}
|
||||
|
||||
public function updatedFornitoreId(): void
|
||||
|
|
|
|||
|
|
@ -90,6 +90,39 @@ public function mount(): void
|
|||
if ($this->selectedSerialId) {
|
||||
$this->openSerialDetail($this->selectedSerialId);
|
||||
}
|
||||
|
||||
if (request()->has('sync') || (in_array((int) $this->fornitoreId, [236, 392], true) && count($this->rows) === 0)) {
|
||||
try {
|
||||
app(\App\Services\Catalog\ContabilitaMysqlSerialImportService::class)->importForSupplier('NCOMSRL');
|
||||
$this->refreshRows();
|
||||
} catch (\Throwable) {
|
||||
// Silently fallback without blocking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function syncContabilitaSerials(): void
|
||||
{
|
||||
try {
|
||||
$service = app(\App\Services\Catalog\ContabilitaMysqlSerialImportService::class);
|
||||
$res = $service->importForSupplier('NCOMSRL');
|
||||
$msg = sprintf(
|
||||
'Contabilità aggiornata: %d fatture analizzate, %d seriali sincronizzati (%d nuovi), %d prodotti.',
|
||||
$res['invoices_count'],
|
||||
$res['serials_created'] + $res['serials_updated'],
|
||||
$res['serials_created'],
|
||||
$res['products_created']
|
||||
);
|
||||
\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 getPraticheUrl(): string
|
||||
{
|
||||
return PraticheTecnorepair::getUrl(['fornitore' => (int) ($this->fornitoreId ?? 0)], panel: 'admin-filament');
|
||||
}
|
||||
|
||||
/** @var array<int, string> */
|
||||
|
|
|
|||
|
|
@ -111,6 +111,17 @@ class FornitoreScheda extends Page
|
|||
|
||||
public ?string $lastGeneratedPassword = null;
|
||||
|
||||
// Nethome-exclusive integration properties
|
||||
public string $mysqlHost = '192.168.0.36';
|
||||
public string $mysqlPort = '3307';
|
||||
public string $mysqlDatabase = 'arc_nehr';
|
||||
public string $mysqlUsername = 'NETGESCON';
|
||||
public string $mysqlPassword = '';
|
||||
public string $mysqlSupplierCode = 'NCOMSRL';
|
||||
public string $tecnorepairMdbPath = '\\\\192.168.0.36\\CServerGO\\LunaSoftware_TecnoRepair\\Archivi\\TecnoRepairDB.mdb';
|
||||
public string $tecnorepairLocalPath = '/home/michele/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb';
|
||||
public bool $tecnorepairAutoSync = true;
|
||||
|
||||
public function cleanDisplayValue(?string $value, string $fallback = '-'): string
|
||||
{
|
||||
$v = trim((string) $value);
|
||||
|
|
@ -201,12 +212,133 @@ public function mount(int | string $record): void
|
|||
$this->refreshDipendentiRows();
|
||||
$this->refreshCatalogRows();
|
||||
|
||||
if ($this->isNethomeFornitore()) {
|
||||
$opConfig = (array) ($this->fornitore->operational_config ?? []);
|
||||
$dbCfg = (array) data_get($opConfig, 'accounting_db', []);
|
||||
$this->mysqlHost = (string) ($dbCfg['host'] ?? '192.168.0.36');
|
||||
$this->mysqlPort = (string) ($dbCfg['port'] ?? '3307');
|
||||
$this->mysqlDatabase = (string) ($dbCfg['database'] ?? 'arc_nehr');
|
||||
$this->mysqlUsername = (string) ($dbCfg['username'] ?? 'NETGESCON');
|
||||
$this->mysqlSupplierCode = (string) ($dbCfg['supplier_code'] ?? 'NCOMSRL');
|
||||
|
||||
$trCfg = (array) data_get($opConfig, 'tecnorepair', []);
|
||||
$this->tecnorepairMdbPath = (string) ($trCfg['mdb_path'] ?? \App\Services\Tecnorepair\TecnoRepairArchiveService::DEFAULT_WINDOWS_UNC_PATH);
|
||||
$this->tecnorepairLocalPath = (string) ($trCfg['local_fallback_path'] ?? \App\Services\Tecnorepair\TecnoRepairArchiveService::DEFAULT_LOCAL_PATH);
|
||||
$this->tecnorepairAutoSync = (bool) ($trCfg['auto_sync'] ?? true);
|
||||
}
|
||||
|
||||
$requestedTab = (string) request()->query('tab', 'profilo');
|
||||
if ($this->isValidSectionTab($requestedTab)) {
|
||||
$this->sectionTab = $requestedTab;
|
||||
}
|
||||
}
|
||||
|
||||
public function isNethomeFornitore(): bool
|
||||
{
|
||||
return (int) ($this->fornitore->id ?? 0) === 236
|
||||
|| ($this->fornitore->partita_iva ?? '') === '10055221005'
|
||||
|| Str::contains(strtoupper((string) ($this->fornitore->ragione_sociale ?? '')), 'NETHOME');
|
||||
}
|
||||
|
||||
public function saveNethomeIntegrations(): void
|
||||
{
|
||||
abort_unless($this->isNethomeFornitore(), 403, 'Azione consentita solo per il fornitore Nethome.');
|
||||
|
||||
$config = (array) ($this->fornitore->operational_config ?? []);
|
||||
$config['accounting_db'] = [
|
||||
'host' => trim($this->mysqlHost),
|
||||
'port' => trim($this->mysqlPort),
|
||||
'database' => trim($this->mysqlDatabase),
|
||||
'username' => trim($this->mysqlUsername),
|
||||
'password' => $this->mysqlPassword ?: ($config['accounting_db']['password'] ?? 'P4ssw0rd.96'),
|
||||
'supplier_code' => trim($this->mysqlSupplierCode),
|
||||
];
|
||||
|
||||
$config['tecnorepair'] = [
|
||||
'mdb_path' => trim($this->tecnorepairMdbPath),
|
||||
'local_fallback_path' => trim($this->tecnorepairLocalPath),
|
||||
'auto_sync' => (bool) $this->tecnorepairAutoSync,
|
||||
'last_sync' => $config['tecnorepair']['last_sync'] ?? null,
|
||||
];
|
||||
|
||||
$this->fornitore->operational_config = $config;
|
||||
$this->fornitore->save();
|
||||
|
||||
Notification::make()->title('Configurazioni Nethome salvate con successo')->success()->send();
|
||||
}
|
||||
|
||||
public function testMysqlConnection(): void
|
||||
{
|
||||
abort_unless($this->isNethomeFornitore(), 403, 'Azione consentita solo per il fornitore Nethome.');
|
||||
|
||||
try {
|
||||
$fp = @fsockopen(trim($this->mysqlHost), (int) $this->mysqlPort, $errno, $errstr, 3);
|
||||
if (! $fp) {
|
||||
throw new \RuntimeException("Porta non raggiungibile: {$errstr} ({$errno})");
|
||||
}
|
||||
fclose($fp);
|
||||
|
||||
$pdo = new \PDO(
|
||||
"mysql:host={$this->mysqlHost};port={$this->mysqlPort};dbname={$this->mysqlDatabase}",
|
||||
$this->mysqlUsername,
|
||||
$this->mysqlPassword ?: 'P4ssw0rd.96',
|
||||
[\PDO::ATTR_TIMEOUT => 5]
|
||||
);
|
||||
|
||||
$version = $pdo->query('SELECT VERSION()')->fetchColumn();
|
||||
Notification::make()->title("Connessione MySQL riuscita! Server: {$version}")->success()->send();
|
||||
} catch (\Throwable $e) {
|
||||
Notification::make()->title('Errore connessione MySQL: ' . $e->getMessage())->danger()->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function sincronizzaContabilitaNethome(): void
|
||||
{
|
||||
abort_unless($this->isNethomeFornitore(), 403, 'Azione consentita solo per il fornitore Nethome.');
|
||||
|
||||
try {
|
||||
$service = app(\App\Services\Catalog\ContabilitaMysqlSerialImportService::class);
|
||||
$res = $service->importForSupplier($this->mysqlSupplierCode ?: 'NCOMSRL');
|
||||
$msg = sprintf(
|
||||
'Contabilità sincronizzata: %d fatture, %d seriali (%d nuovi), %d prodotti.',
|
||||
$res['invoices_count'],
|
||||
$res['serials_created'] + $res['serials_updated'],
|
||||
$res['serials_created'],
|
||||
$res['products_created']
|
||||
);
|
||||
Notification::make()->title($msg)->success()->send();
|
||||
$this->hydrateBoxData(Auth::user());
|
||||
$this->refreshCatalogRows();
|
||||
} catch (\Throwable $e) {
|
||||
Notification::make()->title('Errore sincronizzazione: ' . $e->getMessage())->danger()->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function sincronizzaTecnorepairNethome(): void
|
||||
{
|
||||
abort_unless($this->isNethomeFornitore(), 403, 'Azione consentita solo per il fornitore Nethome.');
|
||||
|
||||
try {
|
||||
$service = app(\App\Services\Tecnorepair\TecnoRepairArchiveService::class);
|
||||
$stats = $service->importArchive(
|
||||
(int) ($this->fornitore->amministratore_id ?: 13),
|
||||
(int) $this->fornitore->id,
|
||||
$this->tecnorepairLocalPath ?: null
|
||||
);
|
||||
$msg = sprintf(
|
||||
'TecnoRepair sincronizzato: %d schede (%d create, %d agg.), %d ricambi catalogati.',
|
||||
$stats['schede_lette'],
|
||||
$stats['schede_create'],
|
||||
$stats['schede_aggiornate'],
|
||||
$stats['ricambi_catalogati']
|
||||
);
|
||||
Notification::make()->title($msg)->success()->send();
|
||||
$this->hydrateBoxData(Auth::user());
|
||||
} catch (\Throwable $e) {
|
||||
Notification::make()->title('Errore import TecnoRepair: ' . $e->getMessage())->danger()->send();
|
||||
}
|
||||
}
|
||||
|
||||
public function setSectionTab(string $tab): void
|
||||
{
|
||||
if ($this->isValidSectionTab($tab)) {
|
||||
|
|
|
|||
507
app/Services/Tecnorepair/TecnoRepairArchiveService.php
Normal file
507
app/Services/Tecnorepair/TecnoRepairArchiveService.php
Normal file
|
|
@ -0,0 +1,507 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
</select>
|
||||
</div>
|
||||
@endif
|
||||
<a href="/admin-filament/fornitore/pratiche?fornitore={{ $this->fornitoreId }}" class="inline-flex items-center rounded-md bg-sky-700 px-3 py-1.5 text-xs font-bold text-white hover:bg-sky-600 shadow-xs">💻 Pratiche TecnoRepair</a>
|
||||
<a href="{{ $this->getTicketsUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Ticket operativi</a>
|
||||
<a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Collaboratori</a>
|
||||
<a href="{{ $this->getRubricaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Rubrica clienti</a>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,638 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="space-y-5">
|
||||
{{-- TOP TOOLBAR: FORNITORE CONTEXT & QUICK SYNC BUTTONS --}}
|
||||
<div class="p-4 rounded-xl border border-slate-200 bg-white/90 shadow-sm backdrop-blur flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-sky-700 text-white flex items-center justify-center font-black text-lg shadow-sm">
|
||||
TR
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-base font-bold text-slate-900 leading-tight">TecnoRepair Pro 4.5 — Gestione Pratiche Riparazione</h2>
|
||||
<p class="text-xs text-slate-500">
|
||||
Fornitore operativo: <span class="font-semibold text-slate-800">{{ $this->fornitoreLabel ?? 'Nethome' }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@if(count($this->fornitoriOptions) > 1)
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="fornitoreSelect" class="text-xs font-semibold text-slate-600">Cambia fornitore:</label>
|
||||
<select
|
||||
id="fornitoreSelect"
|
||||
wire:model.live="fornitoreId"
|
||||
class="text-xs py-1.5 px-2.5 rounded-lg border border-slate-300 bg-white font-medium text-slate-700 shadow-xs focus:ring-2 focus:ring-sky-500"
|
||||
>
|
||||
@foreach($this->fornitoriOptions as $fid => $flabel)
|
||||
<option value="{{ $fid }}">{{ $flabel }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="sincronizzaMdb"
|
||||
wire:loading.attr="disabled"
|
||||
class="px-3 py-1.5 rounded-lg bg-indigo-700 hover:bg-indigo-600 text-white font-bold text-xs shadow-xs transition flex items-center gap-1.5"
|
||||
>
|
||||
<span wire:loading.remove wire:target="sincronizzaMdb">⚡ Sincronizza MDB TecnoRepair</span>
|
||||
<span wire:loading wire:target="sincronizzaMdb">⏳ Lettura MDB in corso...</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
wire:click="sincronizzaContabilita"
|
||||
wire:loading.attr="disabled"
|
||||
class="px-3 py-1.5 rounded-lg bg-emerald-700 hover:bg-emerald-600 text-white font-bold text-xs shadow-xs transition flex items-center gap-1.5"
|
||||
>
|
||||
<span wire:loading.remove wire:target="sincronizzaContabilita">🔄 Aggiorna da Contabilità</span>
|
||||
<span wire:loading wire:target="sincronizzaContabilita">⏳ Aggiornamento...</span>
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="/admin-filament/fornitore/seriali?fornitore={{ $this->fornitoreId }}"
|
||||
class="px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-800 border border-slate-300 font-bold text-xs shadow-xs transition"
|
||||
>
|
||||
🔍 Ricerca Seriali
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/admin-filament/fornitore/prodotti?fornitore={{ $this->fornitoreId }}"
|
||||
class="px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-800 border border-slate-300 font-bold text-xs shadow-xs transition"
|
||||
>
|
||||
📦 Catalogo & Ricambi
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- SUMMARY KPI CARDS --}}
|
||||
<div class="grid grid-cols-2 sm:grid-cols-5 gap-3">
|
||||
<div class="p-3.5 rounded-xl border border-slate-200 bg-white shadow-xs">
|
||||
<div class="text-xs font-semibold text-slate-500 uppercase tracking-wider">Totale Schede</div>
|
||||
<div class="mt-1 text-2xl font-black text-slate-900">{{ number_format($this->totals['totale'] ?? 0, 0, ',', '.') }}</div>
|
||||
</div>
|
||||
<div class="p-3.5 rounded-xl border border-sky-200 bg-sky-50 shadow-xs">
|
||||
<div class="text-xs font-semibold text-sky-700 uppercase tracking-wider">In Lavorazione</div>
|
||||
<div class="mt-1 text-2xl font-black text-sky-900">{{ number_format($this->totals['aperte'] ?? 0, 0, ',', '.') }}</div>
|
||||
</div>
|
||||
<div class="p-3.5 rounded-xl border border-emerald-200 bg-emerald-50 shadow-xs">
|
||||
<div class="text-xs font-semibold text-emerald-700 uppercase tracking-wider">Riparate</div>
|
||||
<div class="mt-1 text-2xl font-black text-emerald-900">{{ number_format($this->totals['riparate'] ?? 0, 0, ',', '.') }}</div>
|
||||
</div>
|
||||
<div class="p-3.5 rounded-xl border border-rose-200 bg-rose-50 shadow-xs">
|
||||
<div class="text-xs font-semibold text-rose-700 uppercase tracking-wider">Rottamate / Ricambi</div>
|
||||
<div class="mt-1 text-2xl font-black text-rose-900">{{ number_format($this->totals['rottamate'] ?? 0, 0, ',', '.') }}</div>
|
||||
</div>
|
||||
<div class="p-3.5 rounded-xl border border-amber-200 bg-amber-50 shadow-xs">
|
||||
<div class="text-xs font-semibold text-amber-700 uppercase tracking-wider">Reso Fornitore / RMA</div>
|
||||
<div class="mt-1 text-2xl font-black text-amber-900">{{ number_format($this->totals['rma_garanzia'] ?? 0, 0, ',', '.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FILTRI PER RICERCA SCHEDE (FEDELE AL DESKTOP TECNOREPAIR) --}}
|
||||
<div class="p-4 rounded-xl border border-slate-200 bg-white shadow-xs space-y-3">
|
||||
<div class="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||
<div class="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center gap-2">
|
||||
<span>🎛️ FILTRI per Ricerca Schede</span>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 font-normal">Standard TecnoRepair</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="resetFiltri"
|
||||
class="text-xs text-sky-700 hover:text-sky-900 font-bold underline"
|
||||
>
|
||||
Reset Tutti i Filtri
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label class="block text-[11px] font-semibold text-slate-600 mb-1">Data Ingresso (Dal / Al)</label>
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
type="date"
|
||||
wire:model.lazy="dataIngressoDal"
|
||||
class="w-full text-xs py-1.5 px-2 rounded-lg border border-slate-300 text-slate-800 focus:ring-1 focus:ring-sky-500"
|
||||
/>
|
||||
<span class="text-slate-400 text-xs">-</span>
|
||||
<input
|
||||
type="date"
|
||||
wire:model.lazy="dataIngressoAl"
|
||||
class="w-full text-xs py-1.5 px-2 rounded-lg border border-slate-300 text-slate-800 focus:ring-1 focus:ring-sky-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-semibold text-slate-600 mb-1">Matricola / IMEI / Seriale</label>
|
||||
<input
|
||||
type="text"
|
||||
wire:model.live.debounce.400ms="searchMatricola"
|
||||
placeholder="Es. 8CG9454YMN, PF508R8..."
|
||||
class="w-full text-xs py-1.5 px-2.5 rounded-lg border border-slate-300 text-slate-800 focus:ring-1 focus:ring-sky-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-semibold text-slate-600 mb-1">Num. Scheda</label>
|
||||
<input
|
||||
type="text"
|
||||
wire:model.live.debounce.400ms="searchNumScheda"
|
||||
placeholder="Es. 1839, 1843..."
|
||||
class="w-full text-xs py-1.5 px-2.5 rounded-lg border border-slate-300 text-slate-800 focus:ring-1 focus:ring-sky-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-[11px] font-semibold text-slate-600 mb-1">Stato Riparazione</label>
|
||||
<select
|
||||
wire:model.live="filterStato"
|
||||
class="w-full text-xs py-1.5 px-2.5 rounded-lg border border-slate-300 text-slate-800 focus:ring-1 focus:ring-sky-500 font-medium"
|
||||
>
|
||||
<option value="0">0 - TUTTI - TUTTI</option>
|
||||
<option value="4">4 - RIPARATO</option>
|
||||
<option value="21">21 - INGRESSO ACCETTAZIONE</option>
|
||||
<option value="2">2 - DISPOSITIVO IN RIPARAZIONE</option>
|
||||
<option value="5">5 - ATTESA RICAMBIO</option>
|
||||
<option value="6">6 - INVIATO NUOVO PREVENTIVO</option>
|
||||
<option value="10">10 - DISPOSITIVO IRRIPARABILE</option>
|
||||
<option value="23">23 - PC DA UTILIZZARE COME RICAMBI</option>
|
||||
<option value="33">33 - MANDATO IN GARANZIA / RMA</option>
|
||||
<option value="31">31 - ACQUISTO RICONDIZIONATO</option>
|
||||
<option value="37">37 - SMALTITO PER ABBANDONO</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 pt-1 border-t border-slate-100">
|
||||
<div>
|
||||
<label class="block text-[11px] font-semibold text-slate-600 mb-1">Cliente / Committente</label>
|
||||
<input
|
||||
type="text"
|
||||
wire:model.live.debounce.400ms="searchCliente"
|
||||
placeholder="Cerca nominativo cliente..."
|
||||
class="w-full text-xs py-1.5 px-2.5 rounded-lg border border-slate-300 text-slate-800 focus:ring-1 focus:ring-sky-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 pt-4">
|
||||
<label class="inline-flex items-center gap-1.5 text-xs text-slate-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model.live="escludiRiconsegnati"
|
||||
class="rounded border-slate-300 text-sky-600 focus:ring-sky-500"
|
||||
/>
|
||||
<span>Escludi Apparecchi Riconsegnati</span>
|
||||
</label>
|
||||
|
||||
<label class="inline-flex items-center gap-1.5 text-xs text-slate-700 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
wire:model.live="soloRiconsegnati"
|
||||
class="rounded border-slate-300 text-sky-600 focus:ring-sky-500"
|
||||
/>
|
||||
<span>Solo Riconsegnati</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2 pt-3">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="refreshData"
|
||||
class="px-4 py-1.5 rounded-lg bg-sky-700 hover:bg-sky-600 text-white font-bold text-xs shadow-xs transition"
|
||||
>
|
||||
🔍 Applica Filtri
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- GRIGLIA ELENCO SCHEDE (COLORAZIONE FEDELE A TECNOREPAIR) --}}
|
||||
<div class="rounded-xl border border-slate-200 bg-white shadow-xs overflow-hidden">
|
||||
<div class="p-3 bg-slate-50 border-b border-slate-200 flex items-center justify-between text-xs font-bold text-slate-700">
|
||||
<div>
|
||||
Schede Elencate: <span class="text-sky-700">{{ count($this->rows) }}</span>
|
||||
<span class="text-slate-400 font-normal">(visualizzate prime 120 per reattività)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-[11px] font-normal">
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-emerald-500 inline-block"></span> Riparato</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-rose-500 inline-block"></span> Irreparabile / Rottamato</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-sky-500 inline-block"></span> Accettazione</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-amber-500 inline-block"></span> Attesa / Laboratorio</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto max-h-[580px]">
|
||||
<table class="w-full text-left text-xs border-collapse font-sans">
|
||||
<thead class="sticky top-0 bg-slate-100 text-slate-700 font-bold border-b border-slate-200 text-[11px] uppercase tracking-wider z-10">
|
||||
<tr>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-14 text-center">Num</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-24">Ingresso</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-44">Cliente</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-28">Telefono</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 min-w-[200px]">Difetto Segnalato</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-20">Marca</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-32">Modello</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-36">Seriale / IMEI</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-28">Cod. Prod</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-16 text-center">Ricons.</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-40">Stato Riparazione</th>
|
||||
<th class="py-2.5 px-2 border-r border-slate-200 w-28">Operatore</th>
|
||||
<th class="py-2.5 px-2 text-center w-24">Azione</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@forelse($this->rows as $r)
|
||||
@php
|
||||
$rowBg = match($r['row_color']) {
|
||||
'green' => 'bg-emerald-50/80 hover:bg-emerald-100/90 text-emerald-950 font-medium',
|
||||
'red' => 'bg-rose-50/80 hover:bg-rose-100/90 text-rose-950 font-medium',
|
||||
'blue' => 'bg-sky-50/80 hover:bg-sky-100/90 text-sky-950',
|
||||
'amber' => 'bg-amber-50/80 hover:bg-amber-100/90 text-amber-950',
|
||||
default => 'bg-white hover:bg-slate-50 text-slate-800',
|
||||
};
|
||||
$borderLeft = match($r['row_color']) {
|
||||
'green' => 'border-l-4 border-l-emerald-600',
|
||||
'red' => 'border-l-4 border-l-rose-600',
|
||||
'blue' => 'border-l-4 border-l-sky-600',
|
||||
'amber' => 'border-l-4 border-l-amber-600',
|
||||
default => 'border-l-4 border-l-transparent',
|
||||
};
|
||||
@endphp
|
||||
<tr
|
||||
class="{{ $rowBg }} {{ $borderLeft }} transition cursor-pointer"
|
||||
wire:click="openScheda({{ $r['id'] }})"
|
||||
>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 font-black text-center text-slate-900">{{ $r['numero_scheda'] }}</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60">{{ $r['ingresso'] }}</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 font-bold truncate max-w-[176px]" title="{{ $r['cliente'] }}">{{ $r['cliente'] }}</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 font-mono text-[11px]">{{ $r['telefono'] }}</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 truncate max-w-[260px]" title="{{ $r['difetto'] }}">{{ $r['difetto'] }}</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 font-semibold uppercase">{{ $r['marca'] }}</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 truncate max-w-[128px]" title="{{ $r['modello'] }}">{{ $r['modello'] }}</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 font-mono text-[11px] font-bold text-sky-800">{{ $r['seriale'] }}</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 font-mono text-[11px] text-slate-600">{{ $r['cod_prodotto'] }}</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 text-center">
|
||||
@if($r['riconsegnato'])
|
||||
<span class="inline-block px-1.5 py-0.5 rounded bg-emerald-600 text-white font-bold text-[10px]">SI</span>
|
||||
@else
|
||||
<span class="text-slate-400 font-normal">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 font-bold text-[11px]">
|
||||
{{ $r['stato_rip'] }}
|
||||
</td>
|
||||
<td class="py-2 px-2 border-r border-slate-200/60 text-[11px] truncate max-w-[112px]">{{ $r['operatore'] }}</td>
|
||||
<td class="py-2 px-2 text-center" onclick="event.stopPropagation()">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="openScheda({{ $r['id'] }})"
|
||||
class="px-2 py-1 rounded bg-sky-700 hover:bg-sky-600 text-white font-bold text-[11px] shadow-xs"
|
||||
>
|
||||
Apri
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="13" class="py-8 text-center text-slate-500 bg-white">
|
||||
Nessuna scheda trovata con i filtri correnti. Prova a cliccare "Reset Tutti i Filtri" o "Sincronizza MDB".
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- SCHEDA APPARECCHIO MODAL / DETTAGLIO INTERATTIVO (7 TABS TECNOREPAIR) --}}
|
||||
@if($this->selectedSchedaId && $this->activeScheda)
|
||||
<div class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4">
|
||||
<div class="relative w-full max-w-6xl rounded-2xl bg-white shadow-2xl border border-slate-300 overflow-hidden flex flex-col max-h-[92vh]">
|
||||
{{-- MODAL HEADER --}}
|
||||
<div class="p-4 bg-gradient-to-r from-slate-900 via-sky-950 to-slate-900 text-white flex items-center justify-between border-b border-sky-800">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="px-2.5 py-1 rounded-md bg-sky-500 text-slate-950 font-black text-sm">
|
||||
SCHEDA #{{ $this->activeScheda['numero_scheda'] }}
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="text-base font-bold text-white">
|
||||
{{ $this->activeScheda['marca'] }} {{ $this->activeScheda['modello'] }}
|
||||
<span class="text-sky-300 font-mono text-xs">({{ $this->activeScheda['seriale'] }})</span>
|
||||
</h3>
|
||||
<p class="text-xs text-sky-200">
|
||||
Cliente: <span class="font-bold text-white">{{ $this->activeScheda['cliente_nome'] }}</span> — Ingresso: {{ $this->activeScheda['data_ingresso'] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-3 py-1 rounded-full text-xs font-bold border border-white/20 bg-white/10 text-white">
|
||||
{{ $this->activeScheda['stato_rip'] }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="closeScheda"
|
||||
class="w-8 h-8 rounded-lg bg-white/10 hover:bg-white/20 text-white flex items-center justify-center font-bold text-sm transition"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- TAB BAR --}}
|
||||
<div class="bg-slate-100 border-b border-slate-200 px-4 pt-2 flex flex-wrap gap-1 text-xs font-bold">
|
||||
@php
|
||||
$tabsMap = [
|
||||
'entrata' => '1. Apparecchio in Entrata',
|
||||
'riparazione' => '2. Riparazione & Flussi ⚡',
|
||||
'ricambi' => '3. Ricambi Utilizzati',
|
||||
'preventivo' => '4. Preventivo - Costi - DDT',
|
||||
'annotazioni' => '5. Annotazioni & Cortesia',
|
||||
'comunicazioni' => '6. Comunicazioni',
|
||||
'cq' => '7. Controllo Qualità (C.Q.)',
|
||||
];
|
||||
@endphp
|
||||
@foreach($tabsMap as $tKey => $tLabel)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="setSchedaTab('{{ $tKey }}')"
|
||||
class="px-3 py-2 rounded-t-lg transition border-t border-x {{ $this->schedaTab === $tKey ? 'bg-white text-sky-800 border-slate-300 -mb-px font-black shadow-xs' : 'bg-slate-200/70 hover:bg-slate-200 text-slate-600 border-transparent' }}"
|
||||
>
|
||||
{{ $tLabel }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- TAB CONTENTS --}}
|
||||
<div class="p-5 overflow-y-auto flex-1 space-y-4">
|
||||
{{-- TAB 1: APPARECCHIO IN ENTRATA --}}
|
||||
@if($this->schedaTab === 'entrata')
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="p-4 rounded-xl border border-slate-200 bg-slate-50 space-y-2">
|
||||
<div class="text-xs font-bold text-slate-700 uppercase tracking-wider border-b border-slate-200 pb-1">👤 Dati Cliente</div>
|
||||
<div class="text-sm font-bold text-slate-900">{{ $this->activeScheda['cliente_nome'] }}</div>
|
||||
<div class="text-xs text-slate-600"><strong>Tel:</strong> {{ $this->activeScheda['cliente_telefono'] }}</div>
|
||||
<div class="text-xs text-slate-600"><strong>Email:</strong> {{ $this->activeScheda['cliente_email'] }}</div>
|
||||
<div class="text-xs text-slate-600"><strong>Indirizzo:</strong> {{ $this->activeScheda['cliente_indirizzo'] }}, {{ $this->activeScheda['cliente_citta'] }}</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 rounded-xl border border-slate-200 bg-slate-50 space-y-2">
|
||||
<div class="text-xs font-bold text-slate-700 uppercase tracking-wider border-b border-slate-200 pb-1">💻 Dati Apparecchio</div>
|
||||
<div class="text-xs text-slate-600"><strong>Tipo:</strong> {{ $this->activeScheda['tipo_apparecchio'] }}</div>
|
||||
<div class="text-xs text-slate-600"><strong>Marca:</strong> {{ $this->activeScheda['marca'] }}</div>
|
||||
<div class="text-xs text-slate-600"><strong>Modello:</strong> {{ $this->activeScheda['modello'] }}</div>
|
||||
<div class="text-xs text-slate-600"><strong>Codice Prodotto:</strong> {{ $this->activeScheda['codice_prodotto'] }}</div>
|
||||
<div class="text-xs text-sky-800 font-mono font-bold"><strong>Seriale 1:</strong> {{ $this->activeScheda['seriale'] }}</div>
|
||||
@if($this->activeScheda['seriale_2'] !== '-')
|
||||
<div class="text-xs text-slate-600 font-mono"><strong>Seriale 2:</strong> {{ $this->activeScheda['seriale_2'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="p-4 rounded-xl border border-slate-200 bg-slate-50 space-y-2">
|
||||
<div class="text-xs font-bold text-slate-700 uppercase tracking-wider border-b border-slate-200 pb-1">🔐 Sicurezza & Accesso</div>
|
||||
<div class="text-xs text-slate-600"><strong>Codice PIN:</strong> <span class="font-mono font-bold text-slate-900">{{ $this->activeScheda['codice_pin'] }}</span></div>
|
||||
<div class="text-xs text-slate-600"><strong>Codice Sblocco:</strong> <span class="font-mono font-bold text-slate-900">{{ $this->activeScheda['codice_sblocco'] }}</span></div>
|
||||
<div class="text-xs text-slate-600"><strong>Tecnico Assegnato:</strong> {{ $this->activeScheda['tecnico'] }}</div>
|
||||
<div class="text-xs text-slate-600"><strong>Accessori Consegnati:</strong> {{ $this->activeScheda['accessori'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 rounded-xl border border-amber-200 bg-amber-50 space-y-1.5">
|
||||
<div class="text-xs font-bold text-amber-900 uppercase tracking-wider">⚠️ Difetto Segnalato dal Cliente</div>
|
||||
<p class="text-sm font-medium text-amber-950 whitespace-pre-line">{{ $this->activeScheda['difetto'] }}</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 rounded-xl border border-slate-200 bg-white space-y-1.5">
|
||||
<div class="text-xs font-bold text-slate-700 uppercase tracking-wider">🔍 Stato Generale Apparecchio</div>
|
||||
<p class="text-xs text-slate-800 whitespace-pre-line">{{ $this->activeScheda['stato_generale'] }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 2: RIPARAZIONE & FLUSSI AUTOMATIZZATI --}}
|
||||
@if($this->schedaTab === 'riparazione')
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 rounded-xl border border-indigo-200 bg-indigo-50/60 space-y-3">
|
||||
<div class="text-xs font-black text-indigo-900 uppercase tracking-wider flex items-center gap-1.5">
|
||||
<span>⚡ FLUSSI AUTOMATIZZATI DI CHIUSURA PRATICA</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{{-- 1. FLUSSO RIPARATO --}}
|
||||
<div class="p-3.5 rounded-xl border border-emerald-300 bg-white shadow-xs space-y-2.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center font-bold text-xs">1</span>
|
||||
<h4 class="font-bold text-xs text-emerald-900">Apparato Riparato</h4>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-600">
|
||||
Imposta stato a <strong>RIPARATO (4)</strong>, data chiusura a oggi e prepara per riconsegna cliente.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
wire:model="workflowNote"
|
||||
placeholder="Nota chiusura o test..."
|
||||
class="w-full text-xs py-1 px-2 rounded border border-slate-300 text-slate-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="eseguiChiusuraRiparato"
|
||||
class="w-full py-2 px-3 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs shadow-xs transition"
|
||||
>
|
||||
✅ Segna come Riparato
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- 2. FLUSSO ROTTAMAZIONE / RICAMBI --}}
|
||||
<div class="p-3.5 rounded-xl border border-rose-300 bg-white shadow-xs space-y-2.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-rose-100 text-rose-700 flex items-center justify-center font-bold text-xs">2</span>
|
||||
<h4 class="font-bold text-xs text-rose-900">Rottamazione / Ricambi</h4>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-600">
|
||||
Imposta stato a <strong>IRRIPARABILE</strong> o destina a pezzi di ricambio interni.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
wire:model="workflowMotivoRottamazione"
|
||||
placeholder="Motivo rottamazione..."
|
||||
class="w-full text-xs py-1 px-2 rounded border border-slate-300 text-slate-800"
|
||||
/>
|
||||
<label class="inline-flex items-center gap-1.5 text-[11px] text-slate-700 cursor-pointer">
|
||||
<input type="checkbox" wire:model="workflowComeRicambi" class="rounded border-slate-300 text-rose-600" />
|
||||
<span>Destina a recupero ricambi (PZ-RICAM)</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="eseguiChiusuraRottamazione"
|
||||
class="w-full py-2 px-3 rounded-lg bg-rose-600 hover:bg-rose-500 text-white font-bold text-xs shadow-xs transition"
|
||||
>
|
||||
♻️ Esegui Rottamazione
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- 3. FLUSSO RESO FORNITORE (RMA) --}}
|
||||
<div class="p-3.5 rounded-xl border border-amber-300 bg-white shadow-xs space-y-2.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-6 h-6 rounded-full bg-amber-100 text-amber-700 flex items-center justify-center font-bold text-xs">3</span>
|
||||
<h4 class="font-bold text-xs text-amber-900">Reso Fornitore (RMA)</h4>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-600">
|
||||
Invia apparato in garanzia/RMA a fornitore (es. NCOM SRL) con tracking del seriale.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
wire:model="workflowRmaCode"
|
||||
placeholder="Codice RMA..."
|
||||
class="w-full text-xs py-1 px-2 rounded border border-slate-300 text-slate-800 font-mono"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="eseguiResoFornitoreRma"
|
||||
class="w-full py-2 px-3 rounded-lg bg-amber-600 hover:bg-amber-500 text-white font-bold text-xs shadow-xs transition"
|
||||
>
|
||||
📦 Esegui Reso Fornitore RMA
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 rounded-xl border border-slate-200 bg-white space-y-2">
|
||||
<div class="text-xs font-bold text-slate-700 uppercase tracking-wider">🛠️ Descrizione Lavorazione Effettuata</div>
|
||||
<p class="text-xs text-slate-800 whitespace-pre-line leading-relaxed">{{ $this->activeScheda['descrizione_lavorazione'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 3: RICAMBI UTILIZZATI --}}
|
||||
@if($this->schedaTab === 'ricambi')
|
||||
<div class="space-y-3">
|
||||
<div class="text-xs font-bold text-slate-700 uppercase tracking-wider">📦 Componenti & Ricambi Collegati alla Scheda</div>
|
||||
@if(!empty($this->activeScheda['ricambi']))
|
||||
<div class="rounded-lg border border-slate-200 overflow-hidden">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-slate-100 font-bold text-slate-700 border-b border-slate-200">
|
||||
<tr>
|
||||
<th class="py-2 px-3">Codice Ricambio</th>
|
||||
<th class="py-2 px-3">Descrizione</th>
|
||||
<th class="py-2 px-3 text-center">ID Riferimento</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@foreach($this->activeScheda['ricambi'] as $item)
|
||||
<tr>
|
||||
<td class="py-2 px-3 font-mono font-bold text-sky-800">{{ $item['CodiceRicambio'] ?? '-' }}</td>
|
||||
<td class="py-2 px-3">{{ $item['Descrizione'] ?? 'Ricambio collegato' }}</td>
|
||||
<td class="py-2 px-3 text-center font-mono text-slate-500">{{ $item['ID'] ?? '-' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="p-6 text-center text-slate-500 bg-slate-50 rounded-xl border border-dashed border-slate-200">
|
||||
Nessun ricambio specifico registrato per questa pratica. Puoi cercarli nel <a href="/admin-filament/fornitore/prodotti" class="text-sky-700 font-bold underline">Catalogo Ricambi</a>.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 4: PREVENTIVO - COSTI - DDT --}}
|
||||
@if($this->schedaTab === 'preventivo')
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="p-3.5 rounded-xl border border-slate-200 bg-slate-50">
|
||||
<div class="text-xs font-semibold text-slate-500">Costo Subito / Sostenuto</div>
|
||||
<div class="mt-1 text-xl font-bold text-slate-900">{{ number_format($this->activeScheda['costo_subito'], 2, ',', '.') }} €</div>
|
||||
</div>
|
||||
<div class="p-3.5 rounded-xl border border-slate-200 bg-slate-50">
|
||||
<div class="text-xs font-semibold text-slate-500">Costo Addebitato Cliente</div>
|
||||
<div class="mt-1 text-xl font-bold text-slate-900">{{ number_format($this->activeScheda['costo_addebitato'], 2, ',', '.') }} €</div>
|
||||
</div>
|
||||
<div class="p-3.5 rounded-xl border border-slate-200 bg-slate-50">
|
||||
<div class="text-xs font-semibold text-slate-500">Acconto Ricevuto</div>
|
||||
<div class="mt-1 text-xl font-bold text-emerald-700">{{ number_format($this->activeScheda['acconto'], 2, ',', '.') }} €</div>
|
||||
</div>
|
||||
<div class="p-3.5 rounded-xl border border-slate-200 bg-slate-50">
|
||||
<div class="text-xs font-semibold text-slate-500">Preventivo Iniziale</div>
|
||||
<div class="mt-1 text-xl font-bold text-sky-800">{{ number_format($this->activeScheda['preventivo'], 2, ',', '.') }} €</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 5: ANNOTAZIONI & ALLEGATI --}}
|
||||
@if($this->schedaTab === 'annotazioni')
|
||||
<div class="space-y-4">
|
||||
<div class="text-xs font-bold text-slate-700 uppercase tracking-wider">📎 File Allegati & Fotografie</div>
|
||||
@if(!empty($this->activeScheda['allegati']))
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
@foreach($this->activeScheda['allegati'] as $all)
|
||||
<div class="p-3 rounded-lg border border-slate-200 bg-slate-50 flex items-center justify-between gap-2">
|
||||
<span class="text-xs font-medium text-slate-800 truncate" title="{{ $all['nome'] }}">{{ $all['nome'] }}</span>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded bg-slate-200 text-slate-700 font-mono">allegato</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<p class="text-xs text-slate-500 italic">Nessun file o foto allegata a questa pratica.</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 6: COMUNICAZIONI --}}
|
||||
@if($this->schedaTab === 'comunicazioni')
|
||||
<div class="p-4 rounded-xl border border-slate-200 bg-slate-50 space-y-2">
|
||||
<div class="text-xs font-bold text-slate-700 uppercase tracking-wider">💬 Storico Comunicazioni con il Cliente</div>
|
||||
<div class="p-3 bg-white rounded-lg border border-slate-200 font-mono text-xs text-slate-800 whitespace-pre-line leading-relaxed">
|
||||
{{ $this->activeScheda['comunicazioni'] ?: 'Nessuna comunicazione registrata.' }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 7: C.Q. CONTROLLO QUALITA --}}
|
||||
@if($this->schedaTab === 'cq')
|
||||
<div class="p-4 rounded-xl border border-slate-200 bg-slate-50 space-y-3">
|
||||
<div class="text-xs font-bold text-slate-700 uppercase tracking-wider">✅ Checklist Standard Controllo Qualità</div>
|
||||
<div class="space-y-2 text-xs text-slate-800">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked disabled class="rounded border-slate-300 text-emerald-600" />
|
||||
<span>Controllo alimentazione, tensione e batteria</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked disabled class="rounded border-slate-300 text-emerald-600" />
|
||||
<span>Verifica webcam, microfono e uscite audio</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked disabled class="rounded border-slate-300 text-emerald-600" />
|
||||
<span>Credenziali e password di sistema collaudate</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked disabled class="rounded border-slate-300 text-emerald-600" />
|
||||
<span>Installazione software e pacchetto applicativo</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked disabled class="rounded border-slate-300 text-emerald-600" />
|
||||
<span>Connettività Wi-Fi / Ethernet e porte I/O</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- MODAL FOOTER --}}
|
||||
<div class="p-3 bg-slate-100 border-t border-slate-200 flex items-center justify-between">
|
||||
<span class="text-xs text-slate-500">
|
||||
Pratica gestita da: <strong class="text-slate-800">{{ $this->activeScheda['tecnico'] }}</strong>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="closeScheda"
|
||||
class="px-4 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-white font-bold text-xs transition"
|
||||
>
|
||||
Chiudi Scheda
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
@ -26,6 +26,16 @@
|
|||
@if($this->getFornitoreSchedaUrl())
|
||||
<a href="{{ $this->getFornitoreSchedaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Scheda fornitore</a>
|
||||
@endif
|
||||
<button
|
||||
type="button"
|
||||
wire:click="syncContabilitaProdotti"
|
||||
wire:loading.attr="disabled"
|
||||
class="inline-flex items-center gap-1.5 rounded-md bg-emerald-700 hover:bg-emerald-600 px-3 py-1.5 text-xs font-bold text-white shadow-xs transition"
|
||||
>
|
||||
<span wire:loading.remove wire:target="syncContabilitaProdotti">🔄 Sincronizza Fatture</span>
|
||||
<span wire:loading wire:target="syncContabilitaProdotti">⏳ Sincronizzazione...</span>
|
||||
</button>
|
||||
<a href="{{ $this->getPraticheUrl() }}" class="inline-flex items-center rounded-md bg-sky-700 px-3 py-1.5 text-xs font-bold text-white hover:bg-sky-600 shadow-xs">💻 Pratiche TecnoRepair</a>
|
||||
<a href="{{ $this->getContabilitaFornitoriUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Fornitori in Contabilità</a>
|
||||
<a href="{{ $this->getLavorazioniUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Lavorazioni</a>
|
||||
<a href="{{ $this->getSerialiUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Ricerca seriali</a>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,16 @@
|
|||
@if($this->getFornitoreSchedaUrl())
|
||||
<a href="{{ $this->getFornitoreSchedaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Scheda fornitore</a>
|
||||
@endif
|
||||
<button
|
||||
type="button"
|
||||
wire:click="syncContabilitaSerials"
|
||||
wire:loading.attr="disabled"
|
||||
class="inline-flex items-center gap-1.5 rounded-md bg-emerald-700 hover:bg-emerald-600 px-3 py-1.5 text-xs font-bold text-white shadow-xs transition"
|
||||
>
|
||||
<span wire:loading.remove wire:target="syncContabilitaSerials">🔄 Sincronizza Fatture</span>
|
||||
<span wire:loading wire:target="syncContabilitaSerials">⏳ Sincronizzazione...</span>
|
||||
</button>
|
||||
<a href="{{ $this->getPraticheUrl() }}" class="inline-flex items-center rounded-md bg-sky-700 px-3 py-1.5 text-xs font-bold text-white hover:bg-sky-600 shadow-xs">💻 Pratiche TecnoRepair</a>
|
||||
<a href="{{ $this->getProdottiUrl() }}" class="inline-flex items-center rounded-md bg-slate-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-slate-800">Hub prodotti</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
</select>
|
||||
</div>
|
||||
@endif
|
||||
<a href="/admin-filament/fornitore/pratiche?fornitore={{ $this->fornitoreId }}" class="inline-flex items-center rounded-md bg-sky-700 px-3 py-1.5 text-xs font-bold text-white hover:bg-sky-600 shadow-xs">💻 Pratiche TecnoRepair</a>
|
||||
<a href="{{ \App\Filament\Pages\Fornitore\LavorazioniOperative::getUrl(['fornitore' => $this->fornitoreId], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Lavorazioni</a>
|
||||
<a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Collaboratori</a>
|
||||
<a href="{{ $this->getImpostazioniUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Impostazioni</a>
|
||||
|
|
|
|||
|
|
@ -470,6 +470,129 @@ class="w-full rounded-lg border-gray-300 text-sm"
|
|||
$acquaScan = (bool) ($acqua['scan_enabled'] ?? false);
|
||||
@endphp
|
||||
|
||||
@if($this->isNethomeFornitore())
|
||||
{{-- CARD INTEGRAZIONI ESCLUSIVE NETHOME (CONTABILITA & TECNOREPAIR) --}}
|
||||
<div class="rounded-xl border-2 border-indigo-300 bg-gradient-to-br from-indigo-50/50 via-white to-sky-50/40 p-5 shadow-sm space-y-4" x-show="tab === 'assistenza' || tab === 'contabilita'" x-cloak>
|
||||
<div class="flex items-center justify-between border-b border-indigo-100 pb-3">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="w-8 h-8 rounded-lg bg-indigo-700 text-white flex items-center justify-center font-bold text-sm shadow-xs">⚙️</span>
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-indigo-950">Integrazioni & Connessioni Esterne Nethome</h3>
|
||||
<p class="text-[11px] text-indigo-700 font-medium">Archivi contabili e pratiche riservati solo a questo fornitore (invisibili agli altri fornitori)</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider bg-indigo-100 text-indigo-800 border border-indigo-200">
|
||||
Riservato Nethome
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- SEZIONE 1: CONTABILITA MYSQL --}}
|
||||
<div class="p-3.5 rounded-lg border border-slate-200 bg-white space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold text-slate-800 flex items-center gap-1.5">
|
||||
<span>🗄️ Database Contabilità Gestionale MySQL (Target Cross)</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="testMysqlConnection"
|
||||
class="px-2.5 py-1 rounded bg-slate-100 hover:bg-slate-200 text-slate-700 border border-slate-300 font-bold text-[11px] shadow-xs transition"
|
||||
>
|
||||
🔍 Verifica Connessione
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 text-xs">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 uppercase">Host Server</label>
|
||||
<input type="text" wire:model.defer="mysqlHost" class="w-full text-xs py-1 px-2 rounded border border-slate-300 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 uppercase">Porta</label>
|
||||
<input type="text" wire:model.defer="mysqlPort" class="w-full text-xs py-1 px-2 rounded border border-slate-300 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 uppercase">Database</label>
|
||||
<input type="text" wire:model.defer="mysqlDatabase" class="w-full text-xs py-1 px-2 rounded border border-slate-300 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 uppercase">Username</label>
|
||||
<input type="text" wire:model.defer="mysqlUsername" class="w-full text-xs py-1 px-2 rounded border border-slate-300 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 uppercase">Password</label>
|
||||
<input type="password" wire:model.defer="mysqlPassword" placeholder="••••••••" class="w-full text-xs py-1 px-2 rounded border border-slate-300 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 uppercase">Cod. Fornitore Contabilità</label>
|
||||
<input type="text" wire:model.defer="mysqlSupplierCode" class="w-full text-xs py-1 px-2 rounded border border-slate-300 font-mono font-bold text-sky-800" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-2 flex flex-wrap items-center justify-between gap-2 border-t border-slate-100">
|
||||
<span class="text-[11px] text-slate-500">Sincronizza fatture elettroniche e seriali registrati nel DB contabile esterno.</span>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="sincronizzaContabilitaNethome"
|
||||
wire:loading.attr="disabled"
|
||||
class="px-3 py-1.5 rounded-lg bg-emerald-700 hover:bg-emerald-600 text-white font-bold text-xs shadow-xs transition flex items-center gap-1.5"
|
||||
>
|
||||
<span wire:loading.remove wire:target="sincronizzaContabilitaNethome">🔄 Sincronizza Fatture & Seriali</span>
|
||||
<span wire:loading wire:target="sincronizzaContabilitaNethome">⏳ Sincronizzazione in corso...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- SEZIONE 2: ARCHIVIO TECNOREPAIR MDB --}}
|
||||
<div class="p-3.5 rounded-lg border border-slate-200 bg-white space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold text-slate-800 flex items-center gap-1.5">
|
||||
<span>🛠️ Archivio TecnoRepair (MDB Riparazioni & Ricambi)</span>
|
||||
</span>
|
||||
<a
|
||||
href="/admin-filament/fornitore/pratiche?fornitore={{ $fornitore->id }}"
|
||||
class="px-2.5 py-1 rounded bg-sky-700 hover:bg-sky-600 text-white font-bold text-[11px] shadow-xs transition"
|
||||
>
|
||||
💻 Apri Pratiche Web
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 text-xs">
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 uppercase">Percorso Rete Windows (UNC)</label>
|
||||
<input type="text" wire:model.defer="tecnorepairMdbPath" class="w-full text-xs py-1 px-2 rounded border border-slate-300 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[10px] font-bold text-slate-500 uppercase">Percorso Replica Locale Linux (Fallback)</label>
|
||||
<input type="text" wire:model.defer="tecnorepairLocalPath" class="w-full text-xs py-1 px-2 rounded border border-slate-300 font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-2 flex flex-wrap items-center justify-between gap-2 border-t border-slate-100">
|
||||
<span class="text-[11px] text-slate-500">Importa schede riparazione, anagrafica clienti e catalogo ricambi da TecnoRepairDB.mdb.</span>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="sincronizzaTecnorepairNethome"
|
||||
wire:loading.attr="disabled"
|
||||
class="px-3 py-1.5 rounded-lg bg-indigo-700 hover:bg-indigo-600 text-white font-bold text-xs shadow-xs transition flex items-center gap-1.5"
|
||||
>
|
||||
<span wire:loading.remove wire:target="sincronizzaTecnorepairNethome">⚡ Sincronizza Archivio MDB</span>
|
||||
<span wire:loading wire:target="sincronizzaTecnorepairNethome">⏳ Importazione...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-2 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="saveNethomeIntegrations"
|
||||
class="px-4 py-2 rounded-lg bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs shadow-xs transition"
|
||||
>
|
||||
💾 Salva Impostazioni Nethome
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="rounded-lg border p-4" x-show="tab === 'assistenza'" x-cloak>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,65 +1,71 @@
|
|||
# CURRENT-205
|
||||
|
||||
TASK_ID: task-fornitore-mysql-seriali-rma
|
||||
TASK_ID: task-tecnorepair-fornitore-sync-workflows
|
||||
MACHINE: .205
|
||||
STATO: completato
|
||||
|
||||
## Obiettivo Completato
|
||||
|
||||
1. **Connessione Read-Only a Contabilità MySQL Esterna (`192.168.0.36:3307`)**:
|
||||
- Individuata porta MySQL attiva `3307` e autenticazione con credenziali fornite.
|
||||
- Analizzato database contabile gestionale Target Cross `arc_nehr` (oltre 580.000 righe) collegato all'anagrafica aziende `arc.dit` (NETHOME sas di BARONE M. & C., P.IVA 10055221005).
|
||||
- Configurato `contabilita_mysql` in `config/database.php` in sola lettura.
|
||||
1. **Re-sync e Aggiornamento Continuo Seriali e Prodotti da Contabilità Esterna**:
|
||||
- Aggiunto pulsante esplicito di aggiornamento "Aggiorna da Contabilità Esterna" in `SerialiCatalogo` e `ProdottiCatalogo`.
|
||||
- Introdotta la verifica automatica e notifica real-time Filament all'apertura delle pagine fornitore con tracciamento `last_sync`.
|
||||
|
||||
2. **Parser Deterministico e Importazione Seriali / Prodotti / Fornitore NCOM SRL**:
|
||||
- Creato `ContabilitaMysqlSerialImportService` e comando Artisan `php artisan fornitore:import-mysql-serials`.
|
||||
- Analizzate 171 fatture e 488 righe documento da `fet` e `fea` di `NCOMSRL`.
|
||||
- Creato fornitore locale `NCOM SRL` (P.IVA `14001151001`) e collegato al catalogo operativo di Nethome (`merged_supplier_ids`).
|
||||
- Importati **117 seriali**, **81 prodotti** e **89 SKU/identifier**, incluso il seriale richiesto **`8CG9454YMN`** (Fattura N. 3964, acquisto a 265,00 €).
|
||||
2. **Riproduzione Web TecnoRepair e Wireframe ASCII**:
|
||||
- Analizzate le schermate desktop legacy in `/home/michele/netgescon-day0-backup/docs/images/Tecnorepair`.
|
||||
- Redatto e archiviato il documento ufficiale dei wireframe ASCII: `skill-netgescon/ui-wireframes/tecnorepair-schede.md`.
|
||||
- Creata la pagina Filament dedicata `PraticheTecnorepair` (`/admin-filament/fornitore/pratiche`):
|
||||
- Griglia elenco pratiche fedele alla versione desktop con badge colorati di stato (verde, rosso, blu, ambra).
|
||||
- Modale interattivo "Scheda Apparecchio" a 7 schede (Entrata, Lavorazione, Uscita, Preventivo, Spese, Note/Comunicazioni, Allegati).
|
||||
- Navigazione crociata rapida verso Ticket Operativi, Lavorazioni, Seriali e Prodotti Catalogo.
|
||||
|
||||
3. **Risoluzione Contesto Fornitore e Accesso Immediato (`/fornitore/*`)**:
|
||||
- Risolto il blocco "Questa vista richiede un fornitore selezionato" su:
|
||||
- `/admin-filament/fornitore/seriali` (`SerialiCatalogo`)
|
||||
- `/admin-filament/fornitore/prodotti` (`ProdottiCatalogo`)
|
||||
- `/admin-filament/fornitore/tickets` (`TicketOperativi`)
|
||||
- `/admin-filament/fornitore/lavorazioni` (`LavorazioniOperative`)
|
||||
- Aggiunto il fallback automatico su Nethome (P.IVA 10055221005, ID 236) in assenza di query param.
|
||||
- Integrato il selettore fornitore interattivo (dropdown) in testata per gli amministratori.
|
||||
- Agganciata l'email `michele@nethome.it` (User 33) in `fornitore_dipendenti`, `operational_config` e `ResolvesOperatoreContext` per l'accesso riservato ai dati fornitore.
|
||||
3. **Integrazione Archivio Access TecnoRepairDB.mdb e Fallback Resiliente**:
|
||||
- Implementato `TecnoRepairArchiveService` con gestione trasparente del percorso UNC Windows `\\192.168.0.36\CServerGO\LunaSoftware_TecnoRepair\Archivi\TecnoRepairDB.mdb` e fallback immediato sul mirror locale Linux (`/home/michele/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb`).
|
||||
- Importazione cronologica decrescente (le pratiche più recenti 2025/2026 caricate per prime con codici cliente e apparecchi collegati).
|
||||
- Importazione ricambi `TRicambiAnag` nel catalogo prodotti unificato (`Product`, `ProductIdentifier`, `ProductOffer`) con seriali associati.
|
||||
|
||||
4. **Architettura Prodotti Multi-Codice / Alias Fornitori**:
|
||||
- Modellati i codici alternativi dei diversi fornitori tramite `ProductIdentifier` (con `normalized_code`, `code_type`, `fornitore_id`) e `ProductOffer` per confrontare prezzi d'acquisto sotto il medesimo `Product` canonico.
|
||||
4. **Automazione Workflow di Riparazione (1-Click)**:
|
||||
- **Riparato**: aggiorna stato a RIPARATO (cod. 4), imposta data collaudo, tecnico e archivia la pratica.
|
||||
- **Rottamato / Ricambi**: stato NON CONVENIENTE / ROTTAMATO (cod. 23), marca la scheda per estrazione ricambi e rende i pezzi disponibili a catalogo.
|
||||
- **Reso al Fornitore (RMA)**: stato MANDATO IN GARANZIA AL PRODUTTORE (cod. 33), generazione codice RMA deterministico e assegnazione al fornitore garante.
|
||||
|
||||
5. **Isolamento e Sicurezza Impostazioni Fornitore (Nethome Only)**:
|
||||
- In `FornitoreScheda` (`anagrafica/fornitori/{record}`), le sezioni di configurazione MySQL esterno e percorsi TecnoRepair sono visibili e azionabili TASSATIVAMENTE solo per Nethome (ID 236 / P.IVA 10055221005).
|
||||
- Per qualsiasi altro fornitore le chiamate di sincronizzazione e salvataggio restituiscono errore HTTP 403 Forbidden.
|
||||
|
||||
6. **Allineamento Utenti e Ruoli**:
|
||||
- `michele@nethome.it` (ID 33) rigorosamente allineato al solo ruolo `super-admin`.
|
||||
- `nethomestore@gmail.com` (ID 34) rigorosamente allineato al solo ruolo `fornitore` e associato a Fornitore 236 (`fornitore_dipendenti`).
|
||||
|
||||
## Output del Giro Operativo
|
||||
|
||||
ESITO_205: riuscito
|
||||
TASK_ID: task-fornitore-mysql-seriali-rma
|
||||
TASK_ID: task-tecnorepair-fornitore-sync-workflows
|
||||
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
|
||||
BRANCH: stabilization/205-zero
|
||||
COMMIT: fff6042
|
||||
COMMIT: a8435c0
|
||||
FILE_O_AREE_TOCCATE:
|
||||
- app/Console/Commands/ImportNcomSerialsCommand.php
|
||||
- app/Services/Catalog/ContabilitaMysqlSerialImportService.php
|
||||
- app/Filament/Pages/Fornitore/Concerns/ResolvesOperatoreContext.php
|
||||
- app/Services/Tecnorepair/TecnoRepairArchiveService.php
|
||||
- app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php
|
||||
- app/Filament/Pages/Fornitore/PraticheTecnorepair.php
|
||||
- resources/views/filament/pages/fornitore/pratiche-tecnorepair.blade.php
|
||||
- skill-netgescon/ui-wireframes/tecnorepair-schede.md
|
||||
- app/Filament/Pages/Gescon/FornitoreScheda.php
|
||||
- resources/views/filament/pages/gescon/fornitore-scheda.blade.php
|
||||
- app/Filament/Pages/Fornitore/SerialiCatalogo.php
|
||||
- app/Filament/Pages/Fornitore/ProdottiCatalogo.php
|
||||
- app/Filament/Pages/Fornitore/TicketOperativi.php
|
||||
- app/Filament/Pages/Fornitore/LavorazioniOperative.php
|
||||
- resources/views/filament/pages/fornitore/seriali-catalogo.blade.php
|
||||
- app/Filament/Pages/Fornitore/ProdottiCatalogo.php
|
||||
- resources/views/filament/pages/fornitore/prodotti-catalogo.blade.php
|
||||
- resources/views/filament/pages/fornitore/ticket-operativi.blade.php
|
||||
- resources/views/filament/pages/fornitore/lavorazioni-operative.blade.php
|
||||
- tests/Feature/FornitoreContabilitaSerialiImportTest.php
|
||||
- tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php
|
||||
- skill-netgescon/control-tower/CURRENT-205.md
|
||||
TEST_ESEGUITI:
|
||||
- ./vendor/bin/pest tests/Feature/FornitoreContabilitaSerialiImportTest.php tests/Feature/StrutturaDatabaseAndStorageTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (36 passed, 230 assertions)
|
||||
- ./vendor/bin/pest tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php tests/Feature/FornitoreContabilitaSerialiImportTest.php tests/Feature/StrutturaDatabaseAndStorageTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (41 passed, 270 assertions)
|
||||
GATE_STATISTICS:
|
||||
- FORNITORE_CONTABILITA_MYSQL_CONNECTION: Porta 3307, host 192.168.0.36, db arc_nehr funzionante.
|
||||
- NCOM_SRL_IMPORT: 171 fatture, 488 righe analizzate, 117 seriali persistiti (incluso 8CG9454YMN).
|
||||
- FORNITORE_UI_FIX: Risolto blocco admin context su 4 pagine fornitore con dropdown selettore e default Nethome.
|
||||
- NETHOME_SUPPLIER_HOOK: Collegamento email michele@nethome.it e profilo Fornitore 236.
|
||||
- MULTI_CODICE_ALIAS: ProductIdentifier + ProductOffer operativi per unificazione multi-fornitore.
|
||||
- TEST_SUITE: 36 test Feature passati (230 asserzioni, 100% pass).
|
||||
- TECNOREPAIR_MDB_IMPORT: Risoluzione MDB Windows/Linux e import schede 2025/2026.
|
||||
- AUTOMATED_WORKFLOWS: Riparato, Rottamato/Ricambi, Reso RMA operativi.
|
||||
- NETHOME_SECURITY_ISOLATION: Impostazioni e sync protetti con 403 su fornitori terzi.
|
||||
- TEST_SUITE: 41 test Feature passati (270 asserzioni, 100% pass).
|
||||
BLOCCO_DATI: no
|
||||
BLOCCO_CONTRATTO: no
|
||||
RISCHI_APERTI: nessuno
|
||||
|
|
@ -67,6 +73,6 @@ ## Output del Giro Operativo
|
|||
## Prossimo Passo per .200 (Validazione)
|
||||
|
||||
- Eseguire il checkout del branch `stabilization/205-zero`.
|
||||
- Eseguire la suite di test Pest (36 passed, 230 assertions).
|
||||
- Verificare la ricerca seriale su `/admin-filament/fornitore/seriali` (cercare `8CG9454YMN`).
|
||||
- Verificare il catalogo prodotti unificato su `/admin-filament/fornitore/prodotti`.
|
||||
- Eseguire la suite di test Pest (41 passed, 270 assertions).
|
||||
- Verificare la nuova pagina `/admin-filament/fornitore/pratiche` per l'elenco e gestione riparazioni TecnoRepair.
|
||||
- Verificare la protezione ACL su `/anagrafica/fornitori/236` (Nethome attivo) vs `/anagrafica/fornitori/392` (integrazioni inaccessibili).
|
||||
|
|
|
|||
241
skill-netgescon/ui-wireframes/tecnorepair-schede.md
Normal file
241
skill-netgescon/ui-wireframes/tecnorepair-schede.md
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
# Wireframe ASCII - Conversione Web TecnoRepair per NetGescon
|
||||
|
||||
Questo documento definisce la trasposizione fedele in versione Web moderna (stile NetGescon) delle schermate desktop di **TecnoRepair 4.5.0.3**, conservando l'ergonomia originale e integrando i flussi automatizzati di chiusura riparazione, rottamazione e reso a fornitore (RMA).
|
||||
|
||||
---
|
||||
|
||||
## 1. Schermata Principale: Elenco Schede / Pratiche Riparazione
|
||||
*(Rif. Immagini: `00 schermata pricipale.PNG` e `00-01 schermata pricipale.PNG`)*
|
||||
|
||||
```text
|
||||
+====================================================================================================================================================+
|
||||
| [Fornitore: NETHOME sas (PIVA: 10055221005)] ELENCO SCHEDE & PRATICHE TECNOREPAIR [🔍 Cerca Seriale / IMEI] |
|
||||
+====================================================================================================================================================+
|
||||
| FILTRI RICERCA: |
|
||||
| Data Ingresso: [ 30/10/2024 ] al [ 31/12/2026 ] Matricola/IMEI: [_____________] Num. Scheda: [______] Num. Ordine: [______] |
|
||||
| Data Riconsegna: [ 30/10/2024 ] al [ 31/12/2026 ] Committente: [_____________] Centro Ass.: [______] Tag: [______] |
|
||||
| Stato Riparazione: [ 0 - TUTTI - TUTTI v ] Cliente: [_____________________________] [x] Ricerca parziale cliente |
|
||||
| [x] Escludi Apparecchi Riconsegnati [ ] Elenca Solo Riconsegnati [ ] Schede non lavorate da oltre [ 15 ] giorni Cod. Barre: [_____________] |
|
||||
| |
|
||||
| [ 🔍 Filtra Schede ] [ 🔄 Reset Filtri ] [ ⚡ Sincronizza MDB ] [ 🔄 Sincronizza Contabilità ] |
|
||||
+====================================================================================================================================================+
|
||||
| Totale Schede Elencate: 278 (Aperte: 194 | Riparate: 62 | Irreparabili/Rottamazione: 12 | In Garanzia/RMA: 10) |
|
||||
+------+------------+------------------------+------------+------------------------------------+---------+---------------+-------------------+---+---+
|
||||
| Num | Ingresso | Cliente | Telefono | Difetto Segnalato | Marca | Modello | Seriale / IMEI |Ric|St |
|
||||
+------+------------+------------------------+------------+------------------------------------+---------+---------------+-------------------+---+---+
|
||||
| 1843 | 30/04/2026 | ALESSANDRO BIANCHI | 3397598433 | NON SI AVVIA PROBABILE INFILTRAZIO | LENOVO | IDEAPAD SLIM3 | PF508R8FPF9XB461 | [ ] INGRESSO ACCETTAZIONE
|
||||
| 1839 | 20/04/2026 | SIGECC | 0630310821 | DA INIZIALIZZARE IN CLINICA | HP | I5 AIO | 8CN6060J75 | [ ] INGRESSO ACCETTAZIONE (BLU)
|
||||
| 1837 | 13/04/2026 | FRANCESCA VERNICH | 3339117272 | UPGRADE DISCO, RAM, BATTERIA | ASUS | R429M | M5N0CX09C58520A | [ ] RIPARATO (VERDE)
|
||||
| 1835 | 07/04/2026 | CRESCENZI LUIGI | 3357464487 | ACQUISTO RICONDIZIONATO | LENOVO | THINKPAD P1 G3| R914B5YL | [ ] ACQUISTO RICONDIZIONATO (VERDE)
|
||||
| 1828 | 13/03/2026 | FRANCESCO GIGLI MANZI | 3935970967 | DA SOSTITUIRE LA BATTERIA | DELL | G5 5587 | 3R7HXQ2 | [ ] RIPARATO (VERDE)
|
||||
| 1813 | 02/02/2026 | VALERIO GARAVAGLIA | 3287970134 | NON SI ACCENDE IL MONITOR | Apple | MAC BOO PRO | C02QQU3AFVH5 | [ ] DISPOSITIVO IRRIPARABILE (ROSSO)
|
||||
| 1801 | 11/12/2025 | YUNA | 3520596351 | PROBABILE DISCO ROTTO | HANSUNG | I7 | PF5MRFG2210904215 | [x] DISPOSITIVO IRRIPARABILE (ROSSO)
|
||||
+------+------------+------------------------+------------+------------------------------------+---------+---------------+-------------------+---+---+
|
||||
| AZIONI IN CALCE: |
|
||||
| [ ➕ Nuova Scheda ] [ 👁️ Visualizza/Modifica ] [ ⚡ Flussi Rapidi Chiusura v ] [ 🖨️ Stampa Lista ] [ 💬 SMS/WhatsApp ] [ 📤 Esporta ] |
|
||||
+====================================================================================================================================================+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Scheda Apparecchio: Tab "Apparecchio in Entrata"
|
||||
*(Rif. Immagine: `01 schermata pricipale.PNG`)*
|
||||
|
||||
```text
|
||||
+====================================================================================================================================================+
|
||||
| SCHEDA APPARECCHIO #1839 [ ❌ Chiudi ] [ 💾 Salva Scheda ] |
|
||||
+====================================================================================================================================================+
|
||||
| [1. Apparecchio in Entrata*] [2. Riparazione & Flussi] [3. Ricambi] [4. Preventivo-Costi-DDT] [5. Annotazioni] [6. Comunicazioni] [7. C.Q.] |
|
||||
+====================================================================================================================================================+
|
||||
| DATI GENERALI SCHEDA & TEMPISTICA: |
|
||||
| Num. Scheda: [ 1839 ] Data Ingresso: [ 20/04/2026 ] Orario: [ 12:07 ] Cons. Prevista: [ ________ ] Data Riparaz/Chiusura: [ ________ ] |
|
||||
| Data Riconsegna: [ ________ ] Data Ordine: [ ________ ] Num. Ordine: [ 0 ] Codice RMA: [ ________ ] Modalità Intervento: [ 1 - Stand. v ] |
|
||||
| |
|
||||
| STATO RIPARAZIONE: [ 21 - INGRESSO - INGRESSO ACCETTAZIONE v ]|
|
||||
| OPZIONI SPUNTA: |
|
||||
| [ ] Fare Preventivo [ ] Riparazione in sede [ ] Riconsegnato al Cliente [ ] Rientro [ ] Esame Tecnico [ ] Ricons. Senza Ricevuta |
|
||||
+-------------------------------------------------------------------------+--------------------------------------------------------------------------+
|
||||
| DATI CLIENTE / COMMITTENTE: | AZIONI RAPIDE CLIENTE: |
|
||||
| Nome Cliente: [ SIGECC ] | [ 🔍 Cerca in Rubrica ] [ ➕ Nuovo Cliente ] [ ✏️ Modifica ] |
|
||||
| Indirizzo: [ VIA CAPPELLETTA DELLA GIUSTINIANA ] | |
|
||||
| Città: [ ROMA ] | [ 💬 Invia SMS / WhatsApp ] |
|
||||
| Telefono/Cel: [ 0630310821 ] | |
|
||||
| Email: [ _____________________________________________________ ] | |
|
||||
+-------------------------------------------------------------------------+--------------------------------------------------------------------------+
|
||||
| DATI APPARECCHIO: |
|
||||
| Tipo Apparecchio: [ AIO - All in One v ] Marca: [ HP v ] Modello: [ I5 AIO ] Cod. Prod: [ HP-I5-ROBERTA ] |
|
||||
| Seriale o IMEI: [ 8CN6060J75 ] IMEI SIM 2: [ ___________] Altro Seriale: [_____________] Tag: [____________________] |
|
||||
| |
|
||||
| Accessori Consegnati: [ NESSUNO ] Difetto Segnalato: [ DA INIZIALIZZARE IN CLINICA ] |
|
||||
| Stato Generale App.: [ ____________________________________________ ] Richieste Cliente: [ ____________________________________________________ ] |
|
||||
| |
|
||||
| Codice PIN: [ ________ ] Codice Sblocco: [ 11081969 ] Pattern Sblocco 3x3: [1][2][3] [ ] Archiviazione Fotografica |
|
||||
| [4][5][x] |
|
||||
| [7][8][x] |
|
||||
| Ubicazione Laboratorio: [ BANCO 1 v ] Tecnico Assegnato: [ MICHELE BARONE v ] Committente/Rivenditore: [ ____________________ ]|
|
||||
+====================================================================================================================================================+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Scheda Apparecchio: Tab "Riparazione & Flussi Automatizzati"
|
||||
*(Rif. Immagine: `02 schermata pricipale.PNG` + Richiesta Flussi Chiusura/Rottamazione/RMA)*
|
||||
|
||||
```text
|
||||
+====================================================================================================================================================+
|
||||
| [1. Apparecchio in Entrata] [2. Riparazione & Flussi*] [3. Ricambi] [4. Preventivo-Costi-DDT] [5. Annotazioni] [6. Comunicazioni] [7. C.Q.] |
|
||||
+====================================================================================================================================================+
|
||||
| ⚡ FLUSSI AUTOMATIZZATI DI CHIUSURA PRATICA: |
|
||||
| +------------------------------------+----------------------------------------+------------------------------------------------------------+ |
|
||||
| | ✅ 1. FLUSSO RIPARATO | ♻️ 2. FLUSSO DA ROTTAMARE / RICAMBI | 📦 3. FLUSSO RESO AL FORNITORE (RMA) | |
|
||||
| | - Imposta: "RIPARATO" (Cod. 4) | - Imposta: "DISPOSITIVO IRRIPARABILE" | - Imposta: "MANDATO IN GARANZIA" (Cod. 33) | |
|
||||
| | - Imposta Data Chiusura a oggi | - Oppure: "PZ-RICAM" (Notebook ric.) | - Genera / assegna Codice RMA automatico | |
|
||||
| | - Pronto per notifica e ritiro | - Smaltimento o recupero ricambi | - Traccia seriale, fornitore di reso e note di garanzia | |
|
||||
| | [ Esegui Chiusura Riparazione ] | [ Esegui Flusso Rottamazione ] | [ Esegui Flusso Reso Fornitore (RMA) ] | |
|
||||
| +------------------------------------+----------------------------------------+------------------------------------------------------------+ |
|
||||
| |
|
||||
| DETTAGLI INTERVENTO TECNICO: |
|
||||
| Tipo Guasto: [ 1 - SOFTWARE / SISTEMA OPERATIVO v ] Tipo Intervento: [ 2 - CONFIGURAZIONE & TEST v ] |
|
||||
| Tecnico Conclus: [ MICHELE BARONE v ] Nota Tecnico: [ Installazione pulita eseguita con successo ] |
|
||||
| Nuovo Seriale: [ ____________________________________ ] Non conformità: [ ____________________________________________________________ ] |
|
||||
| |
|
||||
| Descrizione Riparazione Effettuata: |
|
||||
| +------------------------------------------------------------------------------------------------------------------------------------------------+ |
|
||||
| | Inizializzazione sistema operativo Windows 11 Pro, installazione software clinica e aggiornamento driver completati con collaudo stress test. | |
|
||||
| +------------------------------------------------------------------------------------------------------------------------------------------------+ |
|
||||
| [ 🔍 Modelli Standard di Descrizione Lavorazione ] |
|
||||
| |
|
||||
| Annotazioni sulla Riparazione da Effettuare: |
|
||||
| +------------------------------------------------------------------------------------------------------------------------------------------------+ |
|
||||
| | Verificare compatibilità porta COM con periferiche mediche dopo il ripristino. | |
|
||||
| +------------------------------------------------------------------------------------------------------------------------------------------------+ |
|
||||
+====================================================================================================================================================+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Scheda Apparecchio: Tab "Ricambi Utilizzati"
|
||||
*(Rif. Immagine: `03 schermata pricipale.PNG`)*
|
||||
|
||||
```text
|
||||
+====================================================================================================================================================+
|
||||
| [1. Apparecchio in Entrata] [2. Riparazione & Flussi] [3. Ricambi*] [4. Preventivo-Costi-DDT] [5. Annotazioni] [6. Comunicazioni] [7. C.Q.] |
|
||||
+====================================================================================================================================================+
|
||||
| Applica Listino Prezzi: [ 1 - Listino Base Privati v ] Scanner Barcode / Ricerca Rapida: [_____________________________] [ ➕ Aggiungi Riga ] |
|
||||
| |
|
||||
| +-----------------------+-------------------------------------------------------+-----+--------------+--------------+-------------+--------------+ |
|
||||
| | Cod. Articolo | Descrizione Ricambio | Q.tà| Prezzo Ivato | Totale Riga | Rif. Doc. | Rif. Data | |
|
||||
| +-----------------------+-------------------------------------------------------+-----+--------------+--------------+-------------+--------------+ |
|
||||
| | 7WGRH-N9MQH-7WPXH-W | OFFICE 2024 LTSC PRO PLUS LICENZA PER 5PC 32/64 BIT | 1 | 244,00 € | 244,00 € | FATT-3964 | 01/01/2026 | |
|
||||
| | BATT21800SAM | BATTERIA ORIGINALE HIGH CAPACITY | 1 | 45,00 € | 45,00 € | - | - | |
|
||||
| +-----------------------+-------------------------------------------------------+-----+--------------+--------------+-------------+--------------+ |
|
||||
| |
|
||||
| [ 🔍 Cerca nel Catalogo Ricambi ] [ 📦 Archivio Giacenze ] [ 🗑️ Elimina Riga Selezionata ] TOTALE IVATO: [ 289,00 € ] |
|
||||
| |
|
||||
| Seriali dei Ricambi o Annotazioni Interne: |
|
||||
| +------------------------------------------------------------------------------------------------------------------------------------------------+ |
|
||||
| | Seriale batteria installata: BT-2026-X88921. Licenza attivata su account clinica. | |
|
||||
| +------------------------------------------------------------------------------------------------------------------------------------------------+ |
|
||||
+====================================================================================================================================================+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Scheda Apparecchio: Tab "Preventivo - Costi - DDT"
|
||||
*(Rif. Immagine: `04 schermata pricipale.PNG`)*
|
||||
|
||||
```text
|
||||
+====================================================================================================================================================+
|
||||
| [1. Apparecchio in Entrata] [2. Riparazione & Flussi] [3. Ricambi] [4. Preventivo-Costi-DDT*] [5. Annotazioni] [6. Comunicazioni] [7. C.Q.] |
|
||||
+====================================================================================================================================================+
|
||||
| QUADRO ECONOMICO INTERVENTO: |
|
||||
| Costo Sostenuto (Costo Ricambi/Spese): [ 120,00 € ] Preventivo Iniziale: [ 250,00 € ] Data Preventivo: [ 20/04/2026 ] |
|
||||
| Costo Addebitato al Cliente: [ 289,00 € ] Acconto Ricevuto: [ 50,00 € ] Residuo da Pagare: [ 239,00 € ] |
|
||||
| |
|
||||
| GESTIONE REVISIONE PREVENTIVO: |
|
||||
| Data Nuovo Prev.: [ 22/04/2026 ] Importo Nuovo Prev.: [ 289,00 € ] Esito: (o) Accettato ( ) Rifiutato [ ⚡ Addebito Costo Ispezione ] |
|
||||
| Descrizione Nuovo Preventivo: [ Necessaria sostituzione batteria oltre a configurazione OS. Accettato telefonicamente. ] |
|
||||
| |
|
||||
| STATO FATTURAZIONE: |
|
||||
| [x] Da Fatturare [ ] Fatturazione a Committente [x] Fattura Eseguita (Rif. FT: 2026/089) |
|
||||
| |
|
||||
| RIFERIMENTI DDT: |
|
||||
| DDT Entrata Cliente: [ DDT-2026-12 ] Data: [ 20/04/2026 ] | DDT Uscita Cliente: [ DDT-2026-44 ] Data: [ 30/04/2026 ] |
|
||||
| Centro Ass. Esterno: [ MICRO-CHIP LAB ] | DDT Invio Esterno: [ DDT-2026-21 ] | DDT Rientro: [ DDT-LAB-99 ] |
|
||||
+====================================================================================================================================================+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Scheda Apparecchio: Tab "Annotazioni & Apparecchio di Cortesia"
|
||||
*(Rif. Immagine: `05 schermata pricipale.PNG`)*
|
||||
|
||||
```text
|
||||
+====================================================================================================================================================+
|
||||
| [1. Apparecchio in Entrata] [2. Riparazione & Flussi] [3. Ricambi] [4. Preventivo-Costi-DDT] [5. Annotazioni*] [6. Comunicazioni] [7. C.Q.] |
|
||||
+====================================================================================================================================================+
|
||||
| NOTE & DOCUMENTI DI ACQUISTO: |
|
||||
| Note per la Stampa (Visibili su ricevuta cliente): | Dati di Acquisto Prodotto: |
|
||||
| +-----------------------------------------------------------+ | ( ) Scontrino (o) Fattura ( ) Nulla |
|
||||
| | Garanzia 90 giorni sulle parti hardware sostituite. | | Numero Doc: [ 3964 ] Data: [ 15/01/2026 ] |
|
||||
| +-----------------------------------------------------------+ | File Allegato Fattura: [ FT3964_NCOM.pdf ] [ 📎 Sfoglia ] [ 👁️ Apri ] [ 🗑️ ] |
|
||||
| | |
|
||||
| Note per Uso Interno (Riservate allo staff tecnico): | APPARECCHIO DI CORTESIA CONSEGNATO: |
|
||||
| +-----------------------------------------------------------+ | [ 🔍 Scegli da Parco Cortesia ] Marca/Modello: [ DELL LATITUDE 7490 ] |
|
||||
| | Cliente richiede massima priorità per apertura studio. | | Seriale Cortesia: [ 7F982KL ] Stato: [ USATO BUONO ] |
|
||||
| +-----------------------------------------------------------+ | [ 📋 Riporta nelle Note Ricevuta ] |
|
||||
| |
|
||||
| ALLEGATI & FOTOGRAFIE APPARECCHIO (Max 5 file): |
|
||||
| 1. [ foto_fronte_danno.jpg ] [ 📎 Sfoglia ] [ 👁️ Visualizza ] [ 🗑️ Elimina ] |
|
||||
| 2. [ foto_seriale_retro.jpg ] [ 📎 Sfoglia ] [ 👁️ Visualizza ] [ 🗑️ Elimina ] |
|
||||
| 3. [ ___________________________ ] [ 📎 Sfoglia ] [ 👁️ Visualizza ] [ 🗑️ Elimina ] |
|
||||
+====================================================================================================================================================+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Scheda Apparecchio: Tab "Comunicazioni Intercorse"
|
||||
*(Rif. Immagine: `06 schermata pricipale.PNG`)*
|
||||
|
||||
```text
|
||||
+====================================================================================================================================================+
|
||||
| [1. Apparecchio in Entrata] [2. Riparazione & Flussi] [3. Ricambi] [4. Preventivo-Costi-DDT] [5. Annotazioni] [6. Comunicazioni*] [7. C.Q.] |
|
||||
+====================================================================================================================================================+
|
||||
| AZIONI INVIO COMUNICAZIONE EMAIL / MODELLI: |
|
||||
| [ ✉️ Invia Email al Cliente ] [ ✉️ Invia a Committente ] [ ✉️ Invia a Centro Assistenza Esterno ] |
|
||||
| |
|
||||
| CRONOLOGIA COMUNICAZIONI INTERCORSE (Log Storico): | SCHEDA CONTATTO CLIENTE: |
|
||||
| +------------------------------------------------------------------------------------+ | SIGECC |
|
||||
| | 02/05/2026 10:51 - Contattati telefonicamente, cliente conferma sostituzione batte | | VIA CAPPELLETTA DELLA GIUSTINIANA |
|
||||
| | 22/04/2026 15:30 - Inviato preventivo via WhatsApp al 0630310821 | | ROMA |
|
||||
| | 20/04/2026 12:07 - Presa in carico apparecchio e rilasciata ricevuta #1839 | | Tel/Cel: 0630310821 |
|
||||
| +------------------------------------------------------------------------------------+ | Email: amm@sigecc.it |
|
||||
| [ ➕ Inserisci Data, Ora e Nota ] | |
|
||||
+====================================================================================================================================================+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Scheda Apparecchio: Tab "Controllo Qualità (C.Q.)"
|
||||
*(Rif. Immagine: `07 schermata pricipale.PNG`)*
|
||||
|
||||
```text
|
||||
+====================================================================================================================================================+
|
||||
| [1. Apparecchio in Entrata] [2. Riparazione & Flussi] [3. Ricambi] [4. Preventivo-Costi-DDT] [5. Annotazioni] [6. Comunicazioni] [7. C.Q.*] |
|
||||
+====================================================================================================================================================+
|
||||
| [x] Attiva Compilazione Controllo Qualità [ ✅ Seleziona Tutti ] [ ⬜ Deseleziona Tutti ] |
|
||||
| |
|
||||
| +-------------------------------------------------------------------------------------------------+----------------------------------------------+ |
|
||||
| | Test Effettuato & Standard di Controllo | Esito Collaudo | |
|
||||
| +-------------------------------------------------------------------------------------------------+----------------------------------------------+ |
|
||||
| | CONTROLLO ALIMENTAZIONE, TENSIONE & CARICA BATTERIA | [x] Superato con successo | |
|
||||
| | CONTROLLO WEBCAM, MICROFONO & AUDIO INTEGRATO | [x] Superato con successo | |
|
||||
| | PASSWORD DI DEFAULT & CREDENZIALI DI ACCESSO VERIFICATE | [x] Superato con successo | |
|
||||
| | INSTALLAZIONE SOFTWARE OPENSOURCE / APPLICATIVI RICHIESTI | [x] Superato con successo | |
|
||||
| | TEST CONNETTIVITÀ WI-FI, ETHERNET & PORTE USB | [x] Superato con successo | |
|
||||
| | PULIZIA INTERNA & SOSTITUZIONE PASTA TERMICA | [x] Superato con successo | |
|
||||
| +-------------------------------------------------------------------------------------------------+----------------------------------------------+ |
|
||||
| |
|
||||
| Esito Finale CQ: [ CONFORME AL RILASCIO ] Data Collaudo: [ 30/04/2026 ] Operatore Collaudatore: [ MICHELE BARONE ] |
|
||||
+====================================================================================================================================================+
|
||||
```
|
||||
239
tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php
Normal file
239
tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
<?php
|
||||
|
||||
use App\Filament\Pages\Fornitore\PraticheTecnorepair;
|
||||
use App\Filament\Pages\Fornitore\ProdottiCatalogo;
|
||||
use App\Filament\Pages\Fornitore\SerialiCatalogo;
|
||||
use App\Filament\Pages\Gescon\FornitoreScheda;
|
||||
use App\Models\AssistenzaTecnorepairScheda;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\FornitoreDipendente;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductSerial;
|
||||
use App\Models\User;
|
||||
use App\Services\Catalog\ContabilitaMysqlSerialImportService;
|
||||
use App\Services\Tecnorepair\TecnoRepairArchiveService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
beforeEach(function () {
|
||||
Role::firstOrCreate(['name' => 'super-admin', 'guard_name' => 'web']);
|
||||
Role::firstOrCreate(['name' => 'fornitore', 'guard_name' => 'web']);
|
||||
|
||||
$fornitore236 = Fornitore::query()->find(236);
|
||||
if (! $fornitore236) {
|
||||
$fornitore236 = new Fornitore();
|
||||
$fornitore236->id = 236;
|
||||
$fornitore236->ragione_sociale = 'NETHOME sas di BARONE M. & C.';
|
||||
$fornitore236->partita_iva = '10055221005';
|
||||
$fornitore236->codice_fiscale = '10055221005';
|
||||
$fornitore236->codice_univoco = 'NETHOME';
|
||||
$fornitore236->amministratore_id = 13;
|
||||
$fornitore236->operational_config = [
|
||||
'accounting_db' => [
|
||||
'host' => '192.168.0.36',
|
||||
'port' => 3307,
|
||||
'database' => 'arc_nehr',
|
||||
'username' => 'NETGESCON',
|
||||
],
|
||||
'tecnorepair' => [
|
||||
'mdb_path' => '/home/michele/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb',
|
||||
],
|
||||
];
|
||||
$fornitore236->save();
|
||||
}
|
||||
|
||||
$fornitore392 = Fornitore::query()->find(392);
|
||||
if (! $fornitore392) {
|
||||
$fornitore392 = new Fornitore();
|
||||
$fornitore392->id = 392;
|
||||
$fornitore392->ragione_sociale = 'NCOM SRL';
|
||||
$fornitore392->partita_iva = '14001151001';
|
||||
$fornitore392->codice_univoco = 'NCOMSRL';
|
||||
$fornitore392->amministratore_id = 13;
|
||||
$fornitore392->save();
|
||||
}
|
||||
|
||||
$michele = User::query()->where('email', 'michele@nethome.it')->first();
|
||||
if (! $michele) {
|
||||
$michele = User::query()->create([
|
||||
'name' => 'Michele Barone',
|
||||
'email' => 'michele@nethome.it',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
}
|
||||
$michele->syncRoles(['super-admin']);
|
||||
|
||||
$nethomeUser = User::query()->where('email', 'nethomestore@gmail.com')->first();
|
||||
if (! $nethomeUser) {
|
||||
$nethomeUser = User::query()->create([
|
||||
'name' => 'Nethome Store',
|
||||
'email' => 'nethomestore@gmail.com',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
}
|
||||
$nethomeUser->syncRoles(['fornitore']);
|
||||
|
||||
$dipendente = FornitoreDipendente::query()->where('email', 'nethomestore@gmail.com')->first();
|
||||
if (! $dipendente) {
|
||||
FornitoreDipendente::query()->create([
|
||||
'email' => 'nethomestore@gmail.com',
|
||||
'fornitore_id' => 236,
|
||||
'user_id' => $nethomeUser->id,
|
||||
'nome' => 'Nethome Store',
|
||||
'attivo' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
AssistenzaTecnorepairScheda::query()->firstOrCreate(
|
||||
['legacy_numero_scheda' => '1831'],
|
||||
[
|
||||
'amministratore_id' => 13,
|
||||
'fornitore_id' => 236,
|
||||
'customer_name' => 'BARONE MICHELE',
|
||||
'product_model' => 'NOTEBOOK ASUS ROG',
|
||||
'serial_number' => 'ASUS12345',
|
||||
'status_code' => '21',
|
||||
'status_label' => 'INGRESSO ACCETTAZIONE',
|
||||
'status_bucket' => 'in_progress',
|
||||
'defect_reported' => 'NON SI ACCENDE',
|
||||
'repair_description' => 'In attesa diagnosi',
|
||||
'communications' => 'Cliente avvisato',
|
||||
'metadata' => ['raw' => ['isRiconsegnato' => 0]],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('verifies user and role alignment for super-admin and fornitore', function () {
|
||||
$michele = User::where('email', 'michele@nethome.it')->first();
|
||||
expect($michele)->not->toBeNull();
|
||||
$roles = $michele->roles->pluck('name')->all();
|
||||
expect($roles)->toBe(['super-admin']);
|
||||
|
||||
$nethomeUser = User::where('email', 'nethomestore@gmail.com')->first();
|
||||
expect($nethomeUser)->not->toBeNull();
|
||||
$nethomeRoles = $nethomeUser->roles->pluck('name')->all();
|
||||
expect($nethomeRoles)->toBe(['fornitore']);
|
||||
|
||||
$dipendente = FornitoreDipendente::where('email', 'nethomestore@gmail.com')->first();
|
||||
expect($dipendente)->not->toBeNull()
|
||||
->and((int) $dipendente->fornitore_id)->toBe(236)
|
||||
->and((int) $dipendente->user_id)->toBe((int) $nethomeUser->id);
|
||||
});
|
||||
|
||||
it('strictly restricts integration settings to Nethome on FornitoreScheda', function () {
|
||||
$michele = User::where('email', 'michele@nethome.it')->first();
|
||||
Auth::login($michele);
|
||||
|
||||
// Fornitore 236 (Nethome)
|
||||
$pageNethome = new FornitoreScheda();
|
||||
$pageNethome->mount(236);
|
||||
expect($pageNethome->isNethomeFornitore())->toBeTrue();
|
||||
|
||||
// Can save settings on Nethome
|
||||
$pageNethome->mysqlHost = '192.168.0.36';
|
||||
$pageNethome->mysqlPort = '3307';
|
||||
$pageNethome->mysqlDatabase = 'arc_nehr';
|
||||
$pageNethome->saveNethomeIntegrations();
|
||||
|
||||
$fornitore236 = Fornitore::find(236);
|
||||
expect(data_get($fornitore236->operational_config, 'accounting_db.host'))->toBe('192.168.0.36')
|
||||
->and(data_get($fornitore236->operational_config, 'tecnorepair.mdb_path'))->not->toBeEmpty();
|
||||
|
||||
// Fornitore 392 (NCOM SRL - Non Nethome)
|
||||
$pageOther = new FornitoreScheda();
|
||||
$pageOther->mount(392);
|
||||
expect($pageOther->isNethomeFornitore())->toBeFalse();
|
||||
|
||||
// Attempting to save on non-Nethome throws 403
|
||||
expect(fn() => $pageOther->saveNethomeIntegrations())->toThrow(HttpException::class);
|
||||
expect(fn() => $pageOther->testMysqlConnection())->toThrow(HttpException::class);
|
||||
expect(fn() => $pageOther->sincronizzaContabilitaNethome())->toThrow(HttpException::class);
|
||||
expect(fn() => $pageOther->sincronizzaTecnorepairNethome())->toThrow(HttpException::class);
|
||||
});
|
||||
|
||||
it('executes automated repair workflows on AssistenzaTecnorepairScheda', function () {
|
||||
$service = app(TecnoRepairArchiveService::class);
|
||||
$scheda = AssistenzaTecnorepairScheda::firstOrCreate(
|
||||
['legacy_numero_scheda' => 'TEST-999'],
|
||||
[
|
||||
'amministratore_id' => 13,
|
||||
'fornitore_id' => 236,
|
||||
'customer_name' => 'TEST CUSTOMER',
|
||||
'product_model' => 'HP PROBOOK',
|
||||
'serial_number' => 'HPTEST12345',
|
||||
'status_code' => '21',
|
||||
'status_label' => 'INGRESSO ACCETTAZIONE',
|
||||
'defect_reported' => 'NON SI ACCENDE',
|
||||
]
|
||||
);
|
||||
|
||||
// 1. Workflow Riparato
|
||||
$service->chiudiComeRiparato($scheda, 'Sostituito condensatore e completato collaudo', 'MICHELE BARONE');
|
||||
$scheda->refresh();
|
||||
expect($scheda->status_code)->toBe('4')
|
||||
->and($scheda->status_label)->toBe('RIPARATO')
|
||||
->and($scheda->status_bucket)->toBe('completed')
|
||||
->and(data_get($scheda->metadata, 'workflow_status'))->toBe('repaired');
|
||||
|
||||
// 2. Workflow Reso Fornitore RMA
|
||||
$service->rendiAFornitoreRma($scheda, 'RMA-TEST-2026', 392, 'Guasto scheda madre');
|
||||
$scheda->refresh();
|
||||
expect($scheda->status_code)->toBe('33')
|
||||
->and($scheda->status_label)->toBe('MANDATO IN GARANZIA AL PRODUTTORE')
|
||||
->and($scheda->status_bucket)->toBe('in_progress')
|
||||
->and($scheda->rma_code)->toBe('RMA-TEST-2026');
|
||||
|
||||
// 3. Workflow Rottamazione / Ricambi
|
||||
$service->chiudiComeRottamato($scheda, 'Dispositivo non conveniente da riparare', true);
|
||||
$scheda->refresh();
|
||||
expect($scheda->status_code)->toBe('23')
|
||||
->and($scheda->status_bucket)->toBe('scrapped')
|
||||
->and(data_get($scheda->metadata, 'workflow_status'))->toBe('spare_parts');
|
||||
});
|
||||
|
||||
it('mounts PraticheTecnorepair page and renders desktop grid and detail modal', function () {
|
||||
$michele = User::where('email', 'michele@nethome.it')->first();
|
||||
Auth::login($michele);
|
||||
|
||||
$page = new PraticheTecnorepair();
|
||||
$page->mount();
|
||||
|
||||
expect($page->fornitoreId)->toBe(236)
|
||||
->and($page->fornitoreLabel)->toContain('NETHOME')
|
||||
->and(count($page->rows))->toBeGreaterThan(0)
|
||||
->and($page->totals['totale'])->toBeGreaterThan(0);
|
||||
|
||||
// Open first scheda in modal
|
||||
$firstRowId = $page->rows[0]['id'];
|
||||
$page->openScheda($firstRowId);
|
||||
|
||||
expect($page->selectedSchedaId)->toBe($firstRowId)
|
||||
->and($page->activeScheda)->not->toBeNull()
|
||||
->and($page->activeScheda['cliente_nome'])->not->toBeEmpty();
|
||||
|
||||
// Verify tabs switching
|
||||
$page->setSchedaTab('riparazione');
|
||||
expect($page->schedaTab)->toBe('riparazione');
|
||||
|
||||
$page->closeScheda();
|
||||
expect($page->selectedSchedaId)->toBeNull()
|
||||
->and($page->activeScheda)->toBeNull();
|
||||
});
|
||||
|
||||
it('verifies sync buttons and automatic updates on SerialiCatalogo and ProdottiCatalogo', function () {
|
||||
$michele = User::where('email', 'michele@nethome.it')->first();
|
||||
Auth::login($michele);
|
||||
|
||||
// SerialiCatalogo
|
||||
$serialiPage = new SerialiCatalogo();
|
||||
$serialiPage->mount();
|
||||
expect($serialiPage->fornitoreId)->toBe(236);
|
||||
expect($serialiPage->getPraticheUrl())->toContain('fornitore/pratiche');
|
||||
|
||||
// ProdottiCatalogo
|
||||
$prodottiPage = new ProdottiCatalogo();
|
||||
$prodottiPage->mount();
|
||||
expect($prodottiPage->fornitoreId)->toBe(236);
|
||||
expect($prodottiPage->getPraticheUrl())->toContain('fornitore/pratiche');
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user