Vasyka
dfb92bf5e2
feat: AI chat tool-use (Claude function calling)
...
The Asistent AI chat can now query the tenant DB directly through Claude
tool use. AiToolExecutor exposes 5 read-only tools (search_clients,
get_vehicle, find_parts, recent_workorders, low_stock_parts) all scoped
to the current tenant via BelongsToTenant.
AiAssistantService::callClaude loops on stop_reason=tool_use up to 5 rounds:
- normalize message history to content blocks
- send `tools` definitions + messages to Anthropic API
- on tool_use: execute each tool, append tool_results as user turn, recall
- on end_turn: collect text + cumulative token counts + tool-call audit in
AiMessage.meta.tools
Single-shot helpers (suggestDiagnosis, suggestPrice, vinRecommendations,
suggestParts) are unchanged — only the conversational chat gets tool-use.
Tests (3 new):
- two-round tool_use → execute → final text; verify 5 tools sent both rounds;
cumulative tokens
- executor finds part by brand
- unknown tool name returns error blob
Full suite: 109 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-06-02 19:36:07 +00:00
Vasyka
8fdfc9ef85
feat: Part product images + seasonal tire-swap reminders
...
Part (HasMedia):
- Spatie media `image` single-file collection + imageUrl() helper
- PartResource form: image upload section (image editor, 2 MB max)
- Parts list: circular thumbnail column
- Shop catalog cards: square thumbnail + 📦 placeholder
- Shop part detail: 260px image alongside info, single column when no image
Seasonal tire-swap reminders:
- NotificationDispatcher::tireSeasonalSwap(TireSet) — Telegram first, email
fallback (when set has a vehicle, via ServiceReminderMail with 'tire_swap'
type and a size-aware note)
- tires:remind-seasonal artisan command, self-gating to Feb 15-Mar 15
(notify winter sets stored) and Sep 15-Oct 15 (notify summer sets stored).
60-day cooldown per client via service_reminders_sent. --force / --dry-run.
- Schedule: weekly Mon 09:30
Tests (6 new):
- outside window no-ops; spring window notifies winter; spring ignores summer;
autumn notifies summer; cooldown blocks doubles; --force overrides window
Full suite: 106 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-06-02 19:31:24 +00:00
Vasyka
ac7d5b4733
fix: central company view page 404 + broken stats query
...
The /admin/companies/{id} view page (ViewCompany extends Page) 404'd because
the {record} route param could arrive as a JSON-encoded model (Livewire typed-
property hydration), so findOrFail() received a non-id and threw
ModelNotFoundException. Added resolveRecordKey() to normalize scalar id / model
/ JSON-string down to the integer key.
Also fixed getStats() referencing non-existent parts columns `stock` /
`low_stock_threshold` (real columns are `qty` / `min_qty`), which would 500 the
page once mount resolved.
Added CentralCompanyViewTest as regression (asserts 200 + company name). Full
suite: 100 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-28 10:24:08 +00:00
Vasyka
d6a0bfb890
fix: repair 4 pre-existing failing tests
...
- WorkOrderCalcTest: vehicle fixture used `brand` (column is `make`, NOT NULL)
and WorkOrderPart used `price`/manual total (column is `sell_price`, total is
auto-computed). Switched to correct columns + valid status.
- ExampleTest: stock test expected 200 on `/` but the central domain redirects
to /admin. Replaced with a deterministic central-root → /admin redirect check.
Full suite now green: 99 passed, 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-28 07:14:20 +00:00
Vasyka
5e255b7b40
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 >
2026-05-28 06:49:47 +00:00
Vasyka
e8078f157a
Stage 9 — Subcontractor System: outsourced work with cost+markup
...
Schema:
- subcontractors (specialty, rating, contact)
- subcontract_jobs (work_order link, cost, markup_pct, client_price, status
workflow, sent_at/eta/returned_at, paid_to_sub)
Models:
- SubcontractJob: auto number (SC-YY-NNNN), client_price = cost×(1+markup/100)
when markup>0 (else manual), margin() helper, recalcs parent WO on save/delete
- WorkOrder.recalcTotal now includes non-cancelled subcontract job client_price
Filament (new "Subcontractare" nav group):
- SubcontractorResource (specialty/rating CRUD)
- SubcontractJobResource board with cost/client/margin columns + status filters,
nav badge = open jobs
- SubcontractJobsRelationManager on WorkOrder
Tests (7 new):
- client_price from markup; manual price without markup; auto number;
WO total includes jobs; cancelled excluded; delete recalcs; tenant isolation
Closes roadmap to 16/18 stages (only Stage 10 Bodyshop remains).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-28 06:43:15 +00:00
Vasyka
94938f24d7
Stage 11 — Tire Service: tire hotel + wheel sets
...
Schema:
- tire_sets (client/vehicle, season, size width/profile/diameter, brand/DOT,
rims, tread JSON per position + tread_min cache, TPMS + sensor ids, photos)
- tire_storage (location, season_label, stored/retrieved, check-in/out, fee)
Models:
- TireSet (HasMedia): sizeLabel, isStored, currentStorage, auto tread_min
- TireStorage: durationDays, isActive
Filament (new "Anvelope" nav group):
- TireSetResource: specs form + per-position tread + TPMS + photo upload;
table with size, season badge, min tread (red < 3mm), storage status
- Check-in (location + period + fee → stored) / Check-out (→ retrieved)
- StorageRelationManager (stay history); nav badge = sets currently stored
Tests (6 new):
- sizeLabel formatting; tread_min from positions; check-in active storage;
check-out retrieved + duration; multiple stays per set; tenant isolation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-28 06:33:00 +00:00
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
Vasyka
c90c35d930
Stage 8 — Smart Pricing Engine: contextual coefficients
...
Contextual multipliers layered on top of base MarkupRule pricing, applied
per work-order line based on vehicle, client and urgency.
Schema:
- pricing_coefficients (multiplier, conditions JSON, priority, stackable)
- vehicles.vehicle_class (sedan/suv/commercial/hybrid/ev/premium)
- clients.is_vip
- work_orders.urgency (normal/urgent/express)
PricingEngine::quote(Part, Vehicle?, Client?, urgency):
- base = MarkupRule on buy_price (fallback sell_price or buy×1.30)
- context: class (explicit or inferred hybrid/ev from fuel), age, vip, urgency
- stackable coefficients all multiply; non-stackable take only the highest
- returns {base, final, applied[]} breakdown
PricingCoefficient::matches(ctx) — classes/age range/vip/urgency conditions
(empty = always applies).
Filament:
- PricingCoefficientResource with condition builder (classes, age, vip, urgency)
- vehicle_class select, client is_vip toggle, WO urgency select
- "Preț inteligent" action on WO parts shows breakdown + applies sell_price
Tests (6 new):
- base-only without coefficients; age coefficient gating; VIP; express urgency;
stackable multiply vs non-stackable highest-wins; hybrid inferred from fuel
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-28 05:40:27 +00:00
Vasyka
954ba8f059
Stage 12 — Online Store: public catalog + cart + orders
...
Schema:
- online_orders (token-tracked, status workflow, delivery method/fee)
- online_order_items (price snapshot, fulfilled flag)
- part_cross_refs (OEM/equivalent codes for search)
- parts.is_published (shop visibility)
Storefront (ShopController, tenant subdomain, /shop):
- Catalog with search across name/article/brand/cross-refs, category +
in-stock filters, live stock, white-label themed layout
- Part detail page with cross-ref codes
- VIN search → VinDecoder → guided catalog search
- Session cart (per-tenant key), guest checkout, order confirmation page
- Respects settings.shop.enabled (404 when off); tenant-guarded
Part::searchPublished matches cross-ref articles via whereHas.
Order notifications (ShopOrderNotifier, best-effort):
- Staff: Web Push to active users
- Customer: Telegram if phone matches a linked client
Filament (tenant):
- OnlineOrderResource under "Magazin" nav group, status workflow,
items relation, "Onorează" action issues stock via WarehouseService (FIFO)
- PartResource: is_published toggle + column + bulk publish/unpublish +
CrossRefsRelationManager
- Settings: shop section (enable, delivery methods, fee, free-over)
- Landing page: shop button when enabled
Tests (6 new):
- catalog 404 when disabled; lists published only; cross-ref search;
order placement (token + items + total); fulfill issues stock;
cross-tenant token isolation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-28 05:27:51 +00:00
Vasyka
c413004930
Stage 15 — PWA complete: install prompt + Web Push notifications
...
Dependency:
- minishlink/web-push v10 (VAPID JWT + aes128gcm payload encryption)
- Dockerfile: add curl, mbstring, gmp extensions (web-push needs ext-curl)
VAPID:
- config/webpush.php from env; `php artisan push:vapid` generates keypair
- Shared platform keypair; .env.example has empty placeholders
Schema:
- push_subscriptions (user/company, endpoint unique, p256dh, auth, encoding)
WebPushService:
- send / sendToUser / dispatch via WebPush::flush
- Auto-prunes subscriptions reported expired (404/410)
Subscribe flow:
- POST /push/subscribe + /push/unsubscribe (auth, tenant)
- Tenant panel JS subscribes after SW registration with VAPID public key
Service worker (/sw.js):
- Cache v2, push listener → showNotification, notificationclick → focus/open
Install prompt:
- Floating "Instalează aplicația" button wired to beforeinstallprompt
Staff push:
- WorkOrder master_id change → push to assigned mechanic
- Settings "Test notificare push" action
Tests (6 new):
- subscribe stores + upserts; requires auth (401); validation (422);
service configured; sendToUser with no subs returns zero
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-28 05:11:18 +00:00
Vasyka
e48ef1b755
Stage 7+14 — Mechanic Board + Scan Center
...
Mechanic Workflow (Stage 7):
- /app/mechanic Filament page filtered to master_id = auth user
- Kanban 4 columns (in_work / awaiting_parts / ready / recent), each card
shows WO#, plate, client, complaint summary, photo presence
- 2 KPI tiles (active now / closed today)
- Mobile-responsive grid (auto-fit, minmax 260px)
WarehouseService:
- issueNow(WorkOrderPart) — consume reservations immediately scoped to one
line, without closing the WO (mechanic physically takes part now)
- returnPart(WorkOrderPart, qty?, notes?) — refund to stock as new batch
at original buy_price, writes `return` event, capped at consumed total
WO PartsRelationManager:
- "Eliberează" action — visible when active reservation exists
- "Restituire" action — visible when consumed reservation exists, with qty
modal + notes
Scan Center (Stage 14):
- PartResource "QR" action — per-part SVG QR with payload PART:<article|id>
- BulkAction "Tipărește etichete QR" → /parts/labels?ids=N,M (HTML A4 sheet,
3-col grid, print CSS hides toolbar)
- /app/scan Filament page using html5-qrcode 2.3.8 (CDN), auto-picks back
camera, decodes → Livewire dispatches scanner-decoded → resolveAndRedirect
- Lookup matches PART:N prefix, parts.article, parts.barcode, or numeric id
- Manual input fallback for browsers without camera
Tests (6 new):
- WarehouseIssueReturnTest (3): issueNow consumes immediately; returnPart
creates positive batch + return event; over-return is capped
- ScannerLookupTest (3): PART: prefix lookup, raw barcode lookup, unknown miss
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-27 21:39:39 +00:00
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
Vasyka
85ef2f6e00
Stage 13 — Notifications: Telegram bot + multi-channel + service reminders
...
Schema:
- clients.telegram_chat_id (linked via /start contact-share)
- clients.notify_prefs (per-client channel order override)
- service_reminders_sent (dedup ledger for the daily cron)
Telegram (per tenant):
- TelegramService (sendMessage, getMe, setWebhook with auto-generated secret)
- Bot token stored in companies.settings.telegram.bot_token
- Webhook /telegram/webhook/{slug} validates X-Telegram-Bot-Api-Secret-Token,
matches client by last 9 digits of phone, persists chat_id, replies confirm
- /start prompts share-contact; /stop unlinks chat_id
NotificationDispatcher refactor:
- Multi-channel: telegram first if chat_id + bot configured, then email
- Backwards-compat with legacy boolean notify.{type} flags
- 4 HTML-formatted Telegram messages (wo_ready with tracking link, payment,
appointment, reminder)
Service reminders:
- `reminders:send` artisan command with --slug / --dry-run
- Policy: vehicles whose last closed WO is older than reminder.after_days
(default 365). Skips if sent within reminder.cooldown_days (default 30).
- Schedule daily 09:00
Filament UI:
- Settings page: Telegram bot token field + "Test bot" + "Set webhook" actions
- Settings page: reminder_after_days + reminder_cooldown_days inputs
- ClientResource: telegram_chat_id readonly badge
Tests (6 new, all pass):
- webhook links client via shared contact
- webhook rejects wrong secret → 401
- dispatcher uses telegram when chat_id present (Http::fake)
- dispatcher falls back to email otherwise
- dispatcher returns false when no channel available
- reminder cron respects 30-day cooldown
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-27 20:14:17 +00:00
Vasyka
a2026f640a
Stage 6 — Purchase System: partial receipt + supplier analytics
...
Schema:
- purchase_items.qty_received (backfilled from `received` boolean)
- purchases.warehouse_id (target warehouse FK)
- supplier_part_prices (price history per supplier/part with purchase ref)
- New status `partial` between ordered and received
Purchase ↔ Warehouse integration:
- Purchase::receiveItem(item, qty, warehouse?) — routes through
WarehouseService::receive: creates batch + receipt event + supplier price row
- Purchase::receiveAllRemaining(warehouse?) — receives all outstanding lines
- Purchase::recomputeStatus() — auto: ordered → partial → received
Old flat markReceived() removed — every receipt now writes batches + ledger.
Filament:
- Purchase list: progress %, partial badge, warehouse picker on form
- ItemsRelationManager: per-line "Recepționează" with qty + warehouse modal,
qty_received shown as "X.XX / Y.YY" with colour
- PartResource: new PriceHistoryRelationManager (supplier price history)
- SupplierResource: derived columns onTimeRate / avgDeliveryDays / spend(90d)
+ "Rerating" action
Analytics:
- App\Services\Warehouse\SupplierAnalytics (onTimeRate, avgDeliveryDays,
spend, count, computedRating)
- `suppliers:rate` artisan command + weekly schedule (Mon 04:00)
- Computed rating: 70% on-time + 20% volume + 10% speed bonus
Tests (6 new, all pass):
- Partial receipt of 3/10 → status=partial + 1 batch + 1 price row
- receiveAllRemaining → status=received with received_at set
- Over-receive throws InvalidArgumentException
- Two partial receipts (4+6) → 2 batches FIFO + status=received
- onTimeRate 50% with 1 on-time + 1 late
- computedRating null when <2 deliveries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-27 19:37:12 +00:00
Vasyka
426156fe45
Stage 5.1 — Warehouse ERP: batches + FIFO + reservations + multi-warehouse
...
Schema:
- warehouses (multi-warehouse, code unique per company, is_default)
- part_batches (lot per receipt, qty_in/qty_remaining, buy_price, FIFO-indexed)
- warehouse_events (immutable ledger: opening/receipt/issue/transfer/adjustment/write_off)
- part_reservations (per-WO allocations from specific batches, active/consumed/released)
- companies.default_warehouse_id + parts.qty_reserved
Backfill: 1 default warehouse + 1 opening batch per existing part per company.
WarehouseService:
- receive / issue (FIFO) / reserve / release / consume / transfer / adjust
- DB::transaction + lockForUpdate on batch rows
- InsufficientStockException with requested + available context
- Auto-syncs parts.qty as aggregate cache (source of truth = sum(qty_remaining))
WO integration:
- WorkOrderPart created/updated → reserve from FIFO batches
- WorkOrderPart deleted → release
- WorkOrder status=done → consume reservations into issue events
- WorkOrder status=cancelled → release reservations
Filament:
- WarehouseResource (CRUD)
- BatchesRelationManager on PartResource (FIFO list with qty_remaining + cost)
- "Recepție" action on parts list → calls WarehouseService::receive
- qty_reserved column added on parts list
Tests (8 new, all pass):
- receipt creates batch + event
- FIFO order verified across 3 batches with different received_at
- InsufficientStockException on over-issue
- Reservations block other reservations but don't deplete on-hand
- WO done consumes; WO cancelled releases
- Batches tenant-isolated
- Transfer between warehouses with weighted-avg cost
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-27 19:29:19 +00:00
Vasyka
edcdba9d53
Stage 3 — WO photos + ETA + QR + public tracking page
...
- HasMedia (Spatie) on WorkOrder with `photos` collection
- eta_at + tracking_token columns; token auto-generated on create
- Public /t/{token} page — tenant-scoped via subdomain, white-label themed
- QR code SVG via chillerlan/php-qrcode (inline modal + download)
- Filament: SpatieMediaLibraryFileUpload + ETA picker + tracking section
- EditWorkOrder header action "Link client (QR)" modal
- Fix: Auditable::dontSubmitEmptyLogs() → dontLogEmptyChanges() (removed in activitylog)
- Tests: TrackingPageTest (4 pass) covering token gen + cross-tenant isolation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-27 19:21:23 +00:00
Vasyka
d1e0695930
Deploy 1: i18n + Notifications + Global Search + Tests
...
- SetLocale middleware (ro/ru/en, session-first, user-persisted)
- Lang switcher in topbar (Filament render hook USER_MENU_BEFORE)
- POST /locale/{lang} route persists to user.locale + session
- Database notifications enabled on tenant panel (30s polling)
- GlobalSearch (Cmd+K / Ctrl+K) on Client, Vehicle, WorkOrder, Lead, Part
- Tests: TenantIsolation (4), AuthFlow (2), WorkOrderCalc (3), MarkupRule (3)
2026-05-07 18:22:48 +00:00
Vasyka
5e32f82b3a
Initial Laravel 12 + Filament 5 + Octane skeleton
...
- Laravel 12 base
- Filament 5 (default admin panel)
- Stancl/Tenancy v3 (config + migrations only)
- Spatie Permission
- Octane FrankenPHP runtime
- Sanctum
- Dockerfile multi-stage (composer + node + frankenphp:8.4)
- Entrypoint runs migrations + caches on boot
- .env.example pre-completat cu hosturi interne Coolify
- Health endpoint /up
Repo init pentru multi-tenant SaaS pe Coolify Hetzner.
2026-05-04 12:19:55 +00:00