Faza 3.2: Service modules — Norme-ore, Tehnicieni, Fișe lucru

Schema:
- users + specialization, color, hourly_rate (pentru maistri)
- labors: catalog manopere standard cu category/ore/preț (RO+RU)
- work_orders: nr unique per tenant, status workflow (9 stări),
  pay_status (3 stări), client/vehicle/master/deal/appointment refs,
  complaint/diagnosis/recommendations, total auto-calculat
- wo_works: manopere per fișă, recalc auto la save/delete
- wo_parts: piese per fișă (free-text deocamdată), discount/total auto

Filament resources (group Service):
- LaborResource: CRUD + grupare pe categorie + filter active
- WorkOrderResource: form complex în 4 secțiuni (antet, diagnostic, plată)
  + 2 RelationManagers (Works, Parts)
- MasterResource: vedere User filtrată role=mechanic, edit specializare/
  culoare calendar/tarif oră

Conversie auto: la adaugare manoperă din catalog Labor,
form populează numele + ore + preț/oră derivat (price/hours).

Number generator pentru WO: format WO-{YY}-{NNNN} per tenant per an,
calculat în CreateWorkOrder via WorkOrder::generateNumber().

Seed extins:
- 3 mecanici (Vasile/Andrei/Nicolae) cu culori + specializări
- 10 manopere standard din prototipul AutoCRM.html
- 1 fișă demo (BMW X5 plăcuțe Brembo) cu 1 manoperă + 1 piesă, total auto
This commit is contained in:
2026-05-06 21:24:07 +00:00
parent c17fb2b413
commit 51a0bab39e
24 changed files with 1112 additions and 172 deletions
@@ -0,0 +1,24 @@
<?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('users', function (Blueprint $t) {
$t->string('specialization')->nullable()->after('locale');
$t->string('color', 16)->nullable()->after('specialization');
$t->decimal('hourly_rate', 8, 2)->nullable()->after('color');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $t) {
$t->dropColumn(['specialization', 'color', 'hourly_rate']);
});
}
};
@@ -0,0 +1,37 @@
<?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('labors', function (Blueprint $t) {
$t->id();
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
$t->string('category'); // Motor / Frâne / Suspensie / ...
$t->string('name_ro'); // numele manoperei (ro)
$t->string('name_ru')->nullable();
$t->string('code', 32)->nullable(); // cod intern opțional
$t->decimal('hours', 5, 2)->default(1); // norma-oră
$t->decimal('price', 10, 2)->default(0); // preț calculat (hours * tarif companie de obicei)
$t->boolean('is_active')->default(true);
$t->text('notes')->nullable();
$t->timestamps();
$t->softDeletes();
$t->index(['company_id', 'category']);
$t->index(['company_id', 'is_active']);
});
}
public function down(): void
{
Schema::dropIfExists('labors');
}
};
@@ -0,0 +1,96 @@
<?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('work_orders', function (Blueprint $t) {
$t->id();
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
$t->string('number', 32); // WO-001 — generat per tenant
$t->foreignId('client_id')->nullable()->constrained()->nullOnDelete();
$t->foreignId('vehicle_id')->nullable()->constrained()->nullOnDelete();
$t->foreignId('master_id')->nullable()->constrained('users')->nullOnDelete();
$t->foreignId('deal_id')->nullable()->constrained()->nullOnDelete();
$t->foreignId('appointment_id')->nullable()->constrained()->nullOnDelete();
$t->date('opened_at');
$t->date('closed_at')->nullable();
$t->unsignedInteger('mileage_in')->nullable();
$t->unsignedInteger('mileage_out')->nullable();
$t->text('complaint')->nullable(); // jaluire client
$t->text('diagnosis')->nullable();
$t->text('recommendations')->nullable();
$t->string('status')->default('new');
// new / diagnosis / agreement / approved / in_work /
// awaiting_parts / ready / done / cancelled
$t->string('pay_status')->default('unpaid'); // unpaid / partial / paid
$t->boolean('approved')->default(false);
$t->timestamp('approved_at')->nullable();
$t->decimal('discount_pct', 5, 2)->default(0);
$t->decimal('total', 12, 2)->default(0); // calculat (works + parts - discount)
$t->timestamps();
$t->softDeletes();
$t->unique(['company_id', 'number']);
$t->index(['company_id', 'status']);
$t->index(['company_id', 'opened_at']);
});
Schema::create('wo_works', function (Blueprint $t) {
$t->id();
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
$t->foreignId('work_order_id')->constrained()->cascadeOnDelete();
$t->foreignId('labor_id')->nullable()->constrained()->nullOnDelete();
$t->foreignId('master_id')->nullable()->constrained('users')->nullOnDelete();
$t->string('name'); // snapshot din labor.name_ro la momentul adăugării
$t->decimal('hours', 5, 2)->default(1);
$t->decimal('price_per_hour', 10, 2)->default(0); // tarif normo-oră
$t->decimal('total', 10, 2)->default(0); // hours * price_per_hour
$t->string('status')->default('todo'); // todo / in_progress / done
$t->text('notes')->nullable();
$t->timestamps();
$t->index(['company_id', 'work_order_id']);
});
Schema::create('wo_parts', function (Blueprint $t) {
$t->id();
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
$t->foreignId('work_order_id')->constrained()->cascadeOnDelete();
$t->string('name'); // ex: "Filtru ulei MANN W811/80"
$t->string('article', 64)->nullable();
$t->string('brand', 64)->nullable();
$t->decimal('qty', 8, 2)->default(1);
$t->string('unit', 16)->default('buc');
$t->decimal('buy_price', 10, 2)->default(0);
$t->decimal('sell_price', 10, 2)->default(0);
$t->decimal('discount_pct', 5, 2)->default(0);
$t->decimal('total', 12, 2)->default(0); // qty * sell_price * (1-disc/100)
$t->string('status')->default('needed'); // needed / ordered / delivered / installed
$t->text('notes')->nullable();
$t->timestamps();
$t->index(['company_id', 'work_order_id']);
});
}
public function down(): void
{
Schema::dropIfExists('wo_parts');
Schema::dropIfExists('wo_works');
Schema::dropIfExists('work_orders');
}
};