Add supplier workflow memory and provider search fixes
This commit is contained in:
parent
c3efa1754b
commit
baaadd660b
|
|
@ -34,13 +34,23 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ
|
||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$mdbPath = realpath((string) $this->option('mdb')) ?: (string) $this->option('mdb');
|
$rawMdbPath = trim((string) $this->option('mdb'));
|
||||||
|
$mdbPath = $rawMdbPath !== '' && file_exists($rawMdbPath)
|
||||||
|
? (realpath($rawMdbPath) ?: $rawMdbPath)
|
||||||
|
: $rawMdbPath;
|
||||||
|
|
||||||
if ($mdbPath === '' || ! is_file($mdbPath)) {
|
if ($mdbPath === '' || ! is_file($mdbPath)) {
|
||||||
$this->error('Archivio MDB non trovato: ' . $mdbPath);
|
$this->error('Archivio MDB non trovato: ' . $mdbPath);
|
||||||
|
|
||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! is_readable($mdbPath)) {
|
||||||
|
$this->error('Archivio MDB non leggibile: ' . $mdbPath);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
$fornitore = $this->resolveFornitore($admin, (string) $this->option('fornitore-id'), (string) $this->option('fornitore-term'));
|
$fornitore = $this->resolveFornitore($admin, (string) $this->option('fornitore-id'), (string) $this->option('fornitore-term'));
|
||||||
$limit = max(0, (int) $this->option('limit'));
|
$limit = max(0, (int) $this->option('limit'));
|
||||||
$dryRun = (bool) $this->option('dry-run');
|
$dryRun = (bool) $this->option('dry-run');
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,15 @@
|
||||||
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
||||||
use App\Filament\Pages\Gescon\FornitoreScheda;
|
use App\Filament\Pages\Gescon\FornitoreScheda;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
|
use App\Models\FornitoreStabileImpostazione;
|
||||||
|
use App\Models\Stabile;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Support\ArchivioPaths;
|
use App\Support\ArchivioPaths;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
use Livewire\WithFileUploads;
|
use Livewire\WithFileUploads;
|
||||||
|
|
@ -53,6 +56,19 @@ class ImpostazioniArchivio extends Page
|
||||||
/** @var array<string, mixed>|null */
|
/** @var array<string, mixed>|null */
|
||||||
public ?array $googleWorkspaceStatus = null;
|
public ?array $googleWorkspaceStatus = null;
|
||||||
|
|
||||||
|
public bool $operationalConfigAvailable = false;
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
public array $stabileOptions = [];
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
|
public array $stabileAccessRows = [];
|
||||||
|
|
||||||
|
public string $dispatchBoardNote = '';
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
|
public array $preferredMaterialRows = [];
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
|
|
@ -167,6 +183,8 @@ public function getProdottiUrl(): string
|
||||||
public function getGoogleConnectUrl(): string
|
public function getGoogleConnectUrl(): string
|
||||||
{
|
{
|
||||||
return route('oauth.google.redirect', [
|
return route('oauth.google.redirect', [
|
||||||
|
'account_key' => 'fornitore-' . (int) ($this->fornitore?->id ?? 0),
|
||||||
|
'label' => trim((string) ($this->fornitore?->ragione_sociale ?? '')) ?: ('Fornitore #' . (int) ($this->fornitore?->id ?? 0)),
|
||||||
'return_to' => request()->getRequestUri(),
|
'return_to' => request()->getRequestUri(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -174,14 +192,161 @@ public function getGoogleConnectUrl(): string
|
||||||
public function getGoogleDisconnectUrl(): string
|
public function getGoogleDisconnectUrl(): string
|
||||||
{
|
{
|
||||||
return route('oauth.google.disconnect', [
|
return route('oauth.google.disconnect', [
|
||||||
|
'account_key' => 'fornitore-' . (int) ($this->fornitore?->id ?? 0),
|
||||||
'return_to' => request()->getRequestUri(),
|
'return_to' => request()->getRequestUri(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addStabileAccessRow(): void
|
||||||
|
{
|
||||||
|
$unusedStableId = null;
|
||||||
|
$usedIds = collect($this->stabileAccessRows)
|
||||||
|
->pluck('stabile_id')
|
||||||
|
->filter(fn($value): bool => (int) $value > 0)
|
||||||
|
->map(fn($value): int => (int) $value)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
foreach (array_keys($this->stabileOptions) as $stabileId) {
|
||||||
|
if (! in_array((int) $stabileId, $usedIds, true)) {
|
||||||
|
$unusedStableId = (int) $stabileId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stabileAccessRows[] = [
|
||||||
|
'stabile_id' => $unusedStableId,
|
||||||
|
'chi_suonare' => '',
|
||||||
|
'contatto_nome' => '',
|
||||||
|
'contatto_telefono' => '',
|
||||||
|
'dove_sono_chiavi' => '',
|
||||||
|
'istruzioni_accesso' => '',
|
||||||
|
'fascia_reperibilita' => '',
|
||||||
|
'note_urgenza' => '',
|
||||||
|
'is_priority' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeStabileAccessRow(int $index): void
|
||||||
|
{
|
||||||
|
unset($this->stabileAccessRows[$index]);
|
||||||
|
$this->stabileAccessRows = array_values($this->stabileAccessRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPreferredMaterialRow(): void
|
||||||
|
{
|
||||||
|
$this->preferredMaterialRows[] = [
|
||||||
|
'label' => '',
|
||||||
|
'brand_model' => '',
|
||||||
|
'barcode' => '',
|
||||||
|
'internal_code' => '',
|
||||||
|
'preferred_source' => '',
|
||||||
|
'preferred_url' => '',
|
||||||
|
'default_qty' => 1,
|
||||||
|
'last_price' => null,
|
||||||
|
'currency' => 'EUR',
|
||||||
|
'note' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removePreferredMaterialRow(int $index): void
|
||||||
|
{
|
||||||
|
unset($this->preferredMaterialRows[$index]);
|
||||||
|
$this->preferredMaterialRows = array_values($this->preferredMaterialRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveOperationalWorkflow(): void
|
||||||
|
{
|
||||||
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedAccessRows = collect($this->stabileAccessRows)
|
||||||
|
->map(fn(array $row): ?array => $this->normalizeStabileAccessRow($row))
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$selectedStableIds = collect($normalizedAccessRows)
|
||||||
|
->pluck('stabile_id')
|
||||||
|
->map(fn($value): int => (int) $value)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$existingSettings = FornitoreStabileImpostazione::query()
|
||||||
|
->where('fornitore_id', (int) $this->fornitore->id)
|
||||||
|
->get()
|
||||||
|
->keyBy(fn(FornitoreStabileImpostazione $setting): int => (int) $setting->stabile_id);
|
||||||
|
|
||||||
|
foreach ($normalizedAccessRows as $row) {
|
||||||
|
$setting = $existingSettings->get((int) $row['stabile_id']) ?? new FornitoreStabileImpostazione([
|
||||||
|
'fornitore_id' => (int) $this->fornitore->id,
|
||||||
|
'stabile_id' => (int) $row['stabile_id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$meta = is_array($setting->meta ?? null) ? $setting->meta : [];
|
||||||
|
$meta['access_operativo'] = $row;
|
||||||
|
$setting->fornitore_id = (int) $this->fornitore->id;
|
||||||
|
$setting->stabile_id = (int) $row['stabile_id'];
|
||||||
|
$setting->meta = $meta;
|
||||||
|
$setting->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($existingSettings as $stabileId => $setting) {
|
||||||
|
if (in_array((int) $stabileId, $selectedStableIds, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = is_array($setting->meta ?? null) ? $setting->meta : [];
|
||||||
|
if (! array_key_exists('access_operativo', $meta)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($meta['access_operativo']);
|
||||||
|
|
||||||
|
if ($meta === [] && ! $setting->voce_spesa_default_id && ! $setting->conto_costo_default_id) {
|
||||||
|
$setting->delete();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting->meta = $meta;
|
||||||
|
$setting->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->operationalConfigAvailable) {
|
||||||
|
$config = $this->fornitore->operational_config_safe;
|
||||||
|
|
||||||
|
$dispatchBoardNote = trim($this->dispatchBoardNote);
|
||||||
|
if ($dispatchBoardNote === '') {
|
||||||
|
unset($config['dispatch_board_note']);
|
||||||
|
} else {
|
||||||
|
$config['dispatch_board_note'] = $dispatchBoardNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config['preferred_materials'] = collect($this->preferredMaterialRows)
|
||||||
|
->map(fn(array $row): ?array => $this->normalizePreferredMaterialRow($row))
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->fornitore->operational_config = $config;
|
||||||
|
$this->fornitore->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->refreshArchiveSummary();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Workflow operativo aggiornato')
|
||||||
|
->body($this->operationalConfigAvailable
|
||||||
|
? 'Chiavi, reperibilità e memoria materiali sono stati salvati.'
|
||||||
|
: 'Chiavi e reperibilità per stabile salvate. Esegui la migrazione per attivare anche i preferiti materiali.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
private function refreshArchiveSummary(): void
|
private function refreshArchiveSummary(): void
|
||||||
{
|
{
|
||||||
$relativeBase = $this->getArchiveRelativeBase();
|
$relativeBase = $this->getArchiveRelativeBase();
|
||||||
$absoluteBase = $relativeBase ? storage_path('app/' . $relativeBase) : null;
|
$absoluteBase = $relativeBase ? storage_path('app/' . $relativeBase) : null;
|
||||||
|
$this->operationalConfigAvailable = Schema::hasColumn('fornitori', 'operational_config');
|
||||||
|
|
||||||
if ($relativeBase !== null) {
|
if ($relativeBase !== null) {
|
||||||
foreach ([
|
foreach ([
|
||||||
|
|
@ -222,16 +387,154 @@ private function refreshArchiveSummary(): void
|
||||||
|
|
||||||
$stats = $this->fornitore?->tecnorepair_stats ?? ['total' => 0, 'open' => 0, 'closed' => 0, 'serials' => 0];
|
$stats = $this->fornitore?->tecnorepair_stats ?? ['total' => 0, 'open' => 0, 'closed' => 0, 'serials' => 0];
|
||||||
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus();
|
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus();
|
||||||
|
$this->loadOperationalWorkflowSettings();
|
||||||
$this->moduleRows = [
|
$this->moduleRows = [
|
||||||
['module' => 'Rubrica fornitore', 'status' => 'pronto', 'note' => 'Vista fornitore filtrata ma agganciata all\'anagrafica unica.'],
|
['module' => 'Rubrica fornitore', 'status' => 'pronto', 'note' => 'Vista fornitore filtrata ma agganciata all\'anagrafica unica.'],
|
||||||
['module' => 'Google Workspace', 'status' => data_get($this->googleWorkspaceStatus, 'connected', false) ? 'collegato' : 'da collegare', 'note' => 'Rubrica e sincronizzazioni smartphone riusano l\'account Google del contesto amministratore.'],
|
['module' => 'Google Workspace', 'status' => data_get($this->googleWorkspaceStatus, 'connected', false) ? 'collegato' : 'da collegare', 'note' => 'Rubrica e sincronizzazioni smartphone riusano l\'account Google del contesto amministratore.'],
|
||||||
['module' => 'TecnoRepair MDB', 'status' => ((int) ($stats['total'] ?? 0) > 0) ? 'importato' : 'da importare', 'note' => 'Schede: ' . (int) ($stats['total'] ?? 0) . ' · aperte: ' . (int) ($stats['open'] ?? 0) . ' · chiuse: ' . (int) ($stats['closed'] ?? 0)],
|
['module' => 'TecnoRepair MDB', 'status' => ((int) ($stats['total'] ?? 0) > 0) ? 'importato' : 'da importare', 'note' => 'Schede: ' . (int) ($stats['total'] ?? 0) . ' · aperte: ' . (int) ($stats['open'] ?? 0) . ' · chiuse: ' . (int) ($stats['closed'] ?? 0)],
|
||||||
['module' => 'Catalogo fornitore', 'status' => 'attivo', 'note' => 'Il catalogo vero resta nella pagina Prodotti.'],
|
['module' => 'Catalogo fornitore', 'status' => 'attivo', 'note' => 'Il catalogo vero resta nella pagina Prodotti.'],
|
||||||
|
['module' => 'Chiavi e reperibilità', 'status' => count($this->stabileAccessRows) > 0 ? 'configurato' : 'da configurare', 'note' => 'Istruzioni per accesso, chi suonare e dove recuperare chiavi per ciascuno stabile.'],
|
||||||
|
['module' => 'Memoria materiali', 'status' => count($this->preferredMaterialRows) > 0 ? 'attiva' : ($this->operationalConfigAvailable ? 'da compilare' : 'migrazione richiesta'), 'note' => 'Preferiti ricorrenti per lampade, ricambi e prodotti ripetuti dal fornitore.'],
|
||||||
['module' => 'Comunicazioni / Post-it', 'status' => 'predisposto', 'note' => 'Cartelle e contesto archivio pronti per agganciare chiamate, post-it ed email.'],
|
['module' => 'Comunicazioni / Post-it', 'status' => 'predisposto', 'note' => 'Cartelle e contesto archivio pronti per agganciare chiamate, post-it ed email.'],
|
||||||
['module' => 'FE e documenti', 'status' => 'predisposto', 'note' => 'Archivio separato per FE, documenti e flussi contabili del fornitore.'],
|
['module' => 'FE e documenti', 'status' => 'predisposto', 'note' => 'Archivio separato per FE, documenti e flussi contabili del fornitore.'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function loadOperationalWorkflowSettings(): void
|
||||||
|
{
|
||||||
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
$this->stabileOptions = [];
|
||||||
|
$this->stabileAccessRows = [];
|
||||||
|
$this->dispatchBoardNote = '';
|
||||||
|
$this->preferredMaterialRows = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminId = (int) ($this->fornitore->amministratore_id ?? 0);
|
||||||
|
|
||||||
|
$stabili = Stabile::query()
|
||||||
|
->when($adminId > 0, fn($query) => $query->where('amministratore_id', $adminId))
|
||||||
|
->when(Schema::hasColumn('stabili', 'attivo'), fn($query) => $query->where('attivo', true))
|
||||||
|
->orderBy('codice_stabile')
|
||||||
|
->orderBy('denominazione')
|
||||||
|
->get(['id', 'codice_stabile', 'denominazione']);
|
||||||
|
|
||||||
|
$this->stabileOptions = $stabili
|
||||||
|
->mapWithKeys(fn(Stabile $stabile): array => [
|
||||||
|
(int) $stabile->id => trim((string) ($stabile->codice_stabile ?: ('#' . $stabile->id)) . ' - ' . (string) ($stabile->denominazione ?: 'Stabile')),
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$accessRows = FornitoreStabileImpostazione::query()
|
||||||
|
->with('stabile:id,codice_stabile,denominazione')
|
||||||
|
->where('fornitore_id', (int) $this->fornitore->id)
|
||||||
|
->when($stabili->isNotEmpty(), fn($query) => $query->whereIn('stabile_id', $stabili->pluck('id')->all()))
|
||||||
|
->get()
|
||||||
|
->map(function (FornitoreStabileImpostazione $setting): ?array {
|
||||||
|
$meta = is_array($setting->meta ?? null) ? $setting->meta : [];
|
||||||
|
$access = is_array($meta['access_operativo'] ?? null) ? $meta['access_operativo'] : null;
|
||||||
|
|
||||||
|
if (! is_array($access)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'stabile_id' => (int) ($setting->stabile_id ?? 0),
|
||||||
|
'chi_suonare' => trim((string) ($access['chi_suonare'] ?? '')),
|
||||||
|
'contatto_nome' => trim((string) ($access['contatto_nome'] ?? '')),
|
||||||
|
'contatto_telefono' => trim((string) ($access['contatto_telefono'] ?? '')),
|
||||||
|
'dove_sono_chiavi' => trim((string) ($access['dove_sono_chiavi'] ?? '')),
|
||||||
|
'istruzioni_accesso' => trim((string) ($access['istruzioni_accesso'] ?? '')),
|
||||||
|
'fascia_reperibilita' => trim((string) ($access['fascia_reperibilita'] ?? '')),
|
||||||
|
'note_urgenza' => trim((string) ($access['note_urgenza'] ?? '')),
|
||||||
|
'is_priority' => (bool) ($access['is_priority'] ?? false),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->sortByDesc(fn(array $row): int => $row['is_priority'] ? 1 : 0)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->stabileAccessRows = $accessRows;
|
||||||
|
|
||||||
|
$config = $this->operationalConfigAvailable ? $this->fornitore->operational_config_safe : [];
|
||||||
|
|
||||||
|
$this->dispatchBoardNote = trim((string) ($config['dispatch_board_note'] ?? ''));
|
||||||
|
$this->preferredMaterialRows = collect((array) ($config['preferred_materials'] ?? []))
|
||||||
|
->map(function ($row): ?array {
|
||||||
|
if (! is_array($row)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => trim((string) ($row['label'] ?? '')),
|
||||||
|
'brand_model' => trim((string) ($row['brand_model'] ?? '')),
|
||||||
|
'barcode' => trim((string) ($row['barcode'] ?? '')),
|
||||||
|
'internal_code' => trim((string) ($row['internal_code'] ?? '')),
|
||||||
|
'preferred_source' => trim((string) ($row['preferred_source'] ?? '')),
|
||||||
|
'preferred_url' => trim((string) ($row['preferred_url'] ?? '')),
|
||||||
|
'default_qty' => isset($row['default_qty']) && is_numeric($row['default_qty']) ? (float) $row['default_qty'] : 1,
|
||||||
|
'last_price' => isset($row['last_price']) && is_numeric($row['last_price']) ? (float) $row['last_price'] : null,
|
||||||
|
'currency' => trim((string) ($row['currency'] ?? 'EUR')) ?: 'EUR',
|
||||||
|
'note' => trim((string) ($row['note'] ?? '')),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeStabileAccessRow(array $row): ?array
|
||||||
|
{
|
||||||
|
$stabileId = (int) ($row['stabile_id'] ?? 0);
|
||||||
|
if ($stabileId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = [
|
||||||
|
'stabile_id' => $stabileId,
|
||||||
|
'chi_suonare' => trim((string) ($row['chi_suonare'] ?? '')),
|
||||||
|
'contatto_nome' => trim((string) ($row['contatto_nome'] ?? '')),
|
||||||
|
'contatto_telefono' => trim((string) ($row['contatto_telefono'] ?? '')),
|
||||||
|
'dove_sono_chiavi' => trim((string) ($row['dove_sono_chiavi'] ?? '')),
|
||||||
|
'istruzioni_accesso' => trim((string) ($row['istruzioni_accesso'] ?? '')),
|
||||||
|
'fascia_reperibilita' => trim((string) ($row['fascia_reperibilita'] ?? '')),
|
||||||
|
'note_urgenza' => trim((string) ($row['note_urgenza'] ?? '')),
|
||||||
|
'is_priority' => (bool) ($row['is_priority'] ?? false),
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasContent = collect($normalized)
|
||||||
|
->except(['stabile_id', 'is_priority'])
|
||||||
|
->contains(fn($value): bool => is_string($value) && $value !== '');
|
||||||
|
|
||||||
|
return $hasContent ? $normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePreferredMaterialRow(array $row): ?array
|
||||||
|
{
|
||||||
|
$label = trim((string) ($row['label'] ?? ''));
|
||||||
|
$brandModel = trim((string) ($row['brand_model'] ?? ''));
|
||||||
|
$barcode = trim((string) ($row['barcode'] ?? ''));
|
||||||
|
$internalCode = trim((string) ($row['internal_code'] ?? ''));
|
||||||
|
|
||||||
|
if ($label === '' && $brandModel === '' && $barcode === '' && $internalCode === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => $label,
|
||||||
|
'brand_model' => $brandModel,
|
||||||
|
'barcode' => $barcode,
|
||||||
|
'internal_code' => $internalCode,
|
||||||
|
'preferred_source' => trim((string) ($row['preferred_source'] ?? '')),
|
||||||
|
'preferred_url' => trim((string) ($row['preferred_url'] ?? '')),
|
||||||
|
'default_qty' => isset($row['default_qty']) && is_numeric($row['default_qty']) ? max(0.01, round((float) $row['default_qty'], 2)) : 1.0,
|
||||||
|
'last_price' => isset($row['last_price']) && is_numeric($row['last_price']) ? round((float) $row['last_price'], 2) : null,
|
||||||
|
'currency' => trim((string) ($row['currency'] ?? 'EUR')) ?: 'EUR',
|
||||||
|
'note' => trim((string) ($row['note'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>|null
|
* @return array<string, mixed>|null
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,8 @@ public function getProdottiUrl(): string
|
||||||
public function getGoogleConnectUrl(): string
|
public function getGoogleConnectUrl(): string
|
||||||
{
|
{
|
||||||
return route('oauth.google.redirect', [
|
return route('oauth.google.redirect', [
|
||||||
|
'account_key' => 'fornitore-' . (int) ($this->fornitoreId ?? 0),
|
||||||
|
'label' => trim((string) ($this->fornitoreLabel ?? '')) ?: ('Fornitore #' . (int) ($this->fornitoreId ?? 0)),
|
||||||
'return_to' => request()->getRequestUri(),
|
'return_to' => request()->getRequestUri(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -154,6 +156,7 @@ public function getGoogleConnectUrl(): string
|
||||||
public function getGoogleDisconnectUrl(): string
|
public function getGoogleDisconnectUrl(): string
|
||||||
{
|
{
|
||||||
return route('oauth.google.disconnect', [
|
return route('oauth.google.disconnect', [
|
||||||
|
'account_key' => 'fornitore-' . (int) ($this->fornitoreId ?? 0),
|
||||||
'return_to' => request()->getRequestUri(),
|
'return_to' => request()->getRequestUri(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
use App\Filament\Pages\Gescon\StabileScheda;
|
use App\Filament\Pages\Gescon\StabileScheda;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
use App\Models\FornitoreDipendente;
|
use App\Models\FornitoreDipendente;
|
||||||
|
use App\Models\FornitoreStabileImpostazione;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Models\ProductIdentifier;
|
use App\Models\ProductIdentifier;
|
||||||
use App\Models\ProductOffer;
|
use App\Models\ProductOffer;
|
||||||
|
|
@ -86,6 +87,12 @@ class TicketInterventoScheda extends Page
|
||||||
/** @var array<int, array<string, mixed>> */
|
/** @var array<int, array<string, mixed>> */
|
||||||
public array $materialSuggestions = [];
|
public array $materialSuggestions = [];
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
|
public array $preferredMaterialRows = [];
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
|
public array $historicalMaterialRows = [];
|
||||||
|
|
||||||
public string $apparatoMarca = '';
|
public string $apparatoMarca = '';
|
||||||
|
|
||||||
public string $apparatoModello = '';
|
public string $apparatoModello = '';
|
||||||
|
|
@ -124,6 +131,13 @@ class TicketInterventoScheda extends Page
|
||||||
/** @var array<string, mixed>|null */
|
/** @var array<string, mixed>|null */
|
||||||
public ?array $googleWorkspaceStatus = null;
|
public ?array $googleWorkspaceStatus = null;
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
|
public array $accessSummaryRows = [];
|
||||||
|
|
||||||
|
public string $dispatchBoardNote = '';
|
||||||
|
|
||||||
|
public bool $operationalConfigAvailable = false;
|
||||||
|
|
||||||
public bool $sessionTrackingAvailable = false;
|
public bool $sessionTrackingAvailable = false;
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
|
|
@ -439,6 +453,66 @@ public function removeMateriale(int $index): void
|
||||||
$this->materialiUtilizzati = array_values($this->materialiUtilizzati);
|
$this->materialiUtilizzati = array_values($this->materialiUtilizzati);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function addPreferredMaterial(int $index): void
|
||||||
|
{
|
||||||
|
$row = $this->preferredMaterialRows[$index] ?? null;
|
||||||
|
if (! is_array($row)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->appendMaterialFromMemory($row, 'Materiale preferito aggiunto');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addHistoricalMaterial(int $index): void
|
||||||
|
{
|
||||||
|
$row = $this->historicalMaterialRows[$index] ?? null;
|
||||||
|
if (! is_array($row)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->appendMaterialFromMemory($row, 'Materiale storico aggiunto');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveCurrentMaterialAsPreferred(int $index): void
|
||||||
|
{
|
||||||
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasColumn('fornitori', 'operational_config')) {
|
||||||
|
Notification::make()->title('Migrazione richiesta')->body('Esegui la migrazione che aggiunge operational_config ai fornitori per attivare i preferiti materiali.')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = $this->materialiUtilizzati[$index] ?? null;
|
||||||
|
if (! is_array($current)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = $this->normalizePreferredMaterialConfig([
|
||||||
|
'label' => (string) ($current['descrizione'] ?? ''),
|
||||||
|
'brand_model' => '',
|
||||||
|
'barcode' => (string) ($current['barcode'] ?? ''),
|
||||||
|
'internal_code' => '',
|
||||||
|
'preferred_source' => (string) ($current['source_label'] ?? ''),
|
||||||
|
'preferred_url' => (string) ($current['external_url'] ?? ''),
|
||||||
|
'default_qty' => isset($current['qty']) ? (float) $current['qty'] : 1,
|
||||||
|
'last_price' => isset($current['unit_price']) ? (float) $current['unit_price'] : null,
|
||||||
|
'currency' => (string) ($current['currency'] ?? 'EUR'),
|
||||||
|
'note' => 'Salvato dalla scheda intervento #' . (int) $this->intervento->ticket_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($normalized === null) {
|
||||||
|
Notification::make()->title('Materiale non valido')->warning()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->persistPreferredMaterial($normalized);
|
||||||
|
$this->loadMaterialMemory();
|
||||||
|
|
||||||
|
Notification::make()->title('Materiale salvato nei preferiti')->body((string) ($normalized['label'] ?? 'Materiale'))->success()->send();
|
||||||
|
}
|
||||||
|
|
||||||
public function saveRapporto(): void
|
public function saveRapporto(): void
|
||||||
{
|
{
|
||||||
$validated = $this->validate([
|
$validated = $this->validate([
|
||||||
|
|
@ -708,6 +782,8 @@ public function getProdottiUrl(): string
|
||||||
public function getGoogleConnectUrl(): string
|
public function getGoogleConnectUrl(): string
|
||||||
{
|
{
|
||||||
return route('oauth.google.redirect', [
|
return route('oauth.google.redirect', [
|
||||||
|
'account_key' => 'fornitore-' . (int) $this->fornitore->id,
|
||||||
|
'label' => trim((string) ($this->fornitore->ragione_sociale ?? '')) ?: ('Fornitore #' . (int) $this->fornitore->id),
|
||||||
'return_to' => request()->getRequestUri(),
|
'return_to' => request()->getRequestUri(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -715,6 +791,7 @@ public function getGoogleConnectUrl(): string
|
||||||
public function getGoogleDisconnectUrl(): string
|
public function getGoogleDisconnectUrl(): string
|
||||||
{
|
{
|
||||||
return route('oauth.google.disconnect', [
|
return route('oauth.google.disconnect', [
|
||||||
|
'account_key' => 'fornitore-' . (int) $this->fornitore->id,
|
||||||
'return_to' => request()->getRequestUri(),
|
'return_to' => request()->getRequestUri(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -815,6 +892,7 @@ protected function buildStoredUploadDisplayName(int $index, bool $isImage, strin
|
||||||
protected function reloadIntervento(): void
|
protected function reloadIntervento(): void
|
||||||
{
|
{
|
||||||
$this->sessionTrackingAvailable = Schema::hasTable('ticket_intervento_sessioni');
|
$this->sessionTrackingAvailable = Schema::hasTable('ticket_intervento_sessioni');
|
||||||
|
$this->operationalConfigAvailable = Schema::hasColumn('fornitori', 'operational_config');
|
||||||
|
|
||||||
$relations = [
|
$relations = [
|
||||||
'ticket.stabile',
|
'ticket.stabile',
|
||||||
|
|
@ -919,6 +997,8 @@ protected function reloadIntervento(): void
|
||||||
]
|
]
|
||||||
: null;
|
: null;
|
||||||
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus();
|
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus();
|
||||||
|
$this->loadAccessSummary();
|
||||||
|
$this->loadMaterialMemory();
|
||||||
$this->qrToken = '';
|
$this->qrToken = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1012,6 +1092,8 @@ protected function formatGeoPoint(mixed $lat, mixed $lng, mixed $accuracy): stri
|
||||||
protected function refreshMaterialSuggestions(): void
|
protected function refreshMaterialSuggestions(): void
|
||||||
{
|
{
|
||||||
$term = trim($this->materialSearch);
|
$term = trim($this->materialSearch);
|
||||||
|
$this->loadMaterialMemory();
|
||||||
|
|
||||||
if ($term === '') {
|
if ($term === '') {
|
||||||
$this->materialSuggestions = [];
|
$this->materialSuggestions = [];
|
||||||
return;
|
return;
|
||||||
|
|
@ -1060,6 +1142,251 @@ protected function refreshMaterialSuggestions(): void
|
||||||
})->all();
|
})->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function loadAccessSummary(): void
|
||||||
|
{
|
||||||
|
$this->accessSummaryRows = [];
|
||||||
|
$this->dispatchBoardNote = '';
|
||||||
|
|
||||||
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $this->operationalConfigAvailable ? $this->fornitore->operational_config_safe : [];
|
||||||
|
$this->dispatchBoardNote = trim((string) ($config['dispatch_board_note'] ?? ''));
|
||||||
|
|
||||||
|
$stabileId = (int) ($this->intervento->ticket?->stabile_id ?? 0);
|
||||||
|
if ($stabileId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$setting = FornitoreStabileImpostazione::query()
|
||||||
|
->with('stabile:id,codice_stabile,denominazione')
|
||||||
|
->where('fornitore_id', (int) $this->fornitore->id)
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $setting instanceof FornitoreStabileImpostazione) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = is_array($setting->meta ?? null) ? $setting->meta : [];
|
||||||
|
$access = is_array($meta['access_operativo'] ?? null) ? $meta['access_operativo'] : null;
|
||||||
|
|
||||||
|
if (! is_array($access)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->accessSummaryRows = [[
|
||||||
|
'stabile_label' => trim((string) (($setting->stabile?->codice_stabile ?: ('#' . $stabileId)) . ' - ' . ($setting->stabile?->denominazione ?: 'Stabile'))),
|
||||||
|
'chi_suonare' => trim((string) ($access['chi_suonare'] ?? '')),
|
||||||
|
'contatto_nome' => trim((string) ($access['contatto_nome'] ?? '')),
|
||||||
|
'contatto_telefono' => trim((string) ($access['contatto_telefono'] ?? '')),
|
||||||
|
'dove_sono_chiavi' => trim((string) ($access['dove_sono_chiavi'] ?? '')),
|
||||||
|
'istruzioni_accesso' => trim((string) ($access['istruzioni_accesso'] ?? '')),
|
||||||
|
'fascia_reperibilita' => trim((string) ($access['fascia_reperibilita'] ?? '')),
|
||||||
|
'note_urgenza' => trim((string) ($access['note_urgenza'] ?? '')),
|
||||||
|
'is_priority' => (bool) ($access['is_priority'] ?? false),
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadMaterialMemory(): void
|
||||||
|
{
|
||||||
|
$term = trim($this->materialSearch);
|
||||||
|
$normalized = preg_replace('/[^A-Za-z0-9]+/', '', strtoupper($term)) ?? '';
|
||||||
|
|
||||||
|
$preferredRows = $this->operationalConfigAvailable
|
||||||
|
? collect((array) ($this->fornitore->operational_config_safe['preferred_materials'] ?? []))
|
||||||
|
: collect();
|
||||||
|
|
||||||
|
$this->preferredMaterialRows = $preferredRows
|
||||||
|
->map(fn($row): ?array => is_array($row) ? $this->normalizePreferredMaterialConfig($row) : null)
|
||||||
|
->filter()
|
||||||
|
->filter(fn(array $row): bool => $this->matchesMaterialSearch($row, $term, $normalized))
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$recentInterventi = TicketIntervento::query()
|
||||||
|
->where('fornitore_id', (int) $this->fornitore->id)
|
||||||
|
->where('id', '!=', (int) $this->intervento->id)
|
||||||
|
->whereNotNull('materiali_utilizzati')
|
||||||
|
->latest('updated_at')
|
||||||
|
->limit(80)
|
||||||
|
->get(['id', 'ticket_id', 'materiali_utilizzati', 'updated_at']);
|
||||||
|
|
||||||
|
$aggregated = [];
|
||||||
|
|
||||||
|
foreach ($recentInterventi as $intervento) {
|
||||||
|
foreach ((array) ($intervento->materiali_utilizzati ?? []) as $item) {
|
||||||
|
if (! is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$memory = $this->normalizePreferredMaterialConfig([
|
||||||
|
'label' => (string) ($item['descrizione'] ?? ''),
|
||||||
|
'brand_model' => '',
|
||||||
|
'barcode' => (string) ($item['barcode'] ?? ''),
|
||||||
|
'internal_code' => '',
|
||||||
|
'preferred_source' => (string) ($item['source_label'] ?? ''),
|
||||||
|
'preferred_url' => (string) ($item['external_url'] ?? ''),
|
||||||
|
'default_qty' => isset($item['qty']) ? (float) $item['qty'] : 1,
|
||||||
|
'last_price' => isset($item['unit_price']) ? (float) $item['unit_price'] : null,
|
||||||
|
'currency' => (string) ($item['currency'] ?? 'EUR'),
|
||||||
|
'note' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($memory === null || ! $this->matchesMaterialSearch($memory, $term, $normalized)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $this->buildMaterialMemoryKey($memory);
|
||||||
|
if (! isset($aggregated[$key])) {
|
||||||
|
$aggregated[$key] = array_merge($memory, [
|
||||||
|
'usage_count' => 0,
|
||||||
|
'last_used_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
|
||||||
|
'ticket_id' => (int) $intervento->ticket_id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$aggregated[$key]['usage_count']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$preferredKeys = collect($this->preferredMaterialRows)
|
||||||
|
->map(fn(array $row): string => $this->buildMaterialMemoryKey($row))
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->historicalMaterialRows = collect($aggregated)
|
||||||
|
->reject(fn(array $row, string $key): bool => in_array($key, $preferredKeys, true))
|
||||||
|
->sortByDesc(fn(array $row): int => (int) ($row['usage_count'] ?? 0))
|
||||||
|
->take(8)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function appendMaterialFromMemory(array $row, string $notificationTitle): void
|
||||||
|
{
|
||||||
|
$material = [
|
||||||
|
'product_id' => isset($row['product_id']) && is_numeric($row['product_id']) ? (int) $row['product_id'] : null,
|
||||||
|
'descrizione' => trim((string) ($row['label'] ?? '')),
|
||||||
|
'barcode' => trim((string) ($row['barcode'] ?? '')),
|
||||||
|
'qty' => isset($row['default_qty']) && is_numeric($row['default_qty']) ? (float) $row['default_qty'] : 1,
|
||||||
|
'unit_price' => isset($row['last_price']) && is_numeric($row['last_price']) ? round((float) $row['last_price'], 2) : null,
|
||||||
|
'currency' => trim((string) ($row['currency'] ?? 'EUR')) ?: 'EUR',
|
||||||
|
'source_type' => 'memory',
|
||||||
|
'source_label' => trim((string) ($row['preferred_source'] ?? 'Memoria fornitore')) ?: 'Memoria fornitore',
|
||||||
|
'external_url' => trim((string) ($row['preferred_url'] ?? '')),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($material['descrizione'] === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->materialiUtilizzati[] = $material;
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title($notificationTitle)
|
||||||
|
->body($material['descrizione'])
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function persistPreferredMaterial(array $row): void
|
||||||
|
{
|
||||||
|
if (! $this->fornitore instanceof Fornitore || ! Schema::hasColumn('fornitori', 'operational_config')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $this->fornitore->operational_config_safe;
|
||||||
|
$rows = collect((array) ($config['preferred_materials'] ?? []))
|
||||||
|
->map(fn($item): ?array => is_array($item) ? $this->normalizePreferredMaterialConfig($item) : null)
|
||||||
|
->filter()
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$key = $this->buildMaterialMemoryKey($row);
|
||||||
|
$updatedRows = $rows
|
||||||
|
->reject(fn(array $item): bool => $this->buildMaterialMemoryKey($item) === $key)
|
||||||
|
->prepend($row)
|
||||||
|
->take(20)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$config['preferred_materials'] = $updatedRows;
|
||||||
|
$this->fornitore->operational_config = $config;
|
||||||
|
$this->fornitore->save();
|
||||||
|
$this->fornitore->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizePreferredMaterialConfig(array $row): ?array
|
||||||
|
{
|
||||||
|
$label = trim((string) ($row['label'] ?? ''));
|
||||||
|
$brandModel = trim((string) ($row['brand_model'] ?? ''));
|
||||||
|
$barcode = trim((string) ($row['barcode'] ?? ''));
|
||||||
|
$internalCode = trim((string) ($row['internal_code'] ?? ''));
|
||||||
|
|
||||||
|
if ($label === '' && $brandModel === '' && $barcode === '' && $internalCode === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => $label !== '' ? $label : $brandModel,
|
||||||
|
'brand_model' => $brandModel,
|
||||||
|
'barcode' => $barcode,
|
||||||
|
'internal_code' => $internalCode,
|
||||||
|
'preferred_source' => trim((string) ($row['preferred_source'] ?? $row['source_label'] ?? '')),
|
||||||
|
'preferred_url' => trim((string) ($row['preferred_url'] ?? $row['external_url'] ?? '')),
|
||||||
|
'default_qty' => isset($row['default_qty']) && is_numeric($row['default_qty']) ? max(0.01, round((float) $row['default_qty'], 2)) : 1.0,
|
||||||
|
'last_price' => isset($row['last_price']) && is_numeric($row['last_price']) ? round((float) $row['last_price'], 2) : (isset($row['unit_price']) && is_numeric($row['unit_price']) ? round((float) $row['unit_price'], 2) : null),
|
||||||
|
'currency' => trim((string) ($row['currency'] ?? 'EUR')) ?: 'EUR',
|
||||||
|
'note' => trim((string) ($row['note'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildMaterialMemoryKey(array $row): string
|
||||||
|
{
|
||||||
|
$parts = [
|
||||||
|
trim((string) ($row['barcode'] ?? '')),
|
||||||
|
trim((string) ($row['internal_code'] ?? '')),
|
||||||
|
preg_replace('/[^A-Za-z0-9]+/', '', strtoupper((string) ($row['label'] ?? ''))) ?: '',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if ($part !== '') {
|
||||||
|
return $part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'mat-' . md5(json_encode($row));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function matchesMaterialSearch(array $row, string $term, string $normalized): bool
|
||||||
|
{
|
||||||
|
if ($term === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$haystack = mb_strtolower(implode(' ', array_filter([
|
||||||
|
(string) ($row['label'] ?? ''),
|
||||||
|
(string) ($row['brand_model'] ?? ''),
|
||||||
|
(string) ($row['barcode'] ?? ''),
|
||||||
|
(string) ($row['internal_code'] ?? ''),
|
||||||
|
(string) ($row['preferred_source'] ?? ''),
|
||||||
|
(string) ($row['note'] ?? ''),
|
||||||
|
])));
|
||||||
|
|
||||||
|
if (str_contains($haystack, mb_strtolower($term))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalized === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$barcode = preg_replace('/[^A-Za-z0-9]+/', '', strtoupper((string) ($row['barcode'] ?? ''))) ?? '';
|
||||||
|
$internalCode = preg_replace('/[^A-Za-z0-9]+/', '', strtoupper((string) ($row['internal_code'] ?? ''))) ?? '';
|
||||||
|
|
||||||
|
return $barcode === $normalized || str_contains($barcode, $normalized) || $internalCode === $normalized || str_contains($internalCode, $normalized);
|
||||||
|
}
|
||||||
|
|
||||||
protected function buildAmazonSearchUrl(string $term): string
|
protected function buildAmazonSearchUrl(string $term): string
|
||||||
{
|
{
|
||||||
$query = ['k' => trim($term)];
|
$query = ['k' => trim($term)];
|
||||||
|
|
|
||||||
|
|
@ -884,6 +884,9 @@ private function searchFornitoreMatches(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$digits = preg_replace('/\D+/', '', $raw) ?: '';
|
||||||
|
$canonicalNeedles = $this->normalizeSearchTags($raw);
|
||||||
|
|
||||||
$query = $this->getTableQuery()
|
$query = $this->getTableQuery()
|
||||||
->withCount('dipendenti')
|
->withCount('dipendenti')
|
||||||
->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [(int) ($this->selectedFornitoreId ?? 0)])
|
->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [(int) ($this->selectedFornitoreId ?? 0)])
|
||||||
|
|
@ -891,9 +894,134 @@ private function searchFornitoreMatches(): void
|
||||||
->orderBy('cognome')
|
->orderBy('cognome')
|
||||||
->orderBy('nome');
|
->orderBy('nome');
|
||||||
|
|
||||||
$this->fornitoreMatches = $this->applyFornitoreSearch($query, $raw)
|
$matches = $this->applyFornitoreSearch($query, $raw)
|
||||||
->limit(12)
|
->limit(60)
|
||||||
->get();
|
->get()
|
||||||
|
->sortByDesc(function (Fornitore $fornitore) use ($raw, $digits, $canonicalNeedles): int {
|
||||||
|
return $this->scoreFornitoreMatch($fornitore, $raw, $digits, $canonicalNeedles);
|
||||||
|
})
|
||||||
|
->take(12)
|
||||||
|
->values();
|
||||||
|
|
||||||
|
if ((int) ($this->selectedFornitoreId ?? 0) > 0 && ! $matches->contains('id', (int) $this->selectedFornitoreId)) {
|
||||||
|
$selected = $this->getTableQuery()->find((int) $this->selectedFornitoreId);
|
||||||
|
if ($selected instanceof Fornitore) {
|
||||||
|
$matches->prepend($selected);
|
||||||
|
$matches = $matches->unique('id')->take(12)->values();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->fornitoreMatches = $matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $canonicalNeedles
|
||||||
|
*/
|
||||||
|
private function scoreFornitoreMatch(Fornitore $fornitore, string $raw, string $digits, array $canonicalNeedles): int
|
||||||
|
{
|
||||||
|
$score = (int) ($fornitore->id === (int) ($this->selectedFornitoreId ?? 0) ? 1000 : 0);
|
||||||
|
|
||||||
|
$label = $this->getFornitoreLabel($fornitore);
|
||||||
|
$phone = (string) ($fornitore->telefono ?: $fornitore->cellulare);
|
||||||
|
$tags = $this->splitFornitoreTags((string) ($fornitore->tags ?? ''));
|
||||||
|
|
||||||
|
$haystack = mb_strtolower(implode(' ', array_filter([
|
||||||
|
$label,
|
||||||
|
(string) ($fornitore->email ?? ''),
|
||||||
|
$phone,
|
||||||
|
implode(', ', $tags),
|
||||||
|
(string) ($fornitore->note ?? ''),
|
||||||
|
(string) ($fornitore->partita_iva ?? ''),
|
||||||
|
(string) ($fornitore->codice_fiscale ?? ''),
|
||||||
|
])));
|
||||||
|
|
||||||
|
if ($raw !== '' && str_contains($haystack, mb_strtolower($raw))) {
|
||||||
|
$score += 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($digits !== '') {
|
||||||
|
$phoneDigits = preg_replace('/\D+/', '', $phone) ?: '';
|
||||||
|
if ($phoneDigits !== '' && str_contains($phoneDigits, $digits)) {
|
||||||
|
$score += 45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowTags = array_map(fn(string $tag): string => mb_strtolower($tag), $tags);
|
||||||
|
foreach ($canonicalNeedles as $canonicalNeedle) {
|
||||||
|
foreach ($rowTags as $rowTag) {
|
||||||
|
if ($rowTag === $canonicalNeedle) {
|
||||||
|
$score += 90;
|
||||||
|
} elseif (str_contains($rowTag, $canonicalNeedle) || str_contains($canonicalNeedle, $rowTag)) {
|
||||||
|
$score += 55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function normalizeSearchTags(string $input): array
|
||||||
|
{
|
||||||
|
$tags = [];
|
||||||
|
|
||||||
|
foreach ($this->splitFornitoreTags($input) as $tag) {
|
||||||
|
$normalized = $this->canonicalizeFornitoreTag($tag);
|
||||||
|
if ($normalized !== null) {
|
||||||
|
$tags[] = $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function splitFornitoreTags(string $value): array
|
||||||
|
{
|
||||||
|
$parts = preg_split('/[,;|\n\r\/]+/', $value) ?: [];
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(function (string $part): string {
|
||||||
|
$clean = trim($part);
|
||||||
|
$clean = preg_replace('/\s+/', ' ', $clean) ?? '';
|
||||||
|
|
||||||
|
return $clean;
|
||||||
|
}, $parts), fn(string $part): bool => $part !== ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function canonicalizeFornitoreTag(string $raw): ?string
|
||||||
|
{
|
||||||
|
$clean = trim(mb_strtolower($raw));
|
||||||
|
if ($clean === '' || mb_strlen($clean) < 3) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = [
|
||||||
|
'idr' => 'idraulico',
|
||||||
|
'idraul' => 'idraulico',
|
||||||
|
'elett' => 'elettricista',
|
||||||
|
'elettric' => 'elettricista',
|
||||||
|
'ascens' => 'ascensorista',
|
||||||
|
'puliz' => 'pulizie',
|
||||||
|
'giardin' => 'giardiniere',
|
||||||
|
'assicur' => 'assicurazione',
|
||||||
|
'manut' => 'manutenzione',
|
||||||
|
'spurgh' => 'spurghi',
|
||||||
|
'fogn' => 'spurghi',
|
||||||
|
'serr' => 'serrature',
|
||||||
|
'cald' => 'caldaia',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($map as $prefix => $normalized) {
|
||||||
|
if (str_starts_with($clean, $prefix)) {
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function cleanNullable(?string $value): ?string
|
private function cleanNullable(?string $value): ?string
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class Fornitore extends Model
|
||||||
'modalita_pagamento_predefinita',
|
'modalita_pagamento_predefinita',
|
||||||
'escludi_righe_fe',
|
'escludi_righe_fe',
|
||||||
'fe_features',
|
'fe_features',
|
||||||
|
'operational_config',
|
||||||
'telefono',
|
'telefono',
|
||||||
'telefono_country_code',
|
'telefono_country_code',
|
||||||
'cellulare',
|
'cellulare',
|
||||||
|
|
@ -58,6 +59,7 @@ class Fornitore extends Model
|
||||||
'updated_at' => 'datetime',
|
'updated_at' => 'datetime',
|
||||||
'escludi_righe_fe' => 'boolean',
|
'escludi_righe_fe' => 'boolean',
|
||||||
'fe_features' => 'array',
|
'fe_features' => 'array',
|
||||||
|
'operational_config' => 'array',
|
||||||
'is_tecnorepair_primary' => 'boolean',
|
'is_tecnorepair_primary' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -77,6 +79,22 @@ public function getFeFeaturesSafeAttribute(): array
|
||||||
return is_array($this->fe_features ?? null) ? $this->fe_features : [];
|
return is_array($this->fe_features ?? null) ? $this->fe_features : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function getOperationalConfigSafeAttribute(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (! Schema::hasColumn('fornitori', 'operational_config')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($this->operational_config ?? null) ? $this->operational_config : [];
|
||||||
|
}
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
{
|
{
|
||||||
static::creating(function (Fornitore $fornitore) {
|
static::creating(function (Fornitore $fornitore) {
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,8 @@ public function get(Amministratore $amministratore, ?string $accountKey = null):
|
||||||
}
|
}
|
||||||
|
|
||||||
$resolvedKey = $this->normalizeAccountKey((string) $accountKey);
|
$resolvedKey = $this->normalizeAccountKey((string) $accountKey);
|
||||||
if ($resolvedKey !== '' && isset($accounts[$resolvedKey])) {
|
if ($resolvedKey !== '') {
|
||||||
return $accounts[$resolvedKey];
|
return $accounts[$resolvedKey] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($accounts as $account) {
|
foreach ($accounts as $account) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('fornitori', function (Blueprint $table): void {
|
||||||
|
if (! Schema::hasColumn('fornitori', 'operational_config')) {
|
||||||
|
$table->json('operational_config')->nullable()->after('fe_features');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('fornitori', function (Blueprint $table): void {
|
||||||
|
if (Schema::hasColumn('fornitori', 'operational_config')) {
|
||||||
|
$table->dropColumn('operational_config');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -66,6 +66,152 @@
|
||||||
<div class="mt-4 whitespace-pre-wrap rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-900">{{ $lastImportOutput }}</div>
|
<div class="mt-4 whitespace-pre-wrap rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-900">{{ $lastImportOutput }}</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold">Chiavi, accesso e reperibilità</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">Per ogni stabile del fornitore puoi memorizzare chi suonare, dove sono le chiavi e le istruzioni rapide da mostrare nella scheda intervento.</div>
|
||||||
|
</div>
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="addStabileAccessRow">Aggiungi stabile</x-filament::button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="mt-4 block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Nota regia / dispatch board</span>
|
||||||
|
<textarea wire:model.defer="dispatchBoardNote" rows="3" class="w-full rounded-lg border-gray-300" placeholder="Esempio: chiamare sempre prima il portiere o l'inquilino prima di usare il pass."></textarea>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
@forelse($stabileAccessRows as $index => $row)
|
||||||
|
<div class="rounded-xl border bg-slate-50 p-4">
|
||||||
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Stabile</span>
|
||||||
|
<select wire:model.defer="stabileAccessRows.{{ $index }}.stabile_id" class="w-full rounded-lg border-gray-300">
|
||||||
|
<option value="">Seleziona stabile</option>
|
||||||
|
@foreach($stabileOptions as $stabileId => $label)
|
||||||
|
<option value="{{ $stabileId }}">{{ $label }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Chi suonare / campanello</span>
|
||||||
|
<input type="text" wire:model.defer="stabileAccessRows.{{ $index }}.chi_suonare" class="w-full rounded-lg border-gray-300" placeholder="Es. interno 3, portiere, sig. Rossi" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Referente sul posto</span>
|
||||||
|
<input type="text" wire:model.defer="stabileAccessRows.{{ $index }}.contatto_nome" class="w-full rounded-lg border-gray-300" placeholder="Nome e ruolo" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Telefono referente</span>
|
||||||
|
<input type="text" wire:model.defer="stabileAccessRows.{{ $index }}.contatto_telefono" class="w-full rounded-lg border-gray-300" placeholder="Cellulare o numero richiamabile" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Dove sono le chiavi</span>
|
||||||
|
<input type="text" wire:model.defer="stabileAccessRows.{{ $index }}.dove_sono_chiavi" class="w-full rounded-lg border-gray-300" placeholder="Es. cassetta GK-12, amministratore, portineria" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Istruzioni accesso</span>
|
||||||
|
<textarea wire:model.defer="stabileAccessRows.{{ $index }}.istruzioni_accesso" rows="3" class="w-full rounded-lg border-gray-300" placeholder="Passaggi pratici per entrare, zone comuni, allarmi, portoni, cancelli."></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Fascia reperibilità</span>
|
||||||
|
<input type="text" wire:model.defer="stabileAccessRows.{{ $index }}.fascia_reperibilita" class="w-full rounded-lg border-gray-300" placeholder="Es. 8:30-12:30 / 15:00-18:00" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Note urgenza</span>
|
||||||
|
<input type="text" wire:model.defer="stabileAccessRows.{{ $index }}.note_urgenza" class="w-full rounded-lg border-gray-300" placeholder="Es. se non risponde nessuno chiamare subito il custode" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<label class="inline-flex items-center gap-2 text-xs text-slate-600">
|
||||||
|
<input type="checkbox" wire:model.defer="stabileAccessRows.{{ $index }}.is_priority" class="rounded border-gray-300" />
|
||||||
|
<span>Mostra come accesso prioritario</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" wire:click="removeStabileAccessRow({{ $index }})" class="inline-flex items-center rounded-md bg-rose-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-rose-500">Rimuovi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-xl border border-dashed p-4 text-sm text-gray-500">Nessuna istruzione di accesso configurata. Aggiungi almeno lo stabile dove serve sapere chi suonare o dove recuperare le chiavi.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold">Memoria materiali ricorrenti</div>
|
||||||
|
<div class="mt-1 text-xs text-gray-500">Preferiti per lampade, ricambi e prodotti che il fornitore usa spesso. Verranno proposti nella scheda intervento.</div>
|
||||||
|
</div>
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="addPreferredMaterialRow">Aggiungi materiale</x-filament::button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(! $operationalConfigAvailable)
|
||||||
|
<div class="mt-4 rounded-lg border border-amber-300 bg-amber-50 p-3 text-xs text-amber-800">
|
||||||
|
La memoria materiali richiede la migrazione del database che aggiunge il campo <span class="font-mono">operational_config</span> ai fornitori.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
@forelse($preferredMaterialRows as $index => $row)
|
||||||
|
<div class="rounded-xl border bg-slate-50 p-4">
|
||||||
|
<div class="grid gap-3 md:grid-cols-2">
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Etichetta materiale</span>
|
||||||
|
<input type="text" wire:model.defer="preferredMaterialRows.{{ $index }}.label" class="w-full rounded-lg border-gray-300" placeholder="Es. Lampadina E27 scala A" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Marca / modello</span>
|
||||||
|
<input type="text" wire:model.defer="preferredMaterialRows.{{ $index }}.brand_model" class="w-full rounded-lg border-gray-300" placeholder="Es. Philips CorePro 10W" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Barcode</span>
|
||||||
|
<input type="text" wire:model.defer="preferredMaterialRows.{{ $index }}.barcode" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Codice interno</span>
|
||||||
|
<input type="text" wire:model.defer="preferredMaterialRows.{{ $index }}.internal_code" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Fonte preferita</span>
|
||||||
|
<input type="text" wire:model.defer="preferredMaterialRows.{{ $index }}.preferred_source" class="w-full rounded-lg border-gray-300" placeholder="Catalogo interno, Amazon, grossista, TecnoRepair" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">URL / riferimento acquisto</span>
|
||||||
|
<input type="text" wire:model.defer="preferredMaterialRows.{{ $index }}.preferred_url" class="w-full rounded-lg border-gray-300" placeholder="Link referral o pagina prodotto" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Quantità tipica</span>
|
||||||
|
<input type="number" step="0.01" min="0.01" wire:model.defer="preferredMaterialRows.{{ $index }}.default_qty" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<div class="grid gap-3 md:grid-cols-[1fr,120px] md:col-span-1">
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Ultimo prezzo noto</span>
|
||||||
|
<input type="number" step="0.01" min="0" wire:model.defer="preferredMaterialRows.{{ $index }}.last_price" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm">
|
||||||
|
<span class="mb-1 block font-medium">Valuta</span>
|
||||||
|
<input type="text" wire:model.defer="preferredMaterialRows.{{ $index }}.currency" class="w-full rounded-lg border-gray-300" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label class="block text-sm md:col-span-2">
|
||||||
|
<span class="mb-1 block font-medium">Nota memoria</span>
|
||||||
|
<textarea wire:model.defer="preferredMaterialRows.{{ $index }}.note" rows="2" class="w-full rounded-lg border-gray-300" placeholder="Es. usare sempre in vano scala, colore luce calda, alternativa accettata."></textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex justify-end">
|
||||||
|
<button type="button" wire:click="removePreferredMaterialRow({{ $index }})" class="inline-flex items-center rounded-md bg-rose-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-rose-500">Rimuovi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-xl border border-dashed p-4 text-sm text-gray-500">Nessun materiale preferito ancora salvato.</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-filament::button type="button" color="primary" wire:click="saveOperationalWorkflow">Salva workflow operativo</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,60 @@
|
||||||
<div class="mt-4 whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-sm text-gray-700">{{ $ticket->descrizione }}</div>
|
<div class="mt-4 whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-sm text-gray-700">{{ $ticket->descrizione }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(count($accessSummaryRows) > 0 || filled($dispatchBoardNote))
|
||||||
|
<div class="rounded-xl border border-amber-200 bg-amber-50/60 p-4">
|
||||||
|
<div class="text-sm font-semibold text-amber-900">Accesso, chiavi e reperibilità</div>
|
||||||
|
@if(filled($dispatchBoardNote))
|
||||||
|
<div class="mt-2 rounded-lg border border-amber-200 bg-white/80 p-3 text-xs text-amber-900">{{ $dispatchBoardNote }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-3 space-y-3">
|
||||||
|
@foreach($accessSummaryRows as $row)
|
||||||
|
<div class="rounded-xl border bg-white p-4 text-sm shadow-sm">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="font-medium text-slate-900">{{ $row['stabile_label'] }}</div>
|
||||||
|
@if($row['is_priority'])
|
||||||
|
<span class="rounded-full bg-amber-100 px-2 py-1 text-[11px] font-semibold text-amber-800">Prioritario</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Chi suonare</div>
|
||||||
|
<div class="mt-1">{{ $row['chi_suonare'] !== '' ? $row['chi_suonare'] : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Referente</div>
|
||||||
|
<div class="mt-1">{{ $row['contatto_nome'] !== '' ? $row['contatto_nome'] : '-' }}</div>
|
||||||
|
@if($row['contatto_telefono'] !== '')
|
||||||
|
<a href="tel:{{ preg_replace('/\s+/', '', (string) $row['contatto_telefono']) }}" class="mt-1 inline-flex text-xs font-medium text-primary-600 hover:underline">{{ $row['contatto_telefono'] }}</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Dove sono le chiavi</div>
|
||||||
|
<div class="mt-1">{{ $row['dove_sono_chiavi'] !== '' ? $row['dove_sono_chiavi'] : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Fascia reperibilità</div>
|
||||||
|
<div class="mt-1">{{ $row['fascia_reperibilita'] !== '' ? $row['fascia_reperibilita'] : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
@if($row['istruzioni_accesso'] !== '')
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-slate-500">Istruzioni accesso</div>
|
||||||
|
<div class="mt-1 whitespace-pre-wrap text-slate-700">{{ $row['istruzioni_accesso'] }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if($row['note_urgenza'] !== '')
|
||||||
|
<div class="md:col-span-2 rounded-lg border border-rose-200 bg-rose-50 p-3 text-xs text-rose-900">
|
||||||
|
<span class="font-semibold">Nota urgenza:</span> {{ $row['note_urgenza'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="text-sm font-semibold">Operativo e rapportino</div>
|
<div class="text-sm font-semibold">Operativo e rapportino</div>
|
||||||
|
|
@ -256,6 +310,54 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(count($preferredMaterialRows) > 0)
|
||||||
|
<div class="mt-4 rounded-xl border border-emerald-200 bg-white p-4">
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-emerald-700">Preferiti fornitore</div>
|
||||||
|
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
||||||
|
@foreach($preferredMaterialRows as $index => $row)
|
||||||
|
<div class="rounded-xl border border-emerald-100 bg-emerald-50/40 p-3 text-sm shadow-sm">
|
||||||
|
<div class="font-medium">{{ $row['label'] }}</div>
|
||||||
|
@if($row['brand_model'] !== '')
|
||||||
|
<div class="mt-1 text-xs text-slate-500">{{ $row['brand_model'] }}</div>
|
||||||
|
@endif
|
||||||
|
<div class="mt-1 text-xs text-slate-500">Barcode: {{ $row['barcode'] !== '' ? $row['barcode'] : 'n/d' }} · Qty tipica: {{ rtrim(rtrim(number_format((float) $row['default_qty'], 2, '.', ''), '0'), '.') }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">Fonte: {{ $row['preferred_source'] !== '' ? $row['preferred_source'] : 'Memoria fornitore' }}@if($row['last_price'] !== null) · Prezzo: {{ number_format((float) $row['last_price'], 2, ',', '.') }} {{ $row['currency'] }}@endif</div>
|
||||||
|
@if($row['note'] !== '')
|
||||||
|
<div class="mt-2 text-xs text-slate-600">{{ $row['note'] }}</div>
|
||||||
|
@endif
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<button type="button" wire:click="addPreferredMaterial({{ $index }})" class="inline-flex items-center rounded-md bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-500">Aggiungi al rapportino</button>
|
||||||
|
@if($row['preferred_url'] !== '')
|
||||||
|
<a href="{{ $row['preferred_url'] }}" target="_blank" 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">Apri riferimento</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(count($historicalMaterialRows) > 0)
|
||||||
|
<div class="mt-4 rounded-xl border border-sky-200 bg-white p-4">
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-sky-700">Storico materiali usati</div>
|
||||||
|
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
||||||
|
@foreach($historicalMaterialRows as $index => $row)
|
||||||
|
<div class="rounded-xl border border-sky-100 bg-sky-50/40 p-3 text-sm shadow-sm">
|
||||||
|
<div class="font-medium">{{ $row['label'] }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">Usato {{ $row['usage_count'] }} volte · ultimo ticket #{{ $row['ticket_id'] }} · {{ $row['last_used_at'] }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">Barcode: {{ $row['barcode'] !== '' ? $row['barcode'] : 'n/d' }}@if($row['preferred_source'] !== '') · Fonte: {{ $row['preferred_source'] }}@endif</div>
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
<button type="button" wire:click="addHistoricalMaterial({{ $index }})" class="inline-flex items-center rounded-md bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-500">Riusa nel rapportino</button>
|
||||||
|
@if($row['preferred_url'] !== '')
|
||||||
|
<a href="{{ $row['preferred_url'] }}" target="_blank" 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">Apri riferimento</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if(count($materialSuggestions) > 0)
|
@if(count($materialSuggestions) > 0)
|
||||||
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
||||||
@foreach($materialSuggestions as $suggestion)
|
@foreach($materialSuggestions as $suggestion)
|
||||||
|
|
@ -315,7 +417,10 @@
|
||||||
@else
|
@else
|
||||||
<span class="text-xs text-gray-400">Nessun riferimento esterno</span>
|
<span class="text-xs text-gray-400">Nessun riferimento esterno</span>
|
||||||
@endif
|
@endif
|
||||||
<button type="button" wire:click="removeMateriale({{ (int) $index }})" class="inline-flex items-center rounded-md bg-rose-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-rose-500">Rimuovi</button>
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button type="button" wire:click="saveCurrentMaterialAsPreferred({{ (int) $index }})" class="inline-flex items-center rounded-md bg-slate-100 px-2 py-1 text-[11px] font-medium text-slate-700 hover:bg-slate-200">Memorizza</button>
|
||||||
|
<button type="button" wire:click="removeMateriale({{ (int) $index }})" class="inline-flex items-center rounded-md bg-rose-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-rose-500">Rimuovi</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user