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:
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenant;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Labor extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
public const CATEGORIES = [
|
||||
'Motor', 'Frâne', 'Suspensie', 'Anvelope', 'ITP', 'Cutie viteze',
|
||||
'Caroserie', 'Electrică', 'Climatizare', 'Eșapament', 'Altele',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'category', 'name_ro', 'name_ru', 'code',
|
||||
'hours', 'price', 'is_active', 'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'hours' => 'decimal:2',
|
||||
'price' => 'decimal:2',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
@@ -26,6 +26,7 @@ class User extends Authenticatable implements FilamentUser
|
||||
protected $fillable = [
|
||||
'company_id', 'name', 'email', 'phone', 'avatar_url',
|
||||
'role', 'status', 'locale',
|
||||
'specialization', 'color', 'hourly_rate',
|
||||
'email_verified_at', 'password', 'last_login_at',
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenant;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class WorkOrder extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
public const STATUSES = [
|
||||
'new' => 'Nou',
|
||||
'diagnosis' => 'Diagnosticare',
|
||||
'agreement' => 'Aprobare client',
|
||||
'approved' => 'Aprobat',
|
||||
'in_work' => 'În lucru',
|
||||
'awaiting_parts' => 'Așteaptă piese',
|
||||
'ready' => 'Gata de ridicare',
|
||||
'done' => 'Predat',
|
||||
'cancelled' => 'Anulat',
|
||||
];
|
||||
|
||||
public const PAY_STATUSES = [
|
||||
'unpaid' => 'Neplătit',
|
||||
'partial' => 'Parțial',
|
||||
'paid' => 'Plătit',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'number',
|
||||
'client_id', 'vehicle_id', 'master_id', 'deal_id', 'appointment_id',
|
||||
'opened_at', 'closed_at', 'mileage_in', 'mileage_out',
|
||||
'complaint', 'diagnosis', 'recommendations',
|
||||
'status', 'pay_status', 'approved', 'approved_at',
|
||||
'discount_pct', 'total',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'opened_at' => 'date',
|
||||
'closed_at' => 'date',
|
||||
'approved_at' => 'datetime',
|
||||
'approved' => 'boolean',
|
||||
'discount_pct' => 'decimal:2',
|
||||
'total' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function vehicle(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Vehicle::class);
|
||||
}
|
||||
|
||||
public function master(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'master_id');
|
||||
}
|
||||
|
||||
public function works(): HasMany
|
||||
{
|
||||
return $this->hasMany(WorkOrderWork::class);
|
||||
}
|
||||
|
||||
public function parts(): HasMany
|
||||
{
|
||||
return $this->hasMany(WorkOrderPart::class);
|
||||
}
|
||||
|
||||
public function recalcTotal(): void
|
||||
{
|
||||
$worksTotal = $this->works()->sum('total');
|
||||
$partsTotal = $this->parts()->sum('total');
|
||||
$sub = (float) $worksTotal + (float) $partsTotal;
|
||||
$disc = (float) $this->discount_pct;
|
||||
$this->total = round($sub * (1 - $disc / 100), 2);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public static function generateNumber(int $companyId): string
|
||||
{
|
||||
$year = date('y');
|
||||
$count = static::withoutGlobalScopes()
|
||||
->where('company_id', $companyId)
|
||||
->whereYear('created_at', date('Y'))
|
||||
->count();
|
||||
return sprintf('WO-%s-%04d', $year, $count + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenant;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WorkOrderPart extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'wo_parts';
|
||||
|
||||
public const STATUSES = [
|
||||
'needed' => 'Necesară',
|
||||
'ordered' => 'Comandată',
|
||||
'delivered' => 'Sosită',
|
||||
'installed' => 'Montată',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'work_order_id',
|
||||
'name', 'article', 'brand',
|
||||
'qty', 'unit', 'buy_price', 'sell_price',
|
||||
'discount_pct', 'total', 'status', 'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'qty' => 'decimal:2',
|
||||
'buy_price' => 'decimal:2',
|
||||
'sell_price' => 'decimal:2',
|
||||
'discount_pct' => 'decimal:2',
|
||||
'total' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function workOrder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkOrder::class);
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $row) {
|
||||
$sub = (float) $row->qty * (float) $row->sell_price;
|
||||
$disc = (float) $row->discount_pct;
|
||||
$row->total = round($sub * (1 - $disc / 100), 2);
|
||||
});
|
||||
static::saved(fn (self $row) => $row->workOrder?->recalcTotal());
|
||||
static::deleted(fn (self $row) => $row->workOrder?->recalcTotal());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Tenant;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WorkOrderWork extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $table = 'wo_works';
|
||||
|
||||
public const STATUSES = [
|
||||
'todo' => 'De făcut',
|
||||
'in_progress' => 'În lucru',
|
||||
'done' => 'Finalizat',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'company_id', 'work_order_id', 'labor_id', 'master_id',
|
||||
'name', 'hours', 'price_per_hour', 'total', 'status', 'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'hours' => 'decimal:2',
|
||||
'price_per_hour' => 'decimal:2',
|
||||
'total' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function workOrder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkOrder::class);
|
||||
}
|
||||
|
||||
public function labor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Labor::class);
|
||||
}
|
||||
|
||||
public function master(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'master_id');
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saving(function (self $row) {
|
||||
$row->total = round((float) $row->hours * (float) $row->price_per_hour, 2);
|
||||
});
|
||||
static::saved(fn (self $row) => $row->workOrder?->recalcTotal());
|
||||
static::deleted(fn (self $row) => $row->workOrder?->recalcTotal());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user