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>
This commit is contained in:
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Tenant\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Tenant\Resources\BodyshopJobResource\Pages;
|
||||||
|
use App\Filament\Tenant\Resources\BodyshopJobResource\RelationManagers;
|
||||||
|
use App\Models\Tenant\BodyshopJob;
|
||||||
|
use App\Models\Tenant\Client;
|
||||||
|
use App\Models\Tenant\Vehicle;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas;
|
||||||
|
use Filament\Schemas\Components\Utilities\Get;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class BodyshopJobResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = BodyshopJob::class;
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-paint-brush';
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'Tinichigerie / Detailing';
|
||||||
|
|
||||||
|
protected static string|\UnitEnum|null $navigationGroup = 'Tinichigerie';
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'lucrare caroserie';
|
||||||
|
|
||||||
|
protected static ?string $pluralModelLabel = 'lucrări caroserie';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 80;
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
$open = static::getModel()::query()
|
||||||
|
->whereNotIn('status', ['delivered', 'cancelled'])->count();
|
||||||
|
return $open > 0 ? (string) $open : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema->components([
|
||||||
|
Schemas\Components\Section::make('Lucrare')
|
||||||
|
->columns(3)
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('number')->label('Nr.')->disabled()->dehydrated(false)->placeholder('Generat automat'),
|
||||||
|
Forms\Components\Select::make('type')->label('Tip')->options(BodyshopJob::TYPES)->default('body_repair')->required(),
|
||||||
|
Forms\Components\Select::make('status')->label('Status')->options(BodyshopJob::STATUSES)->default('estimate')->required(),
|
||||||
|
Forms\Components\Select::make('client_id')
|
||||||
|
->label('Client')
|
||||||
|
->options(fn () => Client::pluck('name', 'id'))
|
||||||
|
->searchable()->live(),
|
||||||
|
Forms\Components\Select::make('vehicle_id')
|
||||||
|
->label('Auto')
|
||||||
|
->options(fn (Get $get) => $get('client_id')
|
||||||
|
? Vehicle::where('client_id', $get('client_id'))->get()
|
||||||
|
->mapWithKeys(fn ($v) => [$v->id => "{$v->make} {$v->model} {$v->plate}"])->toArray()
|
||||||
|
: [])
|
||||||
|
->searchable(),
|
||||||
|
Forms\Components\TextInput::make('estimate_amount')->label('Deviz')->numeric()->default(0),
|
||||||
|
Forms\Components\TextInput::make('approved_amount')->label('Aprobat')->numeric()->default(0),
|
||||||
|
]),
|
||||||
|
Schemas\Components\Section::make('Asigurare')
|
||||||
|
->collapsible()
|
||||||
|
->columns(3)
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Toggle::make('is_insurance')->label('Caz de asigurare')->live()->columnSpanFull(),
|
||||||
|
Forms\Components\TextInput::make('insurer')->label('Asigurător')
|
||||||
|
->visible(fn (Get $get) => $get('is_insurance')),
|
||||||
|
Forms\Components\TextInput::make('policy_no')->label('Nr. poliță')
|
||||||
|
->visible(fn (Get $get) => $get('is_insurance')),
|
||||||
|
Forms\Components\TextInput::make('claim_no')->label('Nr. dosar daună')
|
||||||
|
->visible(fn (Get $get) => $get('is_insurance')),
|
||||||
|
Forms\Components\Select::make('insurance_status')->label('Status dosar')
|
||||||
|
->options(BodyshopJob::INSURANCE_STATUSES)
|
||||||
|
->visible(fn (Get $get) => $get('is_insurance')),
|
||||||
|
]),
|
||||||
|
Schemas\Components\Section::make('Foto înainte / după')
|
||||||
|
->columns(2)
|
||||||
|
->schema([
|
||||||
|
\Filament\Forms\Components\SpatieMediaLibraryFileUpload::make('photos_before')
|
||||||
|
->label('Înainte')->collection('photos_before')->multiple()->image()->reorderable()->maxFiles(20),
|
||||||
|
\Filament\Forms\Components\SpatieMediaLibraryFileUpload::make('photos_after')
|
||||||
|
->label('După')->collection('photos_after')->multiple()->image()->reorderable()->maxFiles(20),
|
||||||
|
]),
|
||||||
|
Forms\Components\Textarea::make('notes')->label('Observații')->columnSpanFull()->rows(2),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('number')->label('Nr.')->searchable()->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('client.name')->label('Client')->searchable()->placeholder('—'),
|
||||||
|
Tables\Columns\TextColumn::make('vehicle.plate')->label('Auto')->placeholder('—'),
|
||||||
|
Tables\Columns\TextColumn::make('type')
|
||||||
|
->formatStateUsing(fn ($s) => BodyshopJob::TYPES[$s] ?? $s)
|
||||||
|
->badge()->color('info'),
|
||||||
|
Tables\Columns\IconColumn::make('is_insurance')->label('Asig.')->boolean()->toggleable(),
|
||||||
|
Tables\Columns\TextColumn::make('damage_points_count')->counts('damagePoints')->label('Daune')->alignRight(),
|
||||||
|
Tables\Columns\TextColumn::make('approved_amount')->label('Aprobat')->money('MDL')->alignRight(),
|
||||||
|
Tables\Columns\TextColumn::make('status')
|
||||||
|
->formatStateUsing(fn ($s) => BodyshopJob::STATUSES[$s] ?? $s)
|
||||||
|
->badge()
|
||||||
|
->colors([
|
||||||
|
'gray' => ['estimate'],
|
||||||
|
'info' => ['approved', 'in_progress'],
|
||||||
|
'success' => ['done', 'delivered'],
|
||||||
|
'danger' => ['cancelled'],
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\SelectFilter::make('type')->options(BodyshopJob::TYPES),
|
||||||
|
Tables\Filters\SelectFilter::make('status')->options(BodyshopJob::STATUSES),
|
||||||
|
Tables\Filters\TernaryFilter::make('is_insurance')->label('Caz asigurare'),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Actions\EditAction::make(),
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
])
|
||||||
|
->emptyStateHeading('Nicio lucrare de caroserie')
|
||||||
|
->emptyStateDescription('Înregistrează lucrări de tinichigerie, vopsitorie, PDR, detailing, ceramică, PPF sau polish. Hartă daune, dosar asigurare și arhivă foto înainte/după.')
|
||||||
|
->emptyStateIcon('heroicon-o-paint-brush')
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
RelationManagers\DamagePointsRelationManager::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListBodyshopJobs::route('/'),
|
||||||
|
'create' => Pages\CreateBodyshopJob::route('/create'),
|
||||||
|
'edit' => Pages\EditBodyshopJob::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Tenant\Resources\BodyshopJobResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Tenant\Resources\BodyshopJobResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateBodyshopJob extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = BodyshopJobResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Tenant\Resources\BodyshopJobResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Tenant\Resources\BodyshopJobResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditBodyshopJob extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = BodyshopJobResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [Actions\DeleteAction::make()];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Tenant\Resources\BodyshopJobResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Tenant\Resources\BodyshopJobResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListBodyshopJobs extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = BodyshopJobResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [Actions\CreateAction::make()];
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Tenant\Resources\BodyshopJobResource\RelationManagers;
|
||||||
|
|
||||||
|
use App\Models\Tenant\DamagePoint;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class DamagePointsRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'damagePoints';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Hartă daune';
|
||||||
|
|
||||||
|
public function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema->components([
|
||||||
|
Forms\Components\Select::make('zone')
|
||||||
|
->label('Zonă')
|
||||||
|
->options(array_combine(DamagePoint::ZONES, DamagePoint::ZONES))
|
||||||
|
->searchable()
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Select::make('kind')
|
||||||
|
->label('Tip daună')
|
||||||
|
->options(array_combine(DamagePoint::KINDS, DamagePoint::KINDS))
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Select::make('severity')
|
||||||
|
->label('Gravitate')
|
||||||
|
->options(DamagePoint::SEVERITIES)
|
||||||
|
->default('minor')
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Textarea::make('notes')->label('Observații')->rows(2)->columnSpanFull(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->recordTitleAttribute('zone')
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('zone')->label('Zonă')->badge()->color('gray'),
|
||||||
|
Tables\Columns\TextColumn::make('kind')->label('Tip'),
|
||||||
|
Tables\Columns\TextColumn::make('severity')
|
||||||
|
->label('Gravitate')
|
||||||
|
->formatStateUsing(fn ($s) => DamagePoint::SEVERITIES[$s] ?? $s)
|
||||||
|
->badge()
|
||||||
|
->colors(['gray' => ['minor'], 'warning' => ['medium'], 'danger' => ['severe']]),
|
||||||
|
Tables\Columns\TextColumn::make('notes')->limit(40)->placeholder('—'),
|
||||||
|
])
|
||||||
|
->headerActions([Actions\CreateAction::make()])
|
||||||
|
->actions([Actions\EditAction::make(), Actions\DeleteAction::make()])
|
||||||
|
->emptyStateHeading('Nicio daună marcată')
|
||||||
|
->emptyStateDescription('Adaugă punctele de daună pe zone (capotă, ușă, aripă) cu tip și gravitate — formează harta de daune a mașinii.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Tenant;
|
||||||
|
|
||||||
|
use App\Models\Concerns\BelongsToTenant;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class DamagePoint extends Model
|
||||||
|
{
|
||||||
|
use BelongsToTenant;
|
||||||
|
|
||||||
|
public const ZONES = [
|
||||||
|
'Bară față', 'Capotă', 'Aripă FS', 'Aripă FD',
|
||||||
|
'Ușă FS', 'Ușă FD', 'Ușă SS', 'Ușă SD',
|
||||||
|
'Aripă SS', 'Aripă SD', 'Bară spate', 'Portbagaj',
|
||||||
|
'Plafon', 'Parbriz', 'Lunetă', 'Prag S', 'Prag D',
|
||||||
|
'Oglindă S', 'Oglindă D', 'Jantă',
|
||||||
|
];
|
||||||
|
|
||||||
|
public const KINDS = [
|
||||||
|
'Zgârietură', 'Lovitură', 'Fisură', 'Rugină', 'Vopsea sărită', 'Spart',
|
||||||
|
];
|
||||||
|
|
||||||
|
public const SEVERITIES = [
|
||||||
|
'minor' => 'Minoră',
|
||||||
|
'medium' => 'Medie',
|
||||||
|
'severe' => 'Gravă',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'company_id', 'bodyshop_job_id', 'zone', 'kind', 'severity', 'notes',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function job(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(BodyshopJob::class, 'bodyshop_job_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?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('bodyshop_jobs', function (Blueprint $t) {
|
||||||
|
$t->id();
|
||||||
|
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
|
||||||
|
$t->foreignId('work_order_id')->nullable()->constrained()->nullOnDelete();
|
||||||
|
$t->foreignId('client_id')->nullable()->constrained()->nullOnDelete();
|
||||||
|
$t->foreignId('vehicle_id')->nullable()->constrained()->nullOnDelete();
|
||||||
|
|
||||||
|
$t->string('number', 32);
|
||||||
|
$t->string('type', 24)->default('body_repair'); // body_repair/pdr/painting/detailing/ceramic/ppf/polishing
|
||||||
|
$t->string('status', 16)->default('estimate'); // estimate/approved/in_progress/done/delivered/cancelled
|
||||||
|
|
||||||
|
// Insurance case
|
||||||
|
$t->boolean('is_insurance')->default(false);
|
||||||
|
$t->string('insurer')->nullable();
|
||||||
|
$t->string('policy_no', 64)->nullable();
|
||||||
|
$t->string('claim_no', 64)->nullable();
|
||||||
|
$t->string('insurance_status', 24)->nullable(); // submitted/approved/rejected/paid
|
||||||
|
|
||||||
|
$t->decimal('estimate_amount', 12, 2)->default(0);
|
||||||
|
$t->decimal('approved_amount', 12, 2)->default(0);
|
||||||
|
$t->text('notes')->nullable();
|
||||||
|
|
||||||
|
$t->timestamps();
|
||||||
|
$t->softDeletes();
|
||||||
|
|
||||||
|
$t->unique(['company_id', 'number']);
|
||||||
|
$t->index(['company_id', 'status']);
|
||||||
|
$t->index(['company_id', 'type']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('damage_points', function (Blueprint $t) {
|
||||||
|
$t->id();
|
||||||
|
$t->foreignId('company_id')->constrained()->cascadeOnDelete();
|
||||||
|
$t->foreignId('bodyshop_job_id')->constrained()->cascadeOnDelete();
|
||||||
|
$t->string('zone', 40); // Capotă / Aripă FS / Ușă SD / ...
|
||||||
|
$t->string('kind', 32); // Zgârietură / Lovitură / Fisură / Rugină / ...
|
||||||
|
$t->string('severity', 12)->default('minor'); // minor/medium/severe
|
||||||
|
$t->text('notes')->nullable();
|
||||||
|
$t->timestamps();
|
||||||
|
|
||||||
|
$t->index(['company_id', 'bodyshop_job_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('damage_points');
|
||||||
|
Schema::dropIfExists('bodyshop_jobs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Central\Company;
|
||||||
|
use App\Models\Central\Plan;
|
||||||
|
use App\Models\Tenant\BodyshopJob;
|
||||||
|
use App\Models\Tenant\Client;
|
||||||
|
use App\Models\Tenant\DamagePoint;
|
||||||
|
use App\Tenancy\TenantManager;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class BodyshopTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private Company $company;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->company = $this->makeCompany('body');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_number_auto_generated(): void
|
||||||
|
{
|
||||||
|
$job = BodyshopJob::create(['type' => 'painting', 'status' => 'estimate']);
|
||||||
|
$this->assertStringStartsWith('BS-', $job->number);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_damage_points_relation(): void
|
||||||
|
{
|
||||||
|
$job = BodyshopJob::create(['type' => 'body_repair', 'status' => 'estimate']);
|
||||||
|
DamagePoint::create(['bodyshop_job_id' => $job->id, 'zone' => 'Capotă', 'kind' => 'Lovitură', 'severity' => 'severe']);
|
||||||
|
DamagePoint::create(['bodyshop_job_id' => $job->id, 'zone' => 'Ușă FS', 'kind' => 'Zgârietură', 'severity' => 'minor']);
|
||||||
|
|
||||||
|
$this->assertEquals(2, $job->damagePoints()->count());
|
||||||
|
$this->assertEquals('severe', $job->damagePoints()->where('zone', 'Capotă')->first()->severity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_insurance_fields_persist(): void
|
||||||
|
{
|
||||||
|
$job = BodyshopJob::create([
|
||||||
|
'type' => 'body_repair', 'status' => 'approved',
|
||||||
|
'is_insurance' => true, 'insurer' => 'MoldAsig',
|
||||||
|
'policy_no' => 'POL-123', 'claim_no' => 'CLM-999',
|
||||||
|
'insurance_status' => 'submitted',
|
||||||
|
'estimate_amount' => 12000, 'approved_amount' => 11500,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fresh = $job->fresh();
|
||||||
|
$this->assertTrue($fresh->is_insurance);
|
||||||
|
$this->assertEquals('MoldAsig', $fresh->insurer);
|
||||||
|
$this->assertEquals('CLM-999', $fresh->claim_no);
|
||||||
|
$this->assertEquals(11500.0, (float) $fresh->approved_amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_detailing_types_supported(): void
|
||||||
|
{
|
||||||
|
foreach (['ceramic', 'ppf', 'polishing', 'detailing', 'pdr'] as $type) {
|
||||||
|
$job = BodyshopJob::create(['type' => $type, 'status' => 'estimate']);
|
||||||
|
$this->assertArrayHasKey($job->type, BodyshopJob::TYPES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_bodyshop_jobs_isolated_per_tenant(): void
|
||||||
|
{
|
||||||
|
BodyshopJob::create(['type' => 'painting', 'status' => 'estimate']);
|
||||||
|
$other = $this->makeCompany('otherbody');
|
||||||
|
app(TenantManager::class)->setCurrent($other);
|
||||||
|
$this->assertEquals(0, BodyshopJob::count());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeCompany(string $slug): Company
|
||||||
|
{
|
||||||
|
$plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]);
|
||||||
|
$company = Company::create(['plan_id' => $plan->id, 'slug' => $slug, 'name' => ucfirst($slug), 'status' => 'active']);
|
||||||
|
app(TenantManager::class)->setCurrent($company);
|
||||||
|
return $company;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user