Files
autocrm/app/Models/Tenant/BodyshopJob.php
T
Vasyka 5e255b7b40 Stage 10 — Bodyshop / PDR / Detailing: damage map + insurance + photos
Completes the 18-stage roadmap (17/18 fully functional, 18 partial).

Schema:
- bodyshop_jobs (type body_repair/pdr/painting/detailing/ceramic/ppf/polishing,
  status workflow, insurance case fields, estimate/approved amounts)
- damage_points (zone, kind, severity) — the damage map

Models:
- BodyshopJob (HasMedia: photos_before/photos_after), auto number BS-YY-NNNN
- DamagePoint with ZONES/KINDS/SEVERITIES

Filament (new "Tinichigerie" nav group):
- BodyshopJobResource: type/status, collapsible insurance section (conditional
  fields), before/after photo upload, estimate/approved amounts
- DamagePointsRelationManager (zone + kind + colour-coded severity)
- Table with type badge, insurance flag, damage count; nav badge = open jobs

Tests (5 new):
- auto number; damage points relation; insurance fields persist;
  detailing types supported; tenant isolation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 06:49:47 +00:00

103 lines
2.8 KiB
PHP

<?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;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class BodyshopJob extends Model implements HasMedia
{
use BelongsToTenant, InteractsWithMedia, SoftDeletes;
public const TYPES = [
'body_repair' => 'Tinichigerie',
'pdr' => 'PDR (fără vopsire)',
'painting' => 'Vopsitorie',
'detailing' => 'Detailing',
'ceramic' => 'Ceramică',
'ppf' => 'Folie PPF',
'polishing' => 'Polish',
];
public const STATUSES = [
'estimate' => 'Deviz',
'approved' => 'Aprobat',
'in_progress' => 'În lucru',
'done' => 'Finalizat',
'delivered' => 'Predat',
'cancelled' => 'Anulat',
];
public const INSURANCE_STATUSES = [
'submitted' => 'Depus',
'approved' => 'Aprobat',
'rejected' => 'Respins',
'paid' => 'Plătit',
];
protected $fillable = [
'company_id', 'work_order_id', 'client_id', 'vehicle_id',
'number', 'type', 'status',
'is_insurance', 'insurer', 'policy_no', 'claim_no', 'insurance_status',
'estimate_amount', 'approved_amount', 'notes',
];
protected $casts = [
'is_insurance' => 'boolean',
'estimate_amount' => 'decimal:2',
'approved_amount' => 'decimal:2',
];
public function registerMediaCollections(): void
{
$this->addMediaCollection('photos_before');
$this->addMediaCollection('photos_after');
}
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
public function vehicle(): BelongsTo
{
return $this->belongsTo(Vehicle::class);
}
public function workOrder(): BelongsTo
{
return $this->belongsTo(WorkOrder::class);
}
public function damagePoints(): HasMany
{
return $this->hasMany(DamagePoint::class);
}
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('BS-%s-%04d', $year, $count + 1);
}
protected static function booted(): void
{
static::creating(function (self $job) {
if (empty($job->number)) {
$job->number = static::generateNumber(
$job->company_id ?: app(\App\Tenancy\TenantManager::class)->currentId()
);
}
});
}
}