Vasyka
c84ef5d9bd
fix: calendar collapses to plain text after first render
...
FullCalendar mounts into a Livewire-managed subtree. The first
$wire.getEvents() response triggered a Livewire DOM morph that reverted
#autocrm-calendar to its empty server HTML, destroying the rendered grid
(~1s after load it became unstyled text).
Wrap the calendar container in wire:ignore so Livewire's morphdom skips it.
The quick-create modal stays outside wire:ignore to keep its form reactive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-28 22:58:55 +00:00
Vasyka
40478dd2aa
fix: make purchases partial-receipt migration idempotent
...
On MariaDB (no transactional DDL) a half-applied prior run left
purchases.warehouse_id committed without recording the migration. On retry it
failed with "Duplicate column name 'warehouse_id'", which aborted the migrate
run and blocked every later migration (notifications, push, online store,
pricing, labor templates, tire, subcontractor, bodyshop) — so those tables were
never created (e.g. bodyshop_jobs missing → 500 on tenant pages).
Guard each step with Schema::hasColumn / hasTable so the migration completes on
re-run and unblocks the rest of the batch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-28 22:39:34 +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
59409e1b11
fix: calendar — register createForm via getForms() + remove broken FullCalendar CSS link
...
- Filament v5 multi-form pages need protected getForms() returning form names
- FullCalendar v6 bundles CSS in JS — separate <link> 404'd silently
- Added Russian locale alongside Romanian for i18n switcher
2026-05-08 11:27:04 +00:00
Vasyka
d5680f78dc
fix: column names in globalSearch (leads.source, vehicles.make, parts.article, WO.complaint)
2026-05-08 11:12:16 +00:00
Vasyka
4fc832ede0
Show brand name beside logo in topbar + fix global search
...
- brandLogo closure now returns HtmlString with <img> + <span> side-by-side
so the company name appears next to the logo (Filament default replaces
brandName when brandLogo is set)
- Global search: explicit ->globalSearch() + ->globalSearchKeyBindings(['mod+k'])
(use Mod which Filament auto-maps to Cmd/Ctrl per platform) +
->globalSearchFieldKeyBindingSuffix() shows keyboard hint in search bar
- Same applied to Central panel
2026-05-08 10:58:13 +00:00
Vasyka
b4ac5451bb
Persistent storage volume + remove debug route + validate logo file exists
...
- Coolify persistent volume mounted at /app/storage/app (covers public uploads,
private files, backups). Configured via API:
POST /api/v1/applications/{uuid}/storages with type=persistent
- getLogoUrl() / getFaviconUrl() now validate file_exists($m->getPath()) before
returning URL — guards against stale DB rows from pre-volume era
- Removed /debug-storage diagnostic route (used to find the symlink+volume bug)
2026-05-08 10:41:14 +00:00
Vasyka
3ca7ceeabb
debug: also list storage/app/public contents
2026-05-08 10:11:16 +00:00
Vasyka
012e7c92e9
debug: /debug-storage route to inspect symlink + media state
2026-05-08 10:06:30 +00:00
Vasyka
8f324b7b8e
Currency dropdown + logo on login/topbar via brandLogo closure
...
- Onboarding + Settings: Forms\Components\Select for currency
(MDL/EUR/USD/RON/UAH/RUB) instead of TextInput
- TenantPanel: brandName/brandLogo/favicon as closures resolving from
tenant. Logo now visible on login page, topbar, sidebar (Filament
v5 brandLogo handles all 3 spots automatically)
- Favicon falls back to logo if favicon not separately uploaded
- Removed manual SIDEBAR_LOGO_BEFORE injection (replaced by brandLogo)
- Removed manual <link rel=icon> in HEAD_END (replaced by ->favicon)
2026-05-08 07:50:12 +00:00
Vasyka
93a69dd826
Add Paynet (Moldova) payment gateway
...
PaymentSettings:
- New "🇲🇩 Paynet" section: enabled toggle, mode (test/live), merchant_code,
service_id, user, password, secret (HMAC), webhook URL hint
- Webhook URL: https://service.mir.md/payments/paynet/webhook
PaymentController:
- startPaynet() — builds Paynet redirect (stub mode prints flow)
- paynetWebhook() — verifies HMAC-SHA256 signature canonical
Merchant_Code|Order_ID|Amount|Status, marks subscription paid on Status=OK,
matches by invoice_number = Order_ID
- availableMethods() includes paynet
Tenant /billing:
- 4th payment button "🇲🇩 Paynet" — visible only when configured.
Description: Card MAIB / MICB / Victoriabank, MD Cash, e-money
Routes:
- POST /payments/paynet/webhook (CSRF excluded)
2026-05-08 06:20:11 +00:00
Vasyka
827bf12d89
Demo plan + Payment integrations (Stripe/PayPal/Bank)
...
Models & migrations:
- platform_settings table (key/value JSON store + Cache::remember 5min)
- plans: is_demo bool + trial_days int
- companies: is_demo bool
Plans:
- Demo plan seeded (is_demo=true, is_public=false, all features, 14 trial days)
- Trial 14-day plan seeded (is_public=true, basic features)
- Plan form: is_demo toggle + trial_days field
- Plan table: badge 🎬 Demo / 🎁 N zile trial
Central panel:
- PaymentSettings page (heroicon-credit-card, sort 90)
Form sections: General, Date legale, Stripe, PayPal, Transfer bancar
Each gateway collapsible, fields hidden until enabled toggle
Saves to platform_settings keyed by `payments.{gateway}`
- CompanyResource: is_demo toggle + table description
Payment flow (PaymentController):
- GET /billing — tenant invoices list with Pay button
- POST /pay/{sub} — start checkout (stripe/paypal/bank)
- GET /pay/{sub}/{success,cancel}
- POST /payments/stripe/webhook — mark paid + extend company.active_until
- POST /payments/paypal/webhook — same
Views:
- site/billing.blade.php — invoices list with payment modal (3 methods)
- site/bank-instructions — IBAN/BIC/reference for manual transfer
- site/checkout-stub — placeholder until composer require stripe-php
- site/payment-{success,cancel}
Tenant panel:
- userMenuItems → "Facturile mele" link to /billing
2026-05-08 05:55:30 +00:00
Vasyka
d1a18848d3
fix: Filament v5 callbacks $r → $record (Plans/Subs/SuperAdmins/Companies)
...
+ central PWA: real PNG icons, SW registration, scope=/
- All `fn ($r) =>` and `fn (Type $r) =>` replaced with $record (Filament v5
injects callback params by name; $r resolved to nothing)
- /pwa/admin-{192,512}.png — generated on-the-fly with GD + DejaVuSans-Bold
- /pwa/admin-icon.svg — vector favicon
- /admin-sw.js — service worker (cache shell, network-first elsewhere)
with Service-Worker-Allowed: / header
- Manifest scope=/ + start_url=/admin → install prompt fires on Chrome/Edge/Safari
- BODY_END render hook registers SW on central panel
2026-05-08 04:37:25 +00:00
Vasyka
0ac42dde3d
fix: Forms\Get/Set->Schemas Get/Set on Central panel + rename Super-admini to Utilizatori
2026-05-07 22:08:09 +00:00
Vasyka
10426d0c91
Central panel SaaS upgrade — Plans/Subscriptions/SuperAdmins/Detail page
...
Models & migrations:
- subscriptions table (company, plan, period, amount, status, dates, invoice)
- super_admins: role enum (owner/admin/support/sales/finance) + phone + notes
- Subscription model with STATUSES/PERIODS/PAYMENT_METHODS + invoice number
generator + extends company.active_until on mark_paid
- Company model: subscriptions() + latestSubscription() relations
- SuperAdmin model: role helpers (isOwner, canManageBilling, canManageTenants)
Filament Central panel:
- PlanResource (CRUD, features checklist, limits per plan, abonati count badge)
- SubscriptionResource (CRUD, mark_paid action, navigation badge for overdue)
- SuperAdminResource (CRUD, reset password, toggle 2FA, can't self-delete)
- ViewCompany page with live stats (users/clients/vehicles/WO/parts/revenue/
storage/last_login + days_until_expiry), subscriptions history table,
config snapshot, action buttons (open/issue invoice/upload logo/suspend)
- CompanyResource: row click → view, openUrlInNewTab action, recordTitleAttribute,
empty state, view route registered
- PlatformStats widget upgraded: 6 cards (incl. MRR realized this month, overdue
invoices count, click-through to filtered tables)
- RevenueChart: 12-month MRR line chart
- RecentTenants: latest 8 tenants with click-through
- PendingPayments: pending+overdue invoices table
- Database notifications enabled + Cmd+K global search
- HEAD_END render hook: PWA manifest + theme color + emoji favicon
- /admin-manifest.json route
Seeder:
- Plans aligned with new FEATURE_OPTIONS (kanban/pdf/reports/ai/api/reverb/etc)
- 4 plans: Free / Basic / Pro / Enterprise (with proper limits)
- SuperAdmin gets role='owner'
- Demo subscription for psauto on Pro plan, marked paid this month
2026-05-07 22:02:44 +00:00
Vasyka
0399262514
Deploy 3: Onboarding wizard + empty states + docs operationale
...
- 3-step onboarding wizard at /app/onboarding (auto-redirected via
RequireOnboarding middleware on first login per tenant)
- Empty states with icon + heading + description on Client, Vehicle,
WorkOrder, Lead, Part lists
- Docs: operations/{api,i18n,2fa,monitoring}.md, stack/reverb.md
- Updated 00-index.md and journal.md with status of all 15 items
2026-05-07 20:16:03 +00:00
Vasyka
138671a125
fix: stub login route so auth middleware redirect resolves
2026-05-07 19:47:38 +00:00
Vasyka
1df7361d7e
fix: force JSON responses on /api/* routes
2026-05-07 19:36:46 +00:00
Vasyka
74c6bcb9f1
fix: api routes prefix (Laravel 12 auto-prefixes /api)
2026-05-07 19:29:04 +00:00
Vasyka
eaa05d68c1
Deploy 2: 2FA (App + Email) + REST API + CSV import-export + auto backup
...
- Filament v5 multiFactorAuthentication enabled on both panels (App + Email)
- HasAppAuthentication + HasEmailAuthentication on User and SuperAdmin
- Migration: app_authentication_secret + recovery_codes + email_authentication_at
- Sanctum REST API: /api/v1/login, /me, clients, vehicles, work-orders
- EnsureTokenMatchesTenant middleware blocks cross-tenant token usage
- CsvImportExport service: clients + vehicles bulk via plain CSV
- Import/Export buttons on Client + Vehicle list pages
- ApiTokens page in tenant panel (generate/revoke + last-used)
- BackupAllTenantsCommand + scheduler (daily 03:00, retain 14 days)
- Background scheduler in entrypoint.sh
2026-05-07 19:25:27 +00:00
Vasyka
ce4e21220f
fix: SetLocale tolerant to early-pipeline missing session
2026-05-07 19:15:43 +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
6c72fc7db1
Batch 3: Integrări placeholder + Backup tenant
...
═══ Integrări (Marketing → Integrări) ═══
- /app/integrations Page cu 6 carduri (Telegram/WhatsApp/Google Ads/FB/SMS/Webhook)
- Toggle on/off per integrare; salvare în settings.integrations JSON
- Câmpuri specifice per integrare (token/key/id/secret)
- Banner explicativ: 'placeholder UI — implementare separată'
═══ Backup tenant ═══
- TenantBackupService::export($company) → ZIP cu:
• data/ (1 JSON per tabel: clients/vehicles/leads/deals/work_orders cu sub-relații/...)
• media/ (logo + favicon)
• manifest.json (metadata + counts)
- /app/backup Page cu buton 'Descarcă backup acum'
- Streaming download cu deleteFileAfterSend
- Util pentru: backup local, migrare, audit, GDPR right-to-erasure
Total tenant routes: 104.
Toate cele ~26 module din prototip implementate (sau echivalent funcțional).
2026-05-07 17:36:00 +00:00
Vasyka
4b3201ca1c
Batch 2: Workload heatmap + Site PSauto + VIN search
...
═══ Workload heatmap (Încărcare STO) ═══
- /app/workload custom Page (group Analiză)
- Săptămână (Lu-Du) × posturi → matrice ore programate
- Heatmap colorat: verde→galben→roșu pe ratio capacity (10h/zi)
- Navigare săpt anterior/curent/următor
- Programări fără pod → row '— fără pod —' separat
═══ Site PSauto (landing public) ═══
- / pe tenant subdomain → resources/views/site/landing.blade.php
- Hero cu logo + nume + slogan; gradient theme color
- Servicii (din settings.services) — grid card-uri
- Locație/contact + program lucru standardizat
- Mărci suportate (din settings.cars)
- CTA: phone + email
- Footer cu tenant name + powered by AutoCRM
═══ VIN search ═══
- VinDecoder service: WMI hardcoded (24 producători EU/Asia/USA)
+ year codes (2001-2026) — pure offline, fără API extern
- /app/vin-search Page (group Depozit) cu:
• Input VIN cu uppercase + monospace
• Decode → producător/țară/an/serial
• Match VIN-uri din baza Vehicles
• Search piese din catalog (live debounce 300ms)
- Rezultatele linkează la editor Vehicle/Part
Total tenant routes: 102.
2026-05-07 17:16:09 +00:00
Vasyka
67da97178d
Batch 1: Procentaj + Finanțe consolidat + Recomandări
...
═══ Procentaj (markup rules) ═══
- markup_rules table cu type (category/brand/range), key, range_from/to, markup_pct, priority
- MarkupRule::bestForPart($part) — rezolvare brand → category → range → 30% default
- MarkupRule::applyToPart($part) — recalc sell_price = buy_price × (1 + pct/100)
- Filament resource sub Depozit cu form dinamic per tip
- Action 'Aplică toate regulile la stoc' — recalc tot catalogul (chunk 100)
═══ Finanțe consolidat ═══
- Custom Page /app/finance cu 4 tab-uri:
• Overview: încasări/cheltuieli/profit/datorii (4 cards)
• Cashflow: bar chart per zi (verde=in, roșu=out) + Net total
• P&L: venituri (manopere + piese) vs costuri (cost piese + cheltuieli pe categorie)
+ profit net + marjă %
• Balance: active (cash net + datorii + stoc), all-time totals
- Period filter: lună / luna trecută / an / 30 zile
═══ Recomandări ═══
- Custom Page /app/recommendations 4 sectiuni:
• Clienți pierduți (>6 luni fără WO + are istoric)
• Mașini km>100k (sugestie revizie)
• Fișe neplătite (rest > 0)
• VIP fără contact >30 zile
Total tenant routes: 100.
2026-05-07 15:30:04 +00:00
Vasyka
976c0f03e3
AI Assistant — multi-provider chat (Claude / GPT / Gemini)
...
Schema:
- ai_chats: company_id, user_id, title, provider; index pe activitate
- ai_messages: role (system/user/assistant), content, meta JSON (tokens, latency, model)
Service AiAssistantService (multi-provider):
- ask($chat, $message): persistă mesajul user, build system prompt cu context
tenant (statistici clienți/mașini/cereri/datorii), apelează API-ul providerului,
persistă răspunsul cu meta (tokens, latency)
- callClaude: api.anthropic.com/v1/messages cu claude-sonnet-4-5
- callOpenAI: api.openai.com/v1/chat/completions cu gpt-4o-mini
- callGemini: generativelanguage.googleapis.com cu gemini-1.5-flash
- Try/catch pe toate; eroare devine mesaj asistent fără să crape
System prompt include:
- Numele și orașul companiei
- Statistici curente (clienți, mașini, cereri noi, fișe active, datorii)
- Limita stricta: NU inventează date
Custom Filament Page /app/ai-assistant (group Analiză):
- Sidebar stâng: listă conversații (last 20), buton 'Nouă' + delete cu confirm
- Main: bubble chat (user dreapta albastru, asistent stânga gri)
- Meta jos pe răspuns: provider · latency · tokens
- Empty state friendly cu instrucțiuni configurare
- Loading indicator (3 dots animate) când AI răspunde
- Auto-scroll la mesaj nou
- Enter trimite, Shift+Enter newline
- Auto-titlu chat din primul mesaj user (60 chars)
Settings page extins cu secțiune 'Asistent AI':
- Provider implicit (claude/gpt/gemini)
- 3 chei API (password fields, revealable)
- Key-urile salvate în companies.settings.ai (per tenant, izolat)
2026-05-07 14:50:56 +00:00
Vasyka
7ce78c350c
Reverb infra + Kanban live refresh
...
- laravel/reverb instalat + reverb:install (config/reverb.php, channels.php)
- routes/channels.php: tenant.{slug} private channel cu auth check
user.company_id == tenant.id
- App\Events\WorkOrderUpdated implements ShouldBroadcast pe
PrivateChannel('tenant.{slug}'); broadcastAs 'work-order.updated'
- WorkOrder::booted dispatch event la fiecare update (skip if broadcast=log)
- Filament panel BODY_END inject:
- Pusher JS de la CDN (compatibil Reverb)
- Echo client conectat la Reverb (config dinamic din env)
- Subscribe pe tenant private channel; la 'work-order.updated' →
Livewire.all().forEach($refresh)
- Kanban view: wire:poll.5s (live refresh fallback) +
x-on:autocrm:wo-updated.window=$refresh (instant când WS e activ)
Pentru moment BROADCAST_CONNECTION=log în Coolify (Reverb nu e deployat).
Când deployezi Reverb container separat:
Coolify → New App → Same repo → CMD override:
php artisan reverb:start --host=0.0.0.0 --port=8080
→ FQDN: ws.service.mir.md:8080
→ Set BROADCAST_CONNECTION=reverb pe AutoCRM app
→ Real-time instant fără cod nou.
2026-05-07 14:25:26 +00:00
Vasyka
09fd0bada2
Faza 2 (din continuare): Email notifications
...
4 Mailables auto-trigger pe model events:
- WorkOrderReadyMail: la WO.status → 'ready', către client.email
• Atașat PDF fișa lucru (via WorkOrderPdfService)
• Total/achitat/rest, recomandări (warning box)
- PaymentReceivedMail: la Payment::created, confirmare cu sumă/metodă/ref
- AppointmentConfirmedMail: la Appointment::created status='scheduled'
- ServiceReminderMail: dispatch manual (vehicle, type=itp/oil/general, note)
Layout email branded (resources/views/emails/layout.blade.php):
- Header cu logo tenant + theme_color border-bottom
- Footer cu telefon/email/disclaimer
- Stiluri inline (compatibil tot mail client)
Settings page extins cu 4 toggle:
- 'Mașina e gata de ridicat'
- 'Confirmare plată primită'
- 'Programare confirmată'
- 'Reminder ITP / revizie'
Salvate în companies.settings.notify (JSON), default true.
NotificationDispatcher service centralizat:
- Verifică isEnabled() pe settings.notify[$key]
- Skip dacă client n-are email
- Try/catch + Log::warning pe eșec (nu crapă request-ul)
Mailables folosesc UsesTenantBranding trait pentru context unitar.
Test prin Mailpit: https://mailpit.service.mir.md (capturează toate).
2026-05-07 13:20:19 +00:00
Vasyka
bfe58ed286
Faza 1 (din lista de continuare): Calendar vizual cu FullCalendar 6
...
- Custom Filament Page CalendarBoard la /app/calendar-board (group CRM)
- FullCalendar 6.1.15 din CDN + locale RO
- View-uri: zi (timeGridDay), săptămână (timeGridWeek default), lună
- Programări colorate per maistru (din User.color)
- Live event loading: Livewire $wire.getEvents(start, end)
- Drag-drop reschedule: eventDrop → $wire.moveEvent(id, start, end)
- Resize event (extinde durată): eventResize
- Click slot gol → quick-create modal cu form Filament populat cu data/timpul
- Câmpuri: title, time_start/end, client (live), vehicle (filtrat după client),
master, post, notes
- Click event → confirm + delete
- Toolbar: prev/next/today + month/week/day switch
- 07:00–21:00 grid cu sloturi 30 min, today indicator live
- Modal stilizat (CSS scoped) cu close button + ESC
Total tenant routes: 93.
2026-05-07 13:10:27 +00:00
Vasyka
19a7afac27
Faza 8: PDF generation pentru fișa lucru (DomPDF)
...
- barryvdh/laravel-dompdf instalat
- WorkOrderPdfService: încarcă WO cu toate relațiile (works/parts/payments),
embed-ează logo ca data URI, foloseste theme_color din settings
- Blade template /resources/views/pdf/work-order.blade.php:
- Header cu logo + date companie + nr fișă + data
- Box-uri client + auto (kilometraj/VIN/plate)
- Plângere + diagnostic
- Tabel manopere (h, preț/h, total) cu maistru pe fiecare rând
- Tabel piese (cod, brand, qty, preț, total)
- Box total cu discount + plăți efectuate + rest de achitat
- Block recomandări cu fundal galben (warning)
- Linii semnătură client + maistru
- Footer cu timestamp generare
- Action 'PDF' (icon descărcare) pe rând în lista de WO
- Action 'Descarcă PDF' în header-ul paginii Edit WO
2026-05-07 13:01:42 +00:00
Vasyka
f1d196f018
Faza 7: White-label per tenant — logo + theme color dinamic
...
- spatie/laravel-medialibrary instalat (migration media table)
- filament/spatie-laravel-media-library-plugin
- Company implements HasMedia + InteractsWithMedia
- collections: 'logo' + 'favicon' (singleFile)
- getLogoUrl() / getFaviconUrl() helpers
- Settings page extins: secțiune Logo & favicon cu FileUpload
- On save: clear collection + addMedia from temp upload + cleanup tmp file
- TenantPanelProvider render hooks:
- HEAD_END: theme-color meta + favicon + CSS vars override
(--primary-50 → --primary-950 generate din hex theme_color)
- SIDEBAR_LOGO_BEFORE: afișare logo upload-uit, max-height 56px
Cum funcționează:
- Tenant uploadează logo în Settings
- La fiecare request, render hook injectează <style> cu CSS vars custom
- Filament respectă --primary-* → toate butoanele/badge-urile primesc culoarea brand
- Logo apare deasupra meniului (sidebar)
2026-05-07 12:51:19 +00:00
Vasyka
a7bb9838f4
fix reports view: inline CSS scoped (rp-*) instead of Tailwind utilities
2026-05-07 12:07:08 +00:00
Vasyka
a9b523cc9b
fix kanban styling: inline CSS (Tailwind JIT doesn't scan views/filament/)
2026-05-07 11:52:46 +00:00
Vasyka
f12bf17948
fix kanban: use Alpine @drag* + $wire (DOM ondrop has no $wire access)
2026-05-07 11:29:21 +00:00
Vasyka
cbc66b13c3
fix: Spatie ActivityLog v5 namespace — Models\Concerns\LogsActivity (was Traits\LogsActivity in v4)
...
Spatie\Activitylog\Traits\LogsActivity → Spatie\Activitylog\Models\Concerns\LogsActivity
Spatie\Activitylog\LogOptions → Spatie\Activitylog\Support\LogOptions
2026-05-07 11:22:36 +00:00
Vasyka
66dd0772b5
debug: /__check-activitylog to verify package install
2026-05-07 10:39:54 +00:00