Stage 11 — Tire Service: tire hotel + wheel sets

Schema:
- tire_sets (client/vehicle, season, size width/profile/diameter, brand/DOT,
  rims, tread JSON per position + tread_min cache, TPMS + sensor ids, photos)
- tire_storage (location, season_label, stored/retrieved, check-in/out, fee)

Models:
- TireSet (HasMedia): sizeLabel, isStored, currentStorage, auto tread_min
- TireStorage: durationDays, isActive

Filament (new "Anvelope" nav group):
- TireSetResource: specs form + per-position tread + TPMS + photo upload;
  table with size, season badge, min tread (red < 3mm), storage status
- Check-in (location + period + fee → stored) / Check-out (→ retrieved)
- StorageRelationManager (stay history); nav badge = sets currently stored

Tests (6 new):
- sizeLabel formatting; tread_min from positions; check-in active storage;
  check-out retrieved + duration; multiple stays per set; tenant isolation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 06:33:00 +00:00
parent a1be01b0d5
commit 94938f24d7
9 changed files with 602 additions and 0 deletions
@@ -0,0 +1,73 @@
<?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::create('tire_sets', function (Blueprint $t) {
$t->id();
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
$t->foreignId('client_id')->nullable()->constrained()->nullOnDelete();
$t->foreignId('vehicle_id')->nullable()->constrained()->nullOnDelete();
$t->string('label')->nullable(); // ex "Iarnă Michelin"
$t->string('season', 16)->default('winter'); // summer / winter / allseason
// Size: 205/55 R16
$t->unsignedSmallInteger('width')->nullable(); // 205
$t->unsignedSmallInteger('profile')->nullable(); // 55
$t->unsignedSmallInteger('diameter')->nullable();// 16
$t->string('brand', 64)->nullable();
$t->string('model', 64)->nullable();
$t->string('dot_year', 8)->nullable(); // DOT week/year
$t->boolean('has_rims')->default(false);
$t->string('rim_type', 32)->nullable(); // steel / alloy
$t->json('tread')->nullable(); // {fl, fr, rl, rr} mm
$t->decimal('tread_min', 4, 1)->nullable(); // cached min for sorting/alerts
$t->boolean('tpms')->default(false);
$t->json('tpms_ids')->nullable(); // sensor ids per position
$t->string('condition', 24)->nullable(); // nou / bun / uzat / critic
$t->text('notes')->nullable();
$t->timestamps();
$t->softDeletes();
$t->index(['company_id', 'client_id']);
$t->index(['company_id', 'season']);
});
Schema::create('tire_storage', function (Blueprint $t) {
$t->id();
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
$t->foreignId('tire_set_id')->constrained()->cascadeOnDelete();
$t->string('location', 64)->nullable(); // rack/shelf code
$t->string('season_label', 32)->nullable(); // "Iarnă 2025-2026"
$t->string('status', 16)->default('stored'); // stored / retrieved
$t->dateTime('checked_in_at');
$t->dateTime('checked_out_at')->nullable();
$t->decimal('fee', 10, 2)->default(0);
$t->boolean('paid')->default(false);
$t->text('notes')->nullable();
$t->timestamps();
$t->index(['company_id', 'status']);
$t->index(['company_id', 'tire_set_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('tire_storage');
Schema::dropIfExists('tire_sets');
}
};