Files
autocrm/app/Services/ServiceComposer.php
T
Vasyka 9609295a9e feat(labors): trilingual name — add name_en column + Labor::label() helper
- Migration: add nullable `labors.name_en` alongside existing name_ro/
  name_ru.
- Labor model: label(?locale) accessor returns name_{locale} with
  name_ro fallback.
- LaborResource form: expose 3rd 'Nume (EN)' field.
- Table + Select displays now go through label() so options show the
  locale-appropriate name:
    /app/labors table column,
    WorkOrderResource works Select ('[category] name (Nh)'),
    ServiceTemplate items Select,
    ServiceComposer WorkOrderWork snapshot on create.
- Snapshot name saved on wo_works.name at creation reflects the
  current locale; existing rows keep their stored snapshot untouched.

All 306 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-26 10:54:00 +00:00

135 lines
4.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Models\Central\Company;
use App\Models\Tenant\Labor;
use App\Models\Tenant\Part;
use App\Models\Tenant\ServiceTemplate;
use App\Models\Tenant\WorkOrder;
use App\Models\Tenant\WorkOrderPart;
use App\Models\Tenant\WorkOrderWork;
use Illuminate\Support\Facades\DB;
/**
* Composes work order lines from the labor catalog + templates:
* - adds a labor (hourly or fixed) and optionally its default parts
* - applies a full service template (labor + part lines) in one shot
*/
class ServiceComposer
{
public function hourlyRate(int $companyId): float
{
$company = Company::withoutGlobalScopes()->find($companyId);
return (float) data_get($company?->settings, 'labor_rate', 400);
}
/**
* Add a labor line to a WO. Fixed-price labors set total directly via
* hours=1 × price_per_hour=fixed_price. Returns the created work line.
*/
public function addLabor(WorkOrder $wo, Labor $labor, bool $withParts = true): WorkOrderWork
{
$rate = $this->hourlyRate($wo->company_id);
return DB::transaction(function () use ($wo, $labor, $withParts, $rate) {
if ($labor->pricing_mode === 'fixed') {
$hours = 1;
$pricePerHour = (float) $labor->fixed_price;
} else {
$hours = (float) $labor->hours ?: 1;
$pricePerHour = $rate;
}
$work = WorkOrderWork::create([
'work_order_id' => $wo->id,
'labor_id' => $labor->id,
'name' => $labor->label(),
'hours' => $hours,
'price_per_hour' => $pricePerHour,
'status' => 'todo',
]);
if ($withParts) {
foreach ($labor->laborParts as $lp) {
$part = $lp->part;
if (! $part) continue;
$this->addPart($wo, $part, (float) $lp->qty, $lp->unit ?: $part->unit);
}
}
return $work;
});
}
public function addPart(WorkOrder $wo, Part $part, float $qty, ?string $unit = null): WorkOrderPart
{
return WorkOrderPart::create([
'work_order_id' => $wo->id,
'part_id' => $part->id,
'name' => $part->name,
'article' => $part->article,
'brand' => $part->brand,
'qty' => $qty,
'unit' => $unit ?: $part->unit ?: 'buc',
'buy_price' => (float) $part->buy_price,
'sell_price' => (float) $part->sell_price,
'status' => 'needed',
]);
}
/**
* Apply a full template to a WO: every labor + part item becomes a WO line.
* Labor items are added WITHOUT their own default parts (template is explicit).
*
* @return array{labor:int, parts:int}
*/
public function applyTemplate(WorkOrder $wo, ServiceTemplate $template): array
{
return DB::transaction(function () use ($wo, $template) {
$laborCount = 0;
$partCount = 0;
foreach ($template->items as $item) {
if ($item->kind === 'labor') {
if ($item->labor_id && ($labor = Labor::find($item->labor_id))) {
$work = $this->addLabor($wo, $labor, withParts: false);
if ($item->hours) {
$work->hours = (float) $item->hours;
$work->save();
}
} else {
// Free-text labor line from snapshot.
WorkOrderWork::create([
'work_order_id' => $wo->id,
'name' => $item->name,
'hours' => (float) ($item->hours ?: 1),
'price_per_hour' => $this->hourlyRate($wo->company_id),
'status' => 'todo',
]);
}
$laborCount++;
} elseif ($item->kind === 'part') {
if ($item->part_id && ($part = Part::find($item->part_id))) {
$this->addPart($wo, $part, (float) ($item->qty ?: 1));
} else {
WorkOrderPart::create([
'work_order_id' => $wo->id,
'name' => $item->name,
'qty' => (float) ($item->qty ?: 1),
'unit' => 'buc',
'sell_price' => 0,
'status' => 'needed',
]);
}
$partCount++;
}
}
$wo->recalcTotal();
return ['labor' => $laborCount, 'parts' => $partCount];
});
}
}