Files
Vasyka a1be01b0d5 Stage 4 — Labor Catalog: fixed price + default parts + service templates
Schema:
- labors.pricing_mode (hourly/fixed) + fixed_price
- labor_parts (default parts auto-added with a labor)
- service_templates + service_template_items (labor/part bundles)

ServiceComposer:
- addLabor(wo, labor, withParts) — hourly (hours×rate) or fixed (fixed_price),
  then auto-adds the labor's default parts
- addPart(wo, part, qty) — catalog price snapshot
- applyTemplate(wo, template) — adds all labor+part lines, recalcs total
- hourlyRate from settings.labor_rate

Filament:
- LaborResource: pricing_mode (live) toggles hours/fixed_price fields,
  DefaultPartsRelationManager
- ServiceTemplateResource (Service group) with ItemsRelationManager
- WorkOrder edit "Aplică șablon" action → applyTemplate
- WorksRelationManager CreateAction auto-adds labor default parts

Tests (6 new):
- hourly rate×hours; fixed uses fixed_price; default parts auto-added;
  withParts=false skips; applyTemplate adds lines + recalcs total;
  templates tenant-isolated

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

79 lines
3.4 KiB
PHP

<?php
namespace App\Filament\Tenant\Resources\WorkOrderResource\Pages;
use App\Filament\Tenant\Resources\WorkOrderResource;
use App\Models\Tenant\WorkOrder;
use App\Services\WorkOrderPdfService;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditWorkOrder extends EditRecord
{
protected static string $resource = WorkOrderResource::class;
protected function getHeaderActions(): array
{
return [
Actions\Action::make('apply_template')
->label('Aplică șablon')
->icon('heroicon-m-clipboard-document-list')
->color('gray')
->schema([
\Filament\Forms\Components\Select::make('template_id')
->label('Șablon serviciu')
->options(fn () => \App\Models\Tenant\ServiceTemplate::where('is_active', true)->pluck('name', 'id'))
->searchable()
->required(),
])
->action(function (array $data) {
$template = \App\Models\Tenant\ServiceTemplate::with('items')->find($data['template_id']);
if (! $template) return;
$r = app(\App\Services\ServiceComposer::class)->applyTemplate($this->record, $template);
$this->fillForm();
\Filament\Notifications\Notification::make()
->title("Șablon aplicat: {$r['labor']} manopere, {$r['parts']} piese")
->success()->send();
}),
Actions\Action::make('ai_diagnose')
->label('AI: sugerează diagnostic')
->icon('heroicon-m-sparkles')
->color('primary')
->visible(fn () => ! empty($this->record->complaint))
->modalHeading('Diagnostic AI bazat pe plângerea clientului')
->modalSubmitAction(false)
->modalCancelActionLabel('Închide')
->modalContent(function () {
[$reply, $meta] = app(\App\Services\Ai\AiAssistantService::class)
->suggestDiagnosis($this->record);
return view('filament.tenant.ai-reply', ['reply' => $reply, 'meta' => $meta]);
}),
Actions\Action::make('tracking')
->label('Link client (QR)')
->icon('heroicon-m-qr-code')
->color('primary')
->modalHeading(fn () => 'Tracking client — WO #' . $this->record->number)
->modalSubmitAction(false)
->modalCancelActionLabel('Închide')
->modalContent(fn () => view('filament.tenant.tracking-qr', [
'wo' => $this->record,
])),
Actions\Action::make('pdf')
->label('Descarcă PDF')
->icon('heroicon-m-document-arrow-down')
->color('gray')
->action(function () {
/** @var WorkOrder $wo */
$wo = $this->record;
$svc = app(WorkOrderPdfService::class);
$pdf = $svc->generate($wo);
return response()->streamDownload(
fn () => print($pdf->output()),
$svc->filename($wo)
);
}),
Actions\DeleteAction::make(),
];
}
}