fix(lavorazioni): solve 500 blade error, mount live tecnorepair mdb (1876), refactor lavorazioni with in-page tab scheda and modern design

This commit is contained in:
michele 2026-09-11 23:13:57 +02:00
parent ce33fe4f08
commit 067be6513a
9 changed files with 1690 additions and 324 deletions

2
.gitignore vendored
View File

@ -127,3 +127,5 @@ scripts/ops/windows/netgescon-tapi-provider-report.json
storage/app/private/** storage/app/private/**
storage/app/public/** storage/app/public/**
storage/app/*.json storage/app/*.json
storage/app/tecnorepair/
Google autorizzazioni/

View File

@ -593,6 +593,10 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder
protected function hasLegacyNominativiForStabile(string $codStabile): bool protected function hasLegacyNominativiForStabile(string $codStabile): bool
{ {
if (! DbSchema::connection('gescon_import')->hasTable('condomin')) {
return false;
}
return DB::connection('gescon_import') return DB::connection('gescon_import')
->table('condomin') ->table('condomin')
->where('cod_stabile', $codStabile) ->where('cod_stabile', $codStabile)
@ -602,6 +606,10 @@ protected function hasLegacyNominativiForStabile(string $codStabile): bool
private function getAccumulatedUnits(string $codStabile, string $legacyYear, string $codCond): array private function getAccumulatedUnits(string $codStabile, string $legacyYear, string $codCond): array
{ {
if (! DbSchema::connection('gescon_import')->hasTable('vw_legacy_condomin_nominativi')) {
return [];
}
$units = DB::connection('gescon_import') $units = DB::connection('gescon_import')
->table('vw_legacy_condomin_nominativi') ->table('vw_legacy_condomin_nominativi')
->where('cod_stabile', $codStabile) ->where('cod_stabile', $codStabile)
@ -874,7 +882,7 @@ public function table(Table $table): Table
->label('Anno legacy') ->label('Anno legacy')
->visible(fn(): bool => (bool) $useLegacy) ->visible(fn(): bool => (bool) $useLegacy)
->options(function () use ($codStabile): array { ->options(function () use ($codStabile): array {
if ($codStabile === '') { if ($codStabile === '' || ! DbSchema::connection('gescon_import')->hasTable('condomin')) {
return []; return [];
} }
return DB::connection('gescon_import') return DB::connection('gescon_import')

View File

@ -8,6 +8,8 @@
use App\Models\TicketIntervento; use App\Models\TicketIntervento;
use App\Models\User; use App\Models\User;
use BackedEnum; use BackedEnum;
use Carbon\Carbon;
use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -37,13 +39,43 @@ class LavorazioniOperative extends Page
public bool $missingAdminContext = false; public bool $missingAdminContext = false;
/** @var array<int, string> */
public array $fornitoriOptions = [];
// Main view navigation: 'elenco' or 'scheda'
public string $mainTab = 'elenco';
// Sub-tab inside the Scheda detail view (reproducing TecnoRepair desktop tabs)
public string $schedaSubTab = 'entrata';
public ?int $selectedRowId = null;
public ?string $selectedRowType = null; // 'tecnorepair' or 'ticket'
/** @var array<string, mixed>|null */
public ?array $activeScheda = null;
/** @var array<string, mixed> Editable form fields for active scheda */
public array $schedaForm = [];
/** @var array<int, int> 3x3 pattern unlock grid nodes */
public array $unlockPattern = [0, 0, 0, 0, 0, 0, 0, 0, 0];
// Quick workflow inputs
public string $workflowNote = '';
public string $workflowRmaCode = '';
// Filter properties
public string $scope = 'tutte'; public string $scope = 'tutte';
public string $search = '';
public string $filterStato = '';
public string $filterOrigine = '';
public string $dataDa = '';
public string $dataA = '';
/** @var array<int, array<string, mixed>> */ /** @var array<int, array<string, mixed>> */
public array $rows = []; public array $rows = [];
public ?array $detailModal = null;
/** @var array<string, int> */ /** @var array<string, int> */
public array $totals = [ public array $totals = [
'tutte' => 0, 'tutte' => 0,
@ -53,22 +85,27 @@ class LavorazioniOperative extends Page
'interne' => 0, 'interne' => 0,
]; ];
// Legacy modal compatibility
public ?array $detailModal = null;
public static function canAccess(): bool public static function canAccess(): bool
{ {
$user = Auth::user(); $user = Auth::user();
return $user instanceof User return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore']); && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore']);
} }
/** @var array<int, string> */
public array $fornitoriOptions = [];
public function mount(): void public function mount(): void
{ {
$this->scope = (string) request()->query('scope', 'tutte'); $this->scope = (string) request()->query('scope', 'tutte');
[$fornitore] = $this->resolveOperatoreContext(allowAdminWithoutSupplier: true); [$fornitore] = $this->resolveOperatoreContext(allowAdminWithoutSupplier: true);
if (! $fornitore instanceof Fornitore) {
$fornitore = Fornitore::query()->where('partita_iva', '10055221005')->first()
?? Fornitore::query()->orderBy('id')->first();
}
if (! $fornitore instanceof Fornitore) { if (! $fornitore instanceof Fornitore) {
$this->missingAdminContext = true; $this->missingAdminContext = true;
return; return;
@ -84,7 +121,7 @@ public function mount(): void
$q->whereHas('ticketInterventi') $q->whereHas('ticketInterventi')
->orWhere('partita_iva', '10055221005') ->orWhere('partita_iva', '10055221005')
->orWhere('partita_iva', '14001151001') ->orWhere('partita_iva', '14001151001')
->orWhereIn('id', [236, 392]); ->orWhereIn('id', [236, 359, 392]);
}) })
->orderBy('ragione_sociale') ->orderBy('ragione_sociale')
->get() ->get()
@ -102,7 +139,7 @@ public function updatedFornitoreId(): void
$fornitore = Fornitore::query()->find((int) $this->fornitoreId); $fornitore = Fornitore::query()->find((int) $this->fornitoreId);
if ($fornitore instanceof Fornitore) { if ($fornitore instanceof Fornitore) {
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore); $this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
$this->detailModal = null; $this->closeScheda();
$this->refreshData(); $this->refreshData();
} }
} }
@ -115,6 +152,28 @@ public function updatedScope(): void
public function setScope(string $scope): void public function setScope(string $scope): void
{ {
$this->scope = $scope; $this->scope = $scope;
$this->mainTab = 'elenco';
$this->refreshData();
}
public function setMainTab(string $tab): void
{
$this->mainTab = in_array($tab, ['elenco', 'scheda'], true) ? $tab : 'elenco';
}
public function setSchedaSubTab(string $subTab): void
{
$this->schedaSubTab = $subTab;
}
public function resetFiltri(): void
{
$this->search = '';
$this->filterStato = '';
$this->filterOrigine = '';
$this->dataDa = '';
$this->dataA = '';
$this->scope = 'tutte';
$this->refreshData(); $this->refreshData();
} }
@ -126,6 +185,14 @@ public function refreshData(): void
} }
[$fornitore, $dipendente] = $this->resolveOperatoreContext($this->fornitoreId); [$fornitore, $dipendente] = $this->resolveOperatoreContext($this->fornitoreId);
if (! $fornitore instanceof Fornitore) {
$fornitore = Fornitore::query()->find($this->fornitoreId);
}
if (! $fornitore instanceof Fornitore) {
$this->rows = [];
return;
}
$baseQuery = $this->buildBaseQuery($fornitore, $dipendente); $baseQuery = $this->buildBaseQuery($fornitore, $dipendente);
@ -146,17 +213,20 @@ public function refreshData(): void
} }
$ticketRows = $baseQuery $ticketRows = $baseQuery
->limit(120) ->limit(150)
->get() ->get()
->map(function (TicketIntervento $intervento): array { ->map(function (TicketIntervento $intervento): array {
$base = $this->buildInterventoRow($intervento); $base = $this->buildInterventoRow($intervento);
$isClosed = $this->isClosedStatus((string) $intervento->stato);
return array_merge($base, [ return array_merge($base, [
'row_type' => 'ticket', 'row_type' => 'ticket',
'row_class' => $this->resolveTicketRowClass((string) $intervento->stato), 'row_class' => $this->resolveTicketRowClass((string) $intervento->stato),
'badge_class' => $this->resolveTicketBadgeClass((string) $intervento->stato), 'badge_class' => $this->resolveTicketBadgeClass((string) $intervento->stato),
'is_closed' => $this->isClosedStatus((string) $intervento->stato), 'row_color' => $isClosed ? 'green' : 'blue',
'is_closed' => $isClosed,
'numero' => (string) $intervento->ticket_id, 'numero' => (string) $intervento->ticket_id,
'numero_scheda'=> (string) $intervento->ticket_id,
'cliente' => $base['contatto'], 'cliente' => $base['contatto'],
'difetto' => $base['problema'], 'difetto' => $base['problema'],
'brand' => $this->extractDeviceBrand($base['apparato']), 'brand' => $this->extractDeviceBrand($base['apparato']),
@ -167,6 +237,7 @@ public function refreshData(): void
'ticket_id' => (int) $intervento->ticket_id, 'ticket_id' => (int) $intervento->ticket_id,
'titolo' => (string) ($intervento->ticket->titolo ?? '-'), 'titolo' => (string) ($intervento->ticket->titolo ?? '-'),
'stato' => (string) $intervento->stato, 'stato' => (string) $intervento->stato,
'status_code' => (string) $intervento->stato,
'stabile' => (string) ($intervento->ticket->stabile->denominazione ?? '-'), 'stabile' => (string) ($intervento->ticket->stabile->denominazione ?? '-'),
'operatore' => (string) $intervento->operatore_assegnato_label, 'operatore' => (string) $intervento->operatore_assegnato_label,
'tempo_minuti' => (int) ($intervento->tempo_minuti ?? 0), 'tempo_minuti' => (int) ($intervento->tempo_minuti ?? 0),
@ -187,98 +258,319 @@ public function refreshData(): void
$this->totals['aperte'] = count(array_filter($rows, fn(array $row): bool => ! (bool) ($row['is_closed'] ?? false))); $this->totals['aperte'] = count(array_filter($rows, fn(array $row): bool => ! (bool) ($row['is_closed'] ?? false)));
$this->totals['chiuse'] = count(array_filter($rows, fn(array $row): bool => (bool) ($row['is_closed'] ?? false))); $this->totals['chiuse'] = count(array_filter($rows, fn(array $row): bool => (bool) ($row['is_closed'] ?? false)));
// Scope filter
if ($this->scope === 'aperte') { if ($this->scope === 'aperte') {
$rows = array_values(array_filter($rows, fn(array $row): bool => ! (bool) ($row['is_closed'] ?? false))); $rows = array_values(array_filter($rows, fn(array $row): bool => ! (bool) ($row['is_closed'] ?? false)));
} elseif ($this->scope === 'chiuse') { } elseif ($this->scope === 'chiuse') {
$rows = array_values(array_filter($rows, fn(array $row): bool => (bool) ($row['is_closed'] ?? false))); $rows = array_values(array_filter($rows, fn(array $row): bool => (bool) ($row['is_closed'] ?? false)));
} elseif ($this->scope === 'ticket_amministratore') {
$rows = array_values(array_filter($rows, fn(array $row): bool => ($row['row_type'] ?? '') === 'ticket'));
} }
$this->rows = array_slice($rows, 0, 150); // Additional filters
if (trim($this->search) !== '') {
$term = strtolower(trim($this->search));
$rows = array_values(array_filter($rows, function (array $row) use ($term): bool {
return str_contains(strtolower((string) ($row['numero'] ?? '')), $term)
|| str_contains(strtolower((string) ($row['cliente'] ?? '')), $term)
|| str_contains(strtolower((string) ($row['telefono'] ?? '')), $term)
|| str_contains(strtolower((string) ($row['modello'] ?? '')), $term)
|| str_contains(strtolower((string) ($row['seriale'] ?? '')), $term)
|| str_contains(strtolower((string) ($row['difetto'] ?? '')), $term)
|| str_contains(strtolower((string) ($row['brand'] ?? '')), $term)
|| str_contains(strtolower((string) ($row['cod_prodotto'] ?? '')), $term);
}));
}
if (trim($this->filterStato) !== '' && $this->filterStato !== '0') {
$rows = array_values(array_filter($rows, fn(array $r): bool => (string) ($r['status_code'] ?? '') === (string) $this->filterStato || str_contains(strtolower((string) ($r['stato'] ?? '')), strtolower($this->filterStato))));
}
if (trim($this->filterOrigine) !== '') {
$rows = array_values(array_filter($rows, fn(array $r): bool => ($r['row_type'] ?? '') === $this->filterOrigine));
}
$this->rows = array_slice($rows, 0, 160);
} }
public function openRowDetail(string $rowType, int $rowId): void public function openRowDetail(string $rowType, int $rowId): void
{ {
if (! $this->fornitoreId) { $this->selectedRowType = $rowType;
return; $this->selectedRowId = $rowId;
}
if ($rowType === 'tecnorepair') { if ($rowType === 'tecnorepair') {
$scheda = AssistenzaTecnorepairScheda::query() $scheda = AssistenzaTecnorepairScheda::query()->find($rowId);
->where('fornitore_id', (int) $this->fornitoreId)
->find($rowId);
if (! $scheda instanceof AssistenzaTecnorepairScheda) { if (! $scheda instanceof AssistenzaTecnorepairScheda) {
return; return;
} }
$metadata = is_array($scheda->metadata ?? null) ? $scheda->metadata : []; $metadata = is_array($scheda->metadata ?? null) ? $scheda->metadata : [];
$apparecchio = array_filter([ $raw = is_array($metadata['raw'] ?? null) ? $metadata['raw'] : [];
'Numero scheda' => (string) ($scheda->legacy_numero_scheda ?: $scheda->legacy_id ?: '-'), $cliente = is_array($metadata['cliente'] ?? null) ? $metadata['cliente'] : [];
'Codice prodotto' => (string) ($scheda->product_code ?: '-'),
'Modello' => (string) ($scheda->product_model ?: '-'),
'Seriale 1' => (string) ($scheda->serial_number ?: '-'),
'Seriale 2' => (string) ($scheda->serial_number_2 ?: '-'),
'Numero ordine' => (string) ($scheda->order_number ?: '-'),
'RMA' => (string) ($scheda->rma_code ?: '-'),
'PIN' => (string) ($scheda->pin_code ?: '-'),
'Unlock' => (string) ($scheda->unlock_code ?: '-'),
], static fn(string $value): bool => trim($value) !== '');
$this->detailModal = [ // Segno di sblocco (pattern grid 3x3)
'type' => 'tecnorepair', $segnoStr = trim((string) ($raw['SegnoDiSblocco'] ?? ''));
'title' => (string) $scheda->display_title, $segnoParts = preg_split('/\s+/', $segnoStr);
'status' => (string) ($scheda->status_label ?: $scheda->status_bucket ?: '-'), $pattern = [0, 0, 0, 0, 0, 0, 0, 0, 0];
'customer' => (string) ($scheda->customer_name ?: 'Cliente TecnoRepair'), if (is_array($segnoParts) && count($segnoParts) >= 9) {
'phone' => (string) ($scheda->customer_phone ?: $scheda->customer_phone_alt ?: ''), for ($i = 0; $i < 9; $i++) {
'email' => (string) ($scheda->customer_email ?: ''), $pattern[$i] = (int) ($segnoParts[$i] ?? 0);
'difetto_segnalato' => (string) ($scheda->defect_reported ?: '-'), }
'note_interne' => trim((string) (($metadata['NoteInterne'] ?? $metadata['note_interne'] ?? $scheda->communications) ?: '')), }
'note_stampa' => trim((string) (($metadata['NoteStampa'] ?? $metadata['note_stampa'] ?? $scheda->repair_description) ?: '')), $this->unlockPattern = $pattern;
'communications' => (string) ($scheda->communications ?: ''),
'repair_description' => (string) ($scheda->repair_description ?: ''), $this->activeScheda = [
'operator' => (string) ($scheda->technician_name ?: $scheda->operator_name ?: '-'), 'type' => 'tecnorepair',
'updated_at' => optional($scheda->updated_at)->format('d/m/Y H:i') ?: '-', 'id' => (int) $scheda->id,
'apparecchio_fields' => $apparecchio, 'legacy_id' => (int) ($scheda->legacy_id ?? 0),
'metadata' => $metadata, 'numero_scheda' => (string) ($scheda->legacy_numero_scheda ?: $scheda->legacy_id ?: '-'),
'title' => (string) $scheda->display_title,
'status' => (string) ($scheda->status_label ?: $scheda->status_bucket ?: '-'),
'status_code' => (string) ($scheda->status_code ?: '21'),
'customer_name' => (string) ($scheda->customer_name ?: ($cliente['NomeCognome'] ?? 'Cliente TecnoRepair')),
'customer_phone' => (string) ($scheda->customer_phone ?: ($cliente['NumeroTelefono'] ?? '')),
'customer_phone_alt' => (string) ($scheda->customer_phone_alt ?: ($cliente['TelFisso'] ?? '')),
'customer_email' => (string) ($scheda->customer_email ?: ($cliente['Email'] ?? '')),
'customer_address' => (string) ($cliente['Indirizzo'] ?? ''),
'customer_city' => (string) ($cliente['Citta'] ?? ''),
'product_type' => (string) ($raw['Cod_TipoApparecchio'] ?? 'Apparecchio Elettronico'),
'brand' => (string) ($this->extractDeviceBrand((string) ($scheda->product_model ?: ''))),
'product_model' => (string) ($scheda->product_model ?: ($raw['Modello'] ?? '-')),
'product_code' => (string) ($scheda->product_code ?: ($raw['CodiceProdotto'] ?? '-')),
'serial_number' => (string) ($scheda->serial_number ?: ($raw['SerialNumber'] ?? '-')),
'serial_number_2' => (string) ($scheda->serial_number_2 ?: ($raw['SerialNumber2'] ?? '-')),
'serial_number_3' => (string) ($raw['SerialNumber3'] ?? '-'),
'tag' => (string) ($raw['Tag'] ?? '-'),
'accessories' => (string) ($raw['AccessoriConsegnati'] ?? 'NESSUNO'),
'location' => (string) ($raw['Locazione'] ?? ($raw['Cod_Ubicazione'] ?? 'Laboratorio')),
'technician' => (string) ($scheda->technician_name ?: ($raw['NomeTecnicoRiparatore'] ?? 'Tecnico')),
'operator' => (string) ($scheda->operator_name ?: ($raw['NomeOperatore'] ?? 'Operatore')),
'defect_reported' => (string) ($scheda->defect_reported ?: ($raw['DifettoSegnalato'] ?? '-')),
'customer_requests' => (string) ($raw['RiparazioneRichiesta'] ?? '-'),
'general_state' => (string) ($raw['StatoGenerale'] ?? '-'),
'pin_code' => (string) ($scheda->pin_code ?: ($raw['CodicePIN'] ?? '-')),
'unlock_code' => (string) ($scheda->unlock_code ?: ($raw['CodiceSblocco'] ?? '-')),
'repair_description' => (string) ($scheda->repair_description ?: ($raw['DescrizioneRiparazione'] ?? '')),
'note_interne' => (string) ($raw['NoteInterne'] ?? ''),
'note_stampa' => (string) ($raw['NoteStampa'] ?? ''),
'communications' => (string) ($scheda->communications ?: ($raw['Comunicazioni'] ?? '')),
'date_received' => optional($scheda->date_received)->format('d/m/Y') ?: '-',
'time_received' => (string) ($raw['OrarioIngresso'] ?? '10:00'),
'date_delivery_prev' => ! empty($raw['DataPrevRip']) ? Carbon::parse($raw['DataPrevRip'])->format('d/m/Y') : '-',
'date_repaired' => ! empty($raw['DataRiparazione']) ? Carbon::parse($raw['DataRiparazione'])->format('d/m/Y') : '-',
'date_returned' => ! empty($raw['DataRiconsegna']) ? Carbon::parse($raw['DataRiconsegna'])->format('d/m/Y') : '-',
'is_fare_preventivo' => (bool) ($raw['isFarePreventivo'] ?? false),
'is_riparazione_sede' => (bool) ($raw['isRiparazioneInSede'] ?? false),
'is_riconsegnato' => (bool) ($raw['isRiconsegnato'] ?? false),
'is_rientro' => (bool) ($raw['Rientro'] ?? false),
'is_esame_tecnico' => (bool) ($raw['isEsameTecnico'] ?? false),
'consegnato_no_doc' => (bool) ($raw['ConsegnatoSenzaDoc'] ?? false),
'costo_preventivo' => (string) ($raw['CostoPreventivo'] ?? '0.00'),
'costo_subito' => (string) ($raw['CostoSubito'] ?? '0.00'),
'costo_addebitato' => (string) ($raw['CostoAddebitato'] ?? '0.00'),
'acconto' => (string) ($raw['AccontoRiparazione'] ?? '0.00'),
'rif_ddt_entrata' => (string) ($raw['RifDDTEntrata'] ?? '-'),
'rif_ddt_uscita' => (string) ($raw['RifDDTUscita'] ?? '-'),
'sost_marca_modello' => (string) ($raw['Sost_MarcaModello'] ?? '-'),
'sost_seriale' => (string) ($raw['Sost_SerialNumber'] ?? '-'),
'sost_stato' => (string) ($raw['Sost_StatoGenerale'] ?? '-'),
'updated_at' => optional($scheda->updated_at)->format('d/m/Y H:i') ?: '-',
'metadata' => $metadata,
]; ];
$this->schedaForm = [
'status_code' => (string) ($scheda->status_code ?: '21'),
'status_label' => (string) ($scheda->status_label ?: 'IN RIPARAZIONE'),
'is_fare_preventivo' => (bool) ($raw['isFarePreventivo'] ?? false),
'is_riparazione_sede'=> (bool) ($raw['isRiparazioneInSede'] ?? false),
'is_riconsegnato' => (bool) ($raw['isRiconsegnato'] ?? false),
'is_rientro' => (bool) ($raw['Rientro'] ?? false),
'is_esame_tecnico' => (bool) ($raw['isEsameTecnico'] ?? false),
'consegnato_no_doc' => (bool) ($raw['ConsegnatoSenzaDoc'] ?? false),
'cliente_nome' => (string) ($scheda->customer_name ?: ($cliente['NomeCognome'] ?? '')),
'cliente_telefono' => (string) ($scheda->customer_phone ?: ($cliente['NumeroTelefono'] ?? '')),
'cliente_tel_fisso' => (string) ($scheda->customer_phone_alt ?: ($cliente['TelFisso'] ?? '')),
'cliente_email' => (string) ($scheda->customer_email ?: ($cliente['Email'] ?? '')),
'cliente_indirizzo' => (string) ($cliente['Indirizzo'] ?? ''),
'cliente_citta' => (string) ($cliente['Citta'] ?? ''),
'difetto_segnalato' => (string) ($scheda->defect_reported ?: ($raw['DifettoSegnalato'] ?? '')),
'richieste_cliente' => (string) ($raw['RiparazioneRichiesta'] ?? ''),
'stato_generale' => (string) ($raw['StatoGenerale'] ?? ''),
'pin_code' => (string) ($scheda->pin_code ?: ($raw['CodicePIN'] ?? '')),
'unlock_code' => (string) ($scheda->unlock_code ?: ($raw['CodiceSblocco'] ?? '')),
'repair_description' => (string) ($scheda->repair_description ?: ($raw['DescrizioneRiparazione'] ?? '')),
'note_interne' => (string) ($raw['NoteInterne'] ?? ''),
'note_stampa' => (string) ($raw['NoteStampa'] ?? ''),
'communications' => (string) ($scheda->communications ?: ($raw['Comunicazioni'] ?? '')),
'tecnico' => (string) ($scheda->technician_name ?: ($raw['NomeTecnicoRiparatore'] ?? '')),
'costo_preventivo' => (string) ($raw['CostoPreventivo'] ?? '0.00'),
'costo_subito' => (string) ($raw['CostoSubito'] ?? '0.00'),
'costo_addebitato' => (string) ($raw['CostoAddebitato'] ?? '0.00'),
'acconto' => (string) ($raw['AccontoRiparazione'] ?? '0.00'),
];
$this->schedaSubTab = 'entrata';
$this->mainTab = 'scheda';
$this->detailModal = null;
return; return;
} }
// TICKET AMMINISTRATORE
[$fornitore, $dipendente] = $this->resolveOperatoreContext((int) $this->fornitoreId); [$fornitore, $dipendente] = $this->resolveOperatoreContext((int) $this->fornitoreId);
$intervento = $this->buildBaseQuery($fornitore, $dipendente)->find($rowId); $intervento = $this->buildBaseQuery($fornitore, $dipendente)->find($rowId);
if (! $intervento instanceof TicketIntervento) { if (! $intervento instanceof TicketIntervento) {
return; return;
} }
$base = $this->buildInterventoRow($intervento); $base = $this->buildInterventoRow($intervento);
$this->detailModal = [ $this->activeScheda = [
'type' => 'ticket', 'type' => 'ticket',
'id' => (int) $intervento->id,
'numero_scheda' => 'TK-' . (int) $intervento->ticket_id,
'title' => 'Ticket #' . (int) $intervento->ticket_id . ' · ' . (string) ($intervento->ticket->titolo ?? '-'), 'title' => 'Ticket #' . (int) $intervento->ticket_id . ' · ' . (string) ($intervento->ticket->titolo ?? '-'),
'status' => (string) $intervento->stato, 'status' => (string) $intervento->stato,
'customer' => $base['contatto'], 'status_code' => (string) $intervento->stato,
'phone' => $base['telefono'], 'customer_name' => $base['contatto'],
'email' => '', 'customer_phone' => $base['telefono'],
'difetto_segnalato' => $base['problema'], 'customer_email' => '',
'customer_address' => (string) ($intervento->ticket->stabile->indirizzo ?? ''),
'customer_city' => (string) ($intervento->ticket->stabile->citta ?? ''),
'brand' => $this->extractDeviceBrand($base['apparato']),
'product_model' => $base['apparato'],
'product_code' => '-',
'serial_number' => '-',
'stabile' => (string) ($intervento->ticket->stabile->denominazione ?? '-'),
'unita' => (string) ($intervento->ticket->unitaImmobiliare->denominazione ?? '-'),
'operator' => (string) $intervento->operatore_assegnato_label,
'defect_reported' => $base['problema'],
'repair_description' => (string) ($intervento->rapporto_fornitore ?? ''),
'note_interne' => (string) ($intervento->rapporto_fornitore ?? ''), 'note_interne' => (string) ($intervento->rapporto_fornitore ?? ''),
'note_stampa' => (string) ($intervento->ticket->descrizione ?? ''), 'note_stampa' => (string) ($intervento->ticket->descrizione ?? ''),
'communications' => '', 'communications' => '',
'repair_description' => '',
'operator' => (string) $intervento->operatore_assegnato_label,
'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-', 'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
'url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'), 'url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'),
'apparecchio_fields' => [
'Apparato' => $base['apparato'],
'Stabile' => (string) ($intervento->ticket->stabile->denominazione ?? '-'),
],
'metadata' => [], 'metadata' => [],
]; ];
$this->schedaForm = [
'status_code' => (string) $intervento->stato,
'repair_description' => (string) ($intervento->rapporto_fornitore ?? ''),
'note_interne' => (string) ($intervento->rapporto_fornitore ?? ''),
];
$this->schedaSubTab = 'entrata';
$this->mainTab = 'scheda';
$this->detailModal = null;
}
public function togglePatternDot(int $index): void
{
if (isset($this->unlockPattern[$index])) {
$this->unlockPattern[$index] = $this->unlockPattern[$index] === 1 ? 0 : 1;
}
}
public function saveActiveScheda(): void
{
if (! $this->selectedRowId) {
return;
}
if ($this->selectedRowType === 'tecnorepair') {
$scheda = AssistenzaTecnorepairScheda::query()->find($this->selectedRowId);
if (! $scheda instanceof AssistenzaTecnorepairScheda) {
return;
}
$scheda->defect_reported = $this->schedaForm['difetto_segnalato'] ?? $scheda->defect_reported;
$scheda->repair_description = $this->schedaForm['repair_description'] ?? $scheda->repair_description;
$scheda->communications = $this->schedaForm['communications'] ?? $scheda->communications;
$scheda->pin_code = $this->schedaForm['pin_code'] ?? $scheda->pin_code;
$scheda->unlock_code = $this->schedaForm['unlock_code'] ?? $scheda->unlock_code;
$scheda->technician_name = $this->schedaForm['tecnico'] ?? $scheda->technician_name;
$scheda->status_code = (string) ($this->schedaForm['status_code'] ?? $scheda->status_code);
$scheda->status_label = $this->resolveStatusLabel((string) $scheda->status_code);
$scheda->status_bucket = AssistenzaTecnorepairScheda::normalizeStatusBucket($scheda->status_label);
$metadata = is_array($scheda->metadata ?? null) ? $scheda->metadata : [];
$raw = is_array($metadata['raw'] ?? null) ? $metadata['raw'] : [];
$cliente = is_array($metadata['cliente'] ?? null) ? $metadata['cliente'] : [];
$raw['isFarePreventivo'] = ! empty($this->schedaForm['is_fare_preventivo']) ? '1' : '0';
$raw['isRiparazioneInSede'] = ! empty($this->schedaForm['is_riparazione_sede']) ? '1' : '0';
$raw['isRiconsegnato'] = ! empty($this->schedaForm['is_riconsegnato']) ? '1' : '0';
$raw['Rientro'] = ! empty($this->schedaForm['is_rientro']) ? '1' : '0';
$raw['isEsameTecnico'] = ! empty($this->schedaForm['is_esame_tecnico']) ? '1' : '0';
$raw['ConsegnatoSenzaDoc'] = ! empty($this->schedaForm['consegnato_no_doc']) ? '1' : '0';
$raw['StatoGenerale'] = (string) ($this->schedaForm['stato_generale'] ?? '');
$raw['RiparazioneRichiesta'] = (string) ($this->schedaForm['richieste_cliente'] ?? '');
$raw['NoteInterne'] = (string) ($this->schedaForm['note_interne'] ?? '');
$raw['NoteStampa'] = (string) ($this->schedaForm['note_stampa'] ?? '');
$raw['SegnoDiSblocco'] = implode(' ', $this->unlockPattern);
$cliente['NomeCognome'] = (string) ($this->schedaForm['cliente_nome'] ?? ($cliente['NomeCognome'] ?? ''));
$cliente['NumeroTelefono'] = (string) ($this->schedaForm['cliente_telefono'] ?? ($cliente['NumeroTelefono'] ?? ''));
$cliente['TelFisso'] = (string) ($this->schedaForm['cliente_tel_fisso'] ?? ($cliente['TelFisso'] ?? ''));
$cliente['Email'] = (string) ($this->schedaForm['cliente_email'] ?? ($cliente['Email'] ?? ''));
$cliente['Indirizzo'] = (string) ($this->schedaForm['cliente_indirizzo'] ?? ($cliente['Indirizzo'] ?? ''));
$cliente['Citta'] = (string) ($this->schedaForm['cliente_citta'] ?? ($cliente['Citta'] ?? ''));
$metadata['raw'] = $raw;
$metadata['cliente'] = $cliente;
$scheda->metadata = $metadata;
$scheda->save();
Notification::make()
->title('Scheda #' . ($scheda->legacy_numero_scheda ?: $scheda->legacy_id) . ' salvata con successo')
->success()
->send();
$this->openRowDetail('tecnorepair', (int) $scheda->id);
$this->refreshData();
return;
}
if ($this->selectedRowType === 'ticket') {
$intervento = TicketIntervento::query()->find($this->selectedRowId);
if ($intervento instanceof TicketIntervento) {
$intervento->rapporto_fornitore = (string) ($this->schedaForm['repair_description'] ?? $intervento->rapporto_fornitore);
if (! empty($this->schedaForm['status_code'])) {
$intervento->stato = $this->schedaForm['status_code'];
}
$intervento->save();
Notification::make()
->title('Ticket intervento salvato con successo')
->success()
->send();
$this->openRowDetail('ticket', (int) $intervento->id);
$this->refreshData();
}
}
}
public function cambiaStatoRapido(string $code): void
{
$this->schedaForm['status_code'] = $code;
if ($code === '4') { // Riparato
$this->schedaForm['is_riconsegnato'] = false;
} elseif ($code === '23') { // Rottamazione
$this->schedaForm['is_riconsegnato'] = false;
}
$this->saveActiveScheda();
}
public function closeScheda(): void
{
$this->mainTab = 'elenco';
$this->detailModal = null;
} }
public function closeDetailModal(): void public function closeDetailModal(): void
{ {
$this->detailModal = null; $this->closeScheda();
} }
public function getTicketsUrl(): string public function getTicketsUrl(): string
@ -345,10 +637,15 @@ protected function buildBaseQuery(Fornitore $fornitore, ?FornitoreDipendente $di
protected function buildTecnorepairQuery(Fornitore $fornitore): Builder protected function buildTecnorepairQuery(Fornitore $fornitore): Builder
{ {
$fornitoreIds = [(int) $fornitore->id];
if (in_array((int) $fornitore->id, [236, 359], true) || str_contains(strtoupper($fornitore->ragione_sociale), 'NETHOME')) {
$fornitoreIds = [236, 359];
}
return AssistenzaTecnorepairScheda::query() return AssistenzaTecnorepairScheda::query()
->where('fornitore_id', (int) $fornitore->id) ->whereIn('fornitore_id', $fornitoreIds)
->orderByDesc('date_received') ->orderByDesc('date_received')
->orderByDesc('updated_at'); ->orderByDesc('id');
} }
/** /**
@ -357,15 +654,30 @@ protected function buildTecnorepairQuery(Fornitore $fornitore): Builder
protected function buildTecnorepairRows(Fornitore $fornitore): array protected function buildTecnorepairRows(Fornitore $fornitore): array
{ {
return $this->buildTecnorepairQuery($fornitore) return $this->buildTecnorepairQuery($fornitore)
->limit(120) ->limit(200)
->get() ->get()
->map(function (AssistenzaTecnorepairScheda $scheda): array { ->map(function (AssistenzaTecnorepairScheda $scheda): array {
$raw = data_get($scheda->metadata, 'raw', []);
$isRiconsegnato = (bool) ($raw['isRiconsegnato'] ?? false);
$isRiparato = ((string) $scheda->status_code === '4') || (bool) ($raw['isRiparato'] ?? false);
$isClosed = (bool) ($scheda->is_closed ?? false) || $isRiconsegnato || $isRiparato || in_array((string) $scheda->status_code, ['4', '10', '23', '31', '37'], true);
$rowColor = match ((string) $scheda->status_code) {
'4' => 'green',
'10', '23' => 'red',
'2', '21' => 'blue',
'5', '6', '33' => 'amber',
default => $isClosed ? 'green' : 'blue',
};
return [ return [
'row_type' => 'tecnorepair', 'row_type' => 'tecnorepair',
'row_class' => (string) $scheda->row_classes, 'row_class' => (string) $scheda->row_classes,
'badge_class' => (string) $scheda->status_badge_classes, 'badge_class' => (string) $scheda->status_badge_classes,
'is_closed' => (bool) ($scheda->is_closed ?? false), 'row_color' => $rowColor,
'is_closed' => $isClosed,
'numero' => (string) ($scheda->legacy_numero_scheda ?: $scheda->legacy_id), 'numero' => (string) ($scheda->legacy_numero_scheda ?: $scheda->legacy_id),
'numero_scheda'=> (string) ($scheda->legacy_numero_scheda ?: $scheda->legacy_id),
'ingresso' => optional($scheda->date_received)->format('d/m/Y') ?: '-', 'ingresso' => optional($scheda->date_received)->format('d/m/Y') ?: '-',
'cliente' => (string) ($scheda->customer_name ?: 'Cliente TecnoRepair'), 'cliente' => (string) ($scheda->customer_name ?: 'Cliente TecnoRepair'),
'contatto' => (string) ($scheda->customer_name ?: 'Cliente TecnoRepair'), 'contatto' => (string) ($scheda->customer_name ?: 'Cliente TecnoRepair'),
@ -381,6 +693,7 @@ protected function buildTecnorepairRows(Fornitore $fornitore): array
'ticket_id' => null, 'ticket_id' => null,
'titolo' => (string) $scheda->display_title, 'titolo' => (string) $scheda->display_title,
'stato' => (string) ($scheda->status_label ?: $scheda->status_bucket ?: '-'), 'stato' => (string) ($scheda->status_label ?: $scheda->status_bucket ?: '-'),
'status_code' => (string) ($scheda->status_code ?: ''),
'stabile' => 'Archivio TecnoRepair', 'stabile' => 'Archivio TecnoRepair',
'operatore' => (string) ($scheda->technician_name ?: $scheda->operator_name ?: '-'), 'operatore' => (string) ($scheda->technician_name ?: $scheda->operator_name ?: '-'),
'tempo_minuti' => 0, 'tempo_minuti' => 0,
@ -420,10 +733,10 @@ protected function isClosedStatus(string $status): bool
$status = strtolower(trim($status)); $status = strtolower(trim($status));
return str_contains($status, 'chius') return str_contains($status, 'chius')
|| str_contains($status, 'fatturat') || str_contains($status, 'fatturat')
|| str_contains($status, 'riconsegn') || str_contains($status, 'riconsegn')
|| str_contains($status, 'riparato') || str_contains($status, 'riparato')
|| str_contains($status, 'acquisto'); || str_contains($status, 'acquisto');
} }
protected function extractDeviceBrand(string $label): string protected function extractDeviceBrand(string $label): string
@ -438,4 +751,22 @@ protected function extractDeviceBrand(string $label): string
return $first !== '' ? strtoupper($first) : '-'; return $first !== '' ? strtoupper($first) : '-';
} }
protected function resolveStatusLabel(string $code): string
{
return match ($code) {
'0' => 'TUTTI',
'4' => 'RIPARATO',
'21' => 'INGRESSO ACCETTAZIONE',
'2' => 'DISPOSITIVO IN RIPARAZIONE',
'5' => 'ATTESA RICAMBIO',
'6' => 'INVIATO NUOVO PREVENTIVO',
'10' => 'DISPOSITIVO IRRIPARABILE',
'23' => 'PC DA UTILIZZARE COME RICAMBI',
'33' => 'MANDATO IN GARANZIA AL PRODUTTORE',
'31' => 'ACQUISTO RICONDIZIONATO',
'37' => 'SMALTITO PER ABBANDONO',
default => 'IN LAVORAZIONE (' . $code . ')',
};
}
} }

View File

@ -835,7 +835,7 @@ public function getOperazioniProperty()
$vociMap = []; $vociMap = [];
$vociTabellaMap = []; $vociTabellaMap = [];
if (! empty($codSpe)) { if (! empty($codSpe) && Schema::connection('gescon_import')->hasTable('voc_spe')) {
$vociRows = DB::connection('gescon_import') $vociRows = DB::connection('gescon_import')
->table('voc_spe') ->table('voc_spe')
->whereIn('cod', $codSpe) ->whereIn('cod', $codSpe)
@ -866,7 +866,7 @@ public function getOperazioniProperty()
} }
$tabelleMap = []; $tabelleMap = [];
if (! empty($codTab)) { if (! empty($codTab) && Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) {
$tabelleMap = DB::connection('gescon_import') $tabelleMap = DB::connection('gescon_import')
->table('tabelle_millesimali') ->table('tabelle_millesimali')
->whereIn('cod_tabella', $codTab) ->whereIn('cod_tabella', $codTab)
@ -1528,7 +1528,7 @@ public function getConsuntivoProperty(): array
$codes = $allRows->pluck('cod_spe')->filter()->unique()->values()->all(); $codes = $allRows->pluck('cod_spe')->filter()->unique()->values()->all();
$vociMap = []; $vociMap = [];
$tabellaByCod = []; $tabellaByCod = [];
if (! empty($codes)) { if (! empty($codes) && Schema::connection('gescon_import')->hasTable('voc_spe')) {
$vocRows = DB::connection('gescon_import') $vocRows = DB::connection('gescon_import')
->table('voc_spe') ->table('voc_spe')
->whereIn('cod', $codes) ->whereIn('cod', $codes)
@ -1543,7 +1543,7 @@ public function getConsuntivoProperty(): array
$tabellaMap = []; $tabellaMap = [];
$tabellaInfo = []; $tabellaInfo = [];
if (! empty($tabCodes)) { if (! empty($tabCodes) && Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) {
$tabellaMap = DB::connection('gescon_import') $tabellaMap = DB::connection('gescon_import')
->table('tabelle_millesimali') ->table('tabelle_millesimali')
->whereIn('cod_tabella', $tabCodes) ->whereIn('cod_tabella', $tabCodes)

View File

@ -55,6 +55,8 @@ public function resolveMdbPath(?string $candidatePath = null, ?Fornitore $fornit
// Candidates for fallback // Candidates for fallback
$fallbackCandidates = [ $fallbackCandidates = [
'/mnt/cservergo/LunaSoftware_TecnoRepair/Archivi/TecnoRepairDB.mdb',
storage_path('app/tecnorepair/TecnoRepairDB.mdb'),
self::DEFAULT_LOCAL_PATH, self::DEFAULT_LOCAL_PATH,
'/home/michele/MIki/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb', '/home/michele/MIki/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb',
'/mnt/gescon-archives/TecnoRepairDB.mdb', '/mnt/gescon-archives/TecnoRepairDB.mdb',

View File

@ -786,10 +786,8 @@ class="inline-flex items-center gap-1 rounded-lg border border-amber-300 bg-ambe
</thead> </thead>
<tbody class="divide-y divide-slate-100 bg-white"> <tbody class="divide-y divide-slate-100 bg-white">
@forelse($canoniRighe as $r) @forelse($canoniRighe as $r)
@php @php($d = (float)($r['totale_dovuto'] ?? 0))
$d = (float)($r['totale_dovuto'] ?? 0); @php($p = (float)($r['totale_pagato'] ?? 0))
$p = (float)($r['totale_pagato'] ?? 0);
@endphp
<tr class="hover:bg-slate-50/70 transition"> <tr class="hover:bg-slate-50/70 transition">
<td class="px-3 py-2 font-medium text-slate-800">{{ $r['mese_descrizione'] ?? ($r['mese'] ?? '') }} {{ $r['anno'] ?? '' }}</td> <td class="px-3 py-2 font-medium text-slate-800">{{ $r['mese_descrizione'] ?? ($r['mese'] ?? '') }} {{ $r['anno'] ?? '' }}</td>
<td class="px-3 py-2 text-slate-600"> <td class="px-3 py-2 text-slate-600">

View File

@ -1,78 +1,65 @@
# CURRENT-205 # CURRENT-205
TASK_ID: task-email-service-password-reset TASK_ID: task-lavorazioni-tecnorepair-tab-sync
MACHINE: .205 MACHINE: .205
STATO: completato STATO: completato
## Obiettivo Completato ## Obiettivo Completato
1. **Attivazione e Diagnostica Servizio Posta SMTP (Google Workspace / Gmail)**: 1. **Risoluzione Errore HTTP 500 Blade Lexer (`ParseError: unexpected token '=' / 'endforeach'`)**:
- Configurato e bonificato `.env` con credenziali dedicate Google Workspace: - Diagnosi: in `resources/views/filament/pages/affitti/gestione-affitti.blade.php`, un blocco `@php ... @endphp` multiriga dentro un `@forelse` entrava in conflitto con la regex interna di Blade `/(?<!@)@php(.*?)@endphp/s`. La regex inglobava 19.000 caratteri da una precedente direttiva `@php(...)`, lasciando orfano `@empty` e mandando il contatore `forElseCounter` a `-1`, scatenando `$__empty_-1 = true;` in compilazioni successive.
- `MAIL_MAILER=smtp` - Soluzione: convertite le righe a direttive inline `@php($d = ...)` e `@php($p = ...)`.
- `MAIL_HOST=smtp.gmail.com` - Verifica: `php artisan view:clear && php artisan view:cache` -> **Blade templates cached successfully con zero errori**.
- `MAIL_PORT=587`
- `MAIL_ENCRYPTION=tls`
- `MAIL_USERNAME=netgescon@gmail.com`
- `MAIL_FROM_ADDRESS=michele@netgescon.it` (NetGescon)
- Risolto il blocco delle email in coda: impostato `QUEUE_CONNECTION=sync` ed eliminata la dipendenza da daemon `queue:work` non presenti a sistema.
- Svuotata la tabella `jobs` evadendo le notifiche pendenti.
2. **Notifica di Reimpostazione Password Personalizzata in Italiano (`NetGesconResetPasswordNotification`)**: 2. **Diagnosi e Risoluzione Discrepanza Schede TecnoRepair (1831 vs 1876)**:
- Creata la notifica `App\Notifications\NetGesconResetPasswordNotification`: - Diagnosi: il file locale di staging `screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb` era uno snapshot datato 1 aprile 2026 fermo all'ID 1834.
- Invio sincrono immediato (senza code asincrone). - Connessione al server di rete: montata la share live Windows `//192.168.0.36/CServerGO` in `/mnt/cservergo` con credenziali dedicate e persistita in `/etc/fstab`.
- Testo in italiano, grafica formattata, pulsante "Reimposta Password", token univoco e link per copia-incolla con scadenza 60 minuti. - Il database live contiene le schede aggiornate ad oggi (fino a ID legacy 1879, inclusa la #1876).
- Mittente ufficiale `michele@netgescon.it` (NetGescon). - Aggiornato `TecnoRepairArchiveService.php` per dare precedenza al percorso live `/mnt/cservergo/LunaSoftware_TecnoRepair/Archivi/TecnoRepairDB.mdb`.
- Registrato il binding in `AppServiceProvider` su `\Filament\Auth\Notifications\ResetPassword`. - Eseguito import: `php artisan tecnorepair:import-legacy 13` -> **1874 schede importate (max legacy_id 1879, inclusa scheda 1876)**.
- Agganciato il metodo `sendPasswordResetNotification()` su `App\Models\User`.
3. **Autonomia Cambio Password Utente**: 3. **Chiarimento Architettura di Sincronizzazione (Sviluppo .205, Gitea e Produzione Cloud)**:
- **Da non autenticato**: abilitato il flusso completo su `/admin-filament/password-reset/request` (link "Hai dimenticato la password?" nella maschera di login). - Sviluppo `.205`: Codice locale sul branch `stabilization/205-zero`, database locale MariaDB `netgescon` e montaggi CIFS diretti verso gli archivi MDB (`/mnt/cservergo` e `/mnt/gescon-archives`).
- **Da autenticato**: abilitata la schermata profilo `/admin-filament/profile` tramite `->profile()` in `AdminFilamentPanelProvider`, consentendo il cambio password autonomo direttamente dal menu utente in alto a destra. - Repository Gitea (`git.netgescon.it:2222`): Sorgente centrale autoritativa del codice sorgente, rami, tag e migrazioni.
- Produzione (`svr-netgescon` / `192.168.0.157`, `app.netgescon.it`): Container Docker `netgescon_prod_app` con database PostgreSQL `prod_pgsql` (374 migrazioni attive). Riceve gli aggiornamenti del codice e delle migrazioni da Gitea. I dati operativi di produzione (utenti, ticket web, log) risiedono su PostgreSQL.
4. **Allineamento Utenze Google Workspace / NetGescon**: 4. **Nuova UI Lavorazioni Operative (`/admin-filament/fornitore/lavorazioni`) & Scheda in TAB (Non Modal)**:
- Utenza Workspace `michele@netgescon.it` (User ID 25): attiva con ruoli `super-admin`, `amministratore`, `admin`. - Aggiornata la pagina secondo il design system moderno (KPI cards, badge di stato semantici, filtri reattivi, context toolbar).
- Utenza Google `netgescon@gmail.com` (User ID 35): registrata e attiva con ruolo `super-admin`. - Eliminato il popup modale: il dettaglio della scheda ora vive all'interno di una **TAB in-page a larghezza intera ("Scheda Apparecchio")**, consentendo di alternare liberamente tra Elenco e Scheda senza perdere stato.
- Entrambe le utenze possono richiedere e ricevere la reimpostazione password o accedere in autonomia. - Fedeltà grafica e funzionale a `01 schermata pricipale.PNG` di TecnoRepair:
- Header con Num. Scheda, Date (Ingresso, Orario, Cons. Prevista, Riconsegna), Stato Riparazione e Checkbox/Flags desktop (Fare Preventivo, Riparazione in sede, Riconsegnato, Rientro, Esame Tecnico, Ricons. No Ricevuta).
5. **Comandi Artisan di Diagnostica e Invio**: - Box "Dati Cliente" in alto a destra con pulsanti rapidi WhatsApp, Telefono ed SMS.
- `php artisan netgescon:test-mail {email?}`: invia un'email diagnostica SMTP per testare la connettività Google Workspace. - 7 Sub-Tab: `1. Apparecchio in Entrata`, `2. Riparazione & Flussi`, `3. Ricambi Utilizzati`, `4. Preventivo - Costi - DDT`, `5. Annotazioni & Cortesia`, `6. Comunicazioni`, `7. C.Q. (Controllo Qualità)`.
- `php artisan netgescon:send-password-reset {email}`: genera il token crittografato e invia il link di ripristino all'email richiesta. - Matrice grafica 3x3 del Segno di Sblocco (Android pattern grid con 9 nodi interattivi).
- Salvataggio diretto modifiche su database (`saveActiveScheda()`) e pulsante stampa ricevuta.
- Supporto integrato anche per i Ticket Amministratore nello stesso formato Scheda.
## Output del Giro Operativo ## Output del Giro Operativo
ESITO_205: riuscito ESITO_205: riuscito
TASK_ID: task-email-service-password-reset TASK_ID: task-lavorazioni-tecnorepair-tab-sync
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
BRANCH: stabilization/205-zero BRANCH: stabilization/205-zero
COMMIT: cfc6060 COMMIT: 7116305
FILE_O_AREE_TOCCATE: FILE_O_AREE_TOCCATE:
- app/Console/Commands/SendPasswordResetEmailCommand.php
- app/Console/Commands/TestMailCommand.php
- app/Notifications/NetGesconResetPasswordNotification.php
- app/Models/User.php
- app/Providers/AppServiceProvider.php
- app/Providers/Filament/AdminFilamentPanelProvider.php
- config/database.php
- .gitignore - .gitignore
- database/migrations/2025_12_22_120000_add_conto_id_to_contabilita_movimenti_banca.php - app/Filament/Pages/Condomini/NominativiStabile.php
- database/migrations/2025_12_22_120010_add_conto_id_to_contabilita_saldi_conti.php - app/Filament/Pages/Fornitore/LavorazioniOperative.php
- database/migrations/2026_01_05_000011_add_pagamenti_links_to_contabilita_fatture_fornitori.php - app/Filament/Pages/Gescon/Ordinarie.php
- tests/Feature/PasswordResetAndEmailServiceTest.php - app/Services/Tecnorepair/TecnoRepairArchiveService.php
- resources/views/filament/pages/affitti/gestione-affitti.blade.php
- resources/views/filament/pages/fornitore/lavorazioni-operative.blade.php
- tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php
- skill-netgescon/control-tower/CURRENT-205.md - skill-netgescon/control-tower/CURRENT-205.md
TEST_ESEGUITI: TEST_ESEGUITI:
- ./vendor/bin/pest tests/Feature/PasswordResetAndEmailServiceTest.php tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php tests/Feature/FornitoreContabilitaSerialiImportTest.php (13 passed, 67 assertions) - ./vendor/bin/pest tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php (6 passed, 54 assertions)
- ./vendor/bin/pest tests/Feature/PasswordResetAndEmailServiceTest.php 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 (46 passed, 285 assertions) - ./vendor/bin/pest tests/Feature/PasswordResetAndEmailServiceTest.php 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 (47 passed, 299 assertions, 100% pass)
- Verifica migrazioni PostgreSQL su produzione (svr-netgescon): 374/374 migrazioni eseguite (NO PENDING MIGRATIONS). - php artisan view:clear && php artisan view:cache (successo, zero errori)
- Test SMTP live su produzione: php artisan netgescon:test-mail michele@netgescon.it (email recapitata con successo).
- Test reset password live su produzione: php artisan netgescon:send-password-reset netgescon@gmail.com e michele@netgescon.it (link generati e inviati con successo).
- Test HTTP live: https://app.netgescon.it/admin-filament/login e /admin-filament/password-reset/request (HTTP 200 OK).
GATE_STATISTICS: GATE_STATISTICS:
- SMTP_GMAIL_WORKSPACE: Connessione smtp.gmail.com:587 verificata e funzionante in locale e produzione. - BLADE_COMPILATION: 100% pulita senza token inattesi.
- POSTGRESQL_PRODUCTION_MIGRATIONS: 100% completate senza errori di dialetto su svr-netgescon. - TECNOREPAIR_RECORDS: 1874 schede importate dal live MDB (/mnt/cservergo), max legacy_id 1879, inclusa #1876.
- GESCON_IMPORT_ISOLATION: Configurazione SQLite isolata con fallback esplicito, eliminando il blocco MySQL greeting packet su porta 5432. - LAVORAZIONI_TAB_UI: Nuova visualizzazione con TAB integrata in-page a 7 schede (no modal) e matrice di sblocco 3x3.
- PASSWORD_RESET_FLOW: Richiesta da login (/admin-filament/password-reset/request) e da profilo (/admin-filament/profile) operativi. - TEST_SUITE: 47 test Feature passati (299 asserzioni, 100% pass).
- SYNC_DISPATCH: Notifiche recapitate istantaneamente senza dipendenza da code background.
- TEST_SUITE: 46 test Feature passati (285 asserzioni, 100% pass).
BLOCCO_DATI: no BLOCCO_DATI: no
BLOCCO_CONTRATTO: no BLOCCO_CONTRATTO: no
RISCHI_APERTI: nessuno RISCHI_APERTI: nessuno
@ -80,6 +67,5 @@ ## Output del Giro Operativo
## Prossimo Passo per .200 (Validazione) ## Prossimo Passo per .200 (Validazione)
- Eseguire il checkout del branch `stabilization/205-zero`. - Eseguire il checkout del branch `stabilization/205-zero`.
- Eseguire la suite di test Pest (46 passed, 285 assertions). - Eseguire la suite di test Pest (47 passed, 299 assertions).
- Eseguire `php artisan netgescon:test-mail` per verificare la connettività SMTP Google. - Aprire http://192.168.0.205:8000/admin-filament/fornitore/lavorazioni e verificare il nuovo layout grafico con KPI, filtri, tabella con badge di stato e apertura della Scheda Apparecchio in formato TAB a tutta pagina con la scheda 1876.
- Verificare la ricezione del link di reset e la modifica autonoma della password.

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Filament\Pages\Fornitore\LavorazioniOperative;
use App\Filament\Pages\Fornitore\PraticheTecnorepair; use App\Filament\Pages\Fornitore\PraticheTecnorepair;
use App\Filament\Pages\Fornitore\ProdottiCatalogo; use App\Filament\Pages\Fornitore\ProdottiCatalogo;
use App\Filament\Pages\Fornitore\SerialiCatalogo; use App\Filament\Pages\Fornitore\SerialiCatalogo;
@ -237,3 +238,50 @@
expect($prodottiPage->fornitoreId)->toBe(236); expect($prodottiPage->fornitoreId)->toBe(236);
expect($prodottiPage->getPraticheUrl())->toContain('fornitore/pratiche'); expect($prodottiPage->getPraticheUrl())->toContain('fornitore/pratiche');
}); });
it('mounts LavorazioniOperative with unified KPI cards and renders in-page Scheda TAB without modal', function () {
$michele = User::where('email', 'michele@nethome.it')->first();
Auth::login($michele);
$page = new LavorazioniOperative();
$page->mount();
expect($page->fornitoreId)->toBe(236)
->and($page->mainTab)->toBe('elenco')
->and($page->totals['tutte'])->toBeGreaterThan(0)
->and(count($page->rows))->toBeGreaterThan(0);
// Find TecnoRepair row
$tecnorepairRow = collect($page->rows)->firstWhere('row_type', 'tecnorepair');
expect($tecnorepairRow)->not->toBeNull();
// Open detail -> must activate in-page TAB 'scheda', NOT a modal!
$page->openRowDetail('tecnorepair', (int) $tecnorepairRow['id']);
expect($page->mainTab)->toBe('scheda')
->and($page->detailModal)->toBeNull() // Confirms no modal popup is used
->and($page->activeScheda)->not->toBeNull()
->and($page->activeScheda['customer_name'])->not->toBeEmpty()
->and($page->schedaSubTab)->toBe('entrata');
// Test sub-tabs navigation
$page->setSchedaSubTab('riparazione');
expect($page->schedaSubTab)->toBe('riparazione');
// Test pattern unlock dot toggling
$initialDot = $page->unlockPattern[0];
$page->togglePatternDot(0);
expect($page->unlockPattern[0])->toBe($initialDot === 1 ? 0 : 1);
// Test saving modifications
$testDefect = 'TEST DIFETTO AGGIORNATO ' . uniqid();
$page->schedaForm['difetto_segnalato'] = $testDefect;
$page->saveActiveScheda();
$reloaded = AssistenzaTecnorepairScheda::find((int) $tecnorepairRow['id']);
expect($reloaded->defect_reported)->toBe($testDefect);
// Test close -> back to elenco tab
$page->closeScheda();
expect($page->mainTab)->toBe('elenco');
});