Files
autocrm/app/Filament/Tenant/Resources/WorkOrderResource/Pages/EditWorkOrder.php
T
Vasyka 1ff888131f Stage 16 — AI Layer: VIN decoder + diagnostic / parts / price helpers
VinDecoder (deterministic, no API):
- ISO 3779/3780 parsing: WMI manufacturer (~60 brands), year (cyclical with
  post-2010 disambiguation via position 7), region, plant, NA checksum
- Strip non-VIN chars, accept dashes/spaces, reject I/O/Q per spec

AiAssistantService:
- Refactored provider HTTP into postClaude/postOpenAI/postGemini so both
  chat history and one-shot calls share the same transport
- singleShot(system, userPrompt, provider?) for fire-and-forget calls
- 4 specialized helpers with tight prompts:
  - suggestDiagnosis(WO) — diagnostician based on complaint + VIN info
  - suggestParts(WO, task) — OEM parts list for an operation
  - suggestPrice(Part) — markup recommendation with justification
  - vinRecommendations(vin, mileage) — scheduled maintenance from decoded VIN
- monthlyUsage() — token spend MTD by provider

Filament:
- VehicleResource: "Decode VIN" + "AI: recomandări" actions
- WorkOrderResource Edit: "AI: sugerează diagnostic" header action
- PartResource: "AI: preț recomandat" action
- Shared views: filament.tenant.ai-reply, filament.tenant.vin-decode
- AiAssistant page shows monthly token usage banner

Tests (13 new):
- 8 VinDecoder unit tests with real VIN samples (Honda 2003, VW 1999, Audi
  2014, Dacia, unknown WMI, lowercase/dashes, forbidden chars)
- 5 AiHelpers feature tests with Http::fake covering all providers + no-key
  fallback + token usage aggregation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:24:09 +00:00

59 lines
2.3 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('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(),
];
}
}