` payload (our own QR labels) * - exact barcode match on parts.barcode * - exact article match on parts.article * On match → redirect to Part edit page. */ class Scanner extends Page { protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-qr-code'; protected static ?string $navigationLabel = 'Scaner'; protected static string|\UnitEnum|null $navigationGroup = 'Depozit'; protected static ?int $navigationSort = 39; protected static ?string $title = 'Scaner cod QR / Bare'; protected string $view = 'filament.tenant.pages.scanner'; public string $manual = ''; public ?int $purchaseId = null; public array $receivedToasts = []; public function mount(): void { // Optional ?purchase=N → receipt mode: scans mark items received $purchase = request()->query('purchase'); if ($purchase && ctype_digit((string) $purchase)) { $this->purchaseId = (int) $purchase; } } public function getActivePurchase(): ?Purchase { return $this->purchaseId ? Purchase::find($this->purchaseId) : null; } public function getPendingItems(): array { if (! $this->purchaseId) return []; return PurchaseItem::where('purchase_id', $this->purchaseId) ->whereColumn('qty_received', '<', 'qty') ->orderBy('article') ->get(['id', 'article', 'name', 'qty', 'qty_received']) ->toArray(); } #[On('scanner-decoded')] public function decoded(string $text): void { $this->process(trim($text)); } public function submitManual(): void { if (trim($this->manual) === '') return; $this->process(trim($this->manual)); $this->manual = ''; } protected function process(string $code): void { // Receipt mode: increment qty_received on matching purchase item if ($this->purchaseId) { $this->markReceivedByScan($code); return; } $this->resolveAndRedirect($code); } protected function markReceivedByScan(string $code): void { $clean = str_starts_with($code, 'PART:') ? substr($code, 5) : $code; $item = PurchaseItem::where('purchase_id', $this->purchaseId) ->whereColumn('qty_received', '<', 'qty') ->where(function ($q) use ($clean, $code) { $q->where('article', $clean)->orWhere('article', $code); }) ->first(); if (! $item) { Notification::make() ->title('Articol nu se potrivește comenzii') ->body('Codul ' . $code . ' nu apare în liniile neîncasate ale acestei comenzi.') ->warning()->send(); return; } $item->qty_received = min((float) $item->qty, (float) $item->qty_received + 1); $item->save(); $this->receivedToasts[] = [ 'article' => $item->article, 'name' => $item->name, 'qty_received' => (float) $item->qty_received, 'qty_total' => (float) $item->qty, 'at' => now()->format('H:i:s'), ]; Notification::make() ->title("+1 {$item->article} — {$item->qty_received}/{$item->qty}") ->success()->send(); } protected function resolveAndRedirect(string $code): void { $clean = $code; if (str_starts_with($clean, 'PART:')) { $clean = substr($clean, 5); } $part = Part::where(function ($q) use ($clean, $code) { $q->where('article', $clean) ->orWhere('barcode', $clean) ->orWhere('barcode', $code); if (ctype_digit($clean)) $q->orWhere('id', (int) $clean); }) ->first(); if (! $part) { Notification::make() ->title('Cod necunoscut') ->body('Nu am găsit nicio piesă pentru: ' . $code) ->warning() ->send(); return; } Notification::make() ->title('Piesă găsită: ' . $part->name) ->success() ->send(); $this->redirect( route('filament.tenant.resources.parts.edit', ['record' => $part->id]) ); } }