Stage 7+14 — Mechanic Board + Scan Center

Mechanic Workflow (Stage 7):
- /app/mechanic Filament page filtered to master_id = auth user
- Kanban 4 columns (in_work / awaiting_parts / ready / recent), each card
  shows WO#, plate, client, complaint summary, photo presence
- 2 KPI tiles (active now / closed today)
- Mobile-responsive grid (auto-fit, minmax 260px)

WarehouseService:
- issueNow(WorkOrderPart) — consume reservations immediately scoped to one
  line, without closing the WO (mechanic physically takes part now)
- returnPart(WorkOrderPart, qty?, notes?) — refund to stock as new batch
  at original buy_price, writes `return` event, capped at consumed total

WO PartsRelationManager:
- "Eliberează" action — visible when active reservation exists
- "Restituire" action — visible when consumed reservation exists, with qty
  modal + notes

Scan Center (Stage 14):
- PartResource "QR" action — per-part SVG QR with payload PART:<article|id>
- BulkAction "Tipărește etichete QR" → /parts/labels?ids=N,M (HTML A4 sheet,
  3-col grid, print CSS hides toolbar)
- /app/scan Filament page using html5-qrcode 2.3.8 (CDN), auto-picks back
  camera, decodes → Livewire dispatches scanner-decoded → resolveAndRedirect
- Lookup matches PART:N prefix, parts.article, parts.barcode, or numeric id
- Manual input fallback for browsers without camera

Tests (6 new):
- WarehouseIssueReturnTest (3): issueNow consumes immediately; returnPart
  creates positive batch + return event; over-return is capped
- ScannerLookupTest (3): PART: prefix lookup, raw barcode lookup, unknown miss

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 21:39:39 +00:00
parent 1ff888131f
commit e48ef1b755
13 changed files with 871 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace Tests\Feature;
use App\Models\Central\Company;
use App\Models\Central\Plan;
use App\Models\Tenant\Part;
use App\Tenancy\TenantManager;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ScannerLookupTest extends TestCase
{
use RefreshDatabase;
public function test_scanner_finds_part_by_part_prefix_payload(): void
{
[$company, $part] = $this->makePart('FA-001');
// Simulate the controller-level lookup logic (Scanner::resolveAndRedirect).
$code = 'PART:FA-001';
$clean = str_starts_with($code, 'PART:') ? substr($code, 5) : $code;
$found = Part::where(function ($q) use ($clean) {
$q->where('article', $clean)->orWhere('barcode', $clean);
if (ctype_digit($clean)) $q->orWhere('id', (int) $clean);
})->first();
$this->assertNotNull($found);
$this->assertEquals($part->id, $found->id);
}
public function test_scanner_finds_part_by_raw_barcode(): void
{
[$company, $part] = $this->makePart('FA-002', '4607177921365');
$code = '4607177921365';
$found = Part::where('barcode', $code)->orWhere('article', $code)->first();
$this->assertNotNull($found);
$this->assertEquals($part->id, $found->id);
}
public function test_scanner_misses_for_unknown_code(): void
{
$this->makePart('FA-003');
$found = Part::where('article', 'GHOST-999')->orWhere('barcode', 'GHOST-999')->first();
$this->assertNull($found);
}
private function makePart(string $article, ?string $barcode = null): array
{
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
$company = Company::create([
'plan_id' => $plan->id,
'slug' => 'sc-' . uniqid(),
'name' => 'Scanner Service',
'status' => 'active',
]);
app(TenantManager::class)->setCurrent($company);
$part = Part::create([
'name' => 'Filtru ' . $article,
'article' => $article,
'barcode' => $barcode,
'unit' => 'buc',
'qty' => 0,
'buy_price' => 0, 'sell_price' => 0,
'is_active' => true,
]);
return [$company, $part];
}
}