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>
This commit is contained in:
@@ -135,6 +135,18 @@ class PartResource extends Resource
|
||||
->query(fn ($q) => $q->where('qty', '<=', 0)),
|
||||
])
|
||||
->actions([
|
||||
Actions\Action::make('ai_price')
|
||||
->label('AI: preț recomandat')
|
||||
->icon('heroicon-m-sparkles')
|
||||
->color('primary')
|
||||
->modalHeading(fn (Part $r) => "AI: preț pentru {$r->name}")
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Închide')
|
||||
->modalContent(function (Part $r) {
|
||||
[$reply, $meta] = app(\App\Services\Ai\AiAssistantService::class)
|
||||
->suggestPrice($r);
|
||||
return view('filament.tenant.ai-reply', ['reply' => $reply, 'meta' => $meta]);
|
||||
}),
|
||||
Actions\Action::make('receive')
|
||||
->label('Recepție')
|
||||
->icon('heroicon-m-arrow-down-tray')
|
||||
|
||||
@@ -93,6 +93,31 @@ class VehicleResource extends Resource
|
||||
Tables\Columns\TextColumn::make('created_at')->date()->sortable(),
|
||||
])
|
||||
->actions([
|
||||
Actions\Action::make('decode_vin')
|
||||
->label('Decode VIN')
|
||||
->icon('heroicon-m-cpu-chip')
|
||||
->color('gray')
|
||||
->visible(fn (\App\Models\Tenant\Vehicle $r) => ! empty($r->vin) && strlen($r->vin) === 17)
|
||||
->modalHeading(fn (\App\Models\Tenant\Vehicle $r) => 'Decode VIN: ' . $r->vin)
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Închide')
|
||||
->modalContent(function (\App\Models\Tenant\Vehicle $r) {
|
||||
$info = app(\App\Services\Ai\VinDecoder::class)->decode($r->vin);
|
||||
return view('filament.tenant.vin-decode', ['info' => $info, 'vehicle' => $r]);
|
||||
}),
|
||||
Actions\Action::make('ai_recommend')
|
||||
->label('AI: recomandări')
|
||||
->icon('heroicon-m-sparkles')
|
||||
->color('primary')
|
||||
->visible(fn (\App\Models\Tenant\Vehicle $r) => ! empty($r->vin))
|
||||
->modalHeading('Recomandări AI')
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Închide')
|
||||
->modalContent(function (\App\Models\Tenant\Vehicle $r) {
|
||||
[$reply, $meta] = app(\App\Services\Ai\AiAssistantService::class)
|
||||
->vinRecommendations($r->vin, (int) $r->mileage);
|
||||
return view('filament.tenant.ai-reply', ['reply' => $reply, 'meta' => $meta]);
|
||||
}),
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
])
|
||||
|
||||
@@ -15,6 +15,19 @@ class EditWorkOrder extends EditRecord
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user