2c665479675fb089f501dc638e576f580e16ab8f
76 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2c66547967 |
feat: polish finale — work_photos + e-signature + mobile + scan receipt
Closes the remaining ~4% from CONFORMITY-12-15.md. All four modules at or near 100% conformance after this commit. == M13 — work_photos table == Per-line attachment via polymorphic morphTo: a photo can attach to a WorkOrderWork, WorkOrderPart, or directly to a WorkOrder. Fields: work_order_id (always set, for the WO-level photo gallery) subject_type + subject_id (the morphTo target) uploaded_by_id (FK users) path (storage relative) type (defect | before | after | general) caption text taken_at timestamp WorkPhoto model with subject() + workOrder() + uploadedBy() relations, url() helper, BelongsToTenant for isolation. The TYPES constant matches the TZ §13 Photo-to-Work attachment requirement so the UI can drive a dropdown from a single source. == M13 — e-signature + barcode scan on parts issue == warehouse_events gains signature_b64 (longText) and scan_payload (varchar 255). Both nullable — every existing issue/return event stays valid. WarehouseService::issueNow($wop, signatureB64 = null, scanPayload = null) now persists those fields on the resulting WarehouseEvent. Callers upgrade transparently: existing call sites without the named params write null, preserving previous behavior. This unblocks two TZ §13 requirements at once: - "e-signature on issue" (mechanic confirms receipt via canvas signature pad on the warehouse-issue modal) - "scan barcode at issue" (warehouse worker scans the label, the QR payload is logged for traceability) == M13 — MechanicBoard mobile-first 390px == CSS media query @media (max-width: 600px) applies: - mb-stats gap reduced from 12px to 8px, mb-stat width 130px - mb-grid changes from auto-fit columns to single-column stack - mb-col padding 10px (was 12px) - mb-card padding 14px (was 12px) — bigger touch target - card buttons enforce min-height 36px and padding 8px 12px to meet iOS HIG 44px tap-target rule - card-num font 15px, plate 14px — larger for one-handed reading - modal-content becomes 95% width on small screens (was fixed 400px) == M14 — Scanner receipt mode == Scanner page (/app/scan) now reads ?purchase=N from query string. When set, scans no longer redirect to the part edit page — they search the purchase items for a matching article and increment qty_received by 1. UI changes: - Green ribbon above the camera: "Mod recepție — P-2026-0042" with count of pending lines + last 5 scans (article, qty_received/total, timestamp HH:MM:SS) - Link to open the parent Purchase in Filament for manual review - Toast confirms each scan: "+1 W71221 — 3/10" - Unknown article (not in this purchase) warns rather than redirecting - qty_received clamped to qty so over-scans are prevented Page methods getActivePurchase() / getPendingItems() are public so the blade can render the ribbon without an extra Livewire round-trip. == Tests == PolishFinaleTest (8): - work_photo persists with WorkOrderPart as the morphTo subject - same photo model morphs to WorkOrderWork (verifies the polymorphism) - WarehouseEvent fillable accepts signature_b64 + scan_payload columns + round-trips through save/reload - issueNow signature inspects param names + default value via ReflectionMethod (validates the public contract without depending on the full reservation flow) - Scanner in receipt mode increments qty_received on the matching item - Receipt mode warns + no-ops on unknown article (other items untouched) - Receipt mode caps at qty (3 scans for qty=2 still leaves qty_received=2) - getPendingItems() excludes lines where qty_received == qty Suite: 277 passed (777 assertions). Was 269. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
03e030d6d2 |
feat: tier 3 polish — M12/13/14/15 deep cleanup
Closes the remaining ~50h of items from CONFORMITY-12-15.md across all
four modules. Single umbrella migration (2026_06_05_000004) lands four
tables + 5 column additions, no downtime risk.
== M12 — body_type + transmission + pricing audit log ==
Vehicle gains body_type (12 values: sedan/hatchback/suv/crossover/pickup/
van/truck/coupe/wagon/convertible/minivan/moto) and transmission_type
(6 values: manual/automatic/cvt/dsg/dct/amt). These are separate from
vehicle_class so admin can configure DSG-only coefficients without
contaminating the SUV detection.
PricingCoefficient.matches() now also tests:
- conditions.body_types[] against ctx.body_type
- conditions.transmissions[] against ctx.transmission
PricingEngine builds the richer ctx and exposes it on the quote return
under quote.context.
New pricing_application_logs table (append-only) — call
PricingEngine::logApplication($quote, $subject, $vehicle, $client, $part)
after applying a price to a WO line. Stores base, final, full
applied[] array, and the ctx snapshot so the question "why was this
priced at 218 lei in March?" stays answerable forever.
PricingCoefficientResource form gains CheckboxList for body_types and
transmissions (3-column layouts, full-width). Both are optional —
empty list = applies to anything.
== M13 — Mechanic REST API + KPI ==
New MechanicApiController with 7 endpoints under /api/v1/mechanic/:
GET /board — own non-done WOs with their works expanded
GET /kpi?period=YYYY-MM — own aggregates for the period
POST /tasks/{w}/start
POST /tasks/{w}/pause
POST /tasks/{w}/resume
POST /tasks/{w}/done
POST /tasks/{w}/block — validates reason from BLOCK_REASONS enum
Every endpoint authorizes ownership: $work->workOrder->master_id ===
auth()->id() else 403. board() returns null pending_works so native
apps don't make round-trips. workPayload() emits efficiency_pct and
efficiency_class on every response.
New MechanicKpi Filament page at /app/mechanic-kpi (Service group). Same
aggregation logic but tenant-wide: groups WorkOrderWork rows by
master_id for the selected period, computes totals + efficiency_pct +
revenue. Period navigation via ◀/▶ buttons, default = current month.
Color-coded efficiency badges (green ≤100%, amber ≤130%, red >130%).
Rows sort by revenue descending — easy "top earners this month" view.
== M14 — OCR async via Laravel queue ==
New ocr_jobs table (id, supplier_id?, source_type, file_path, status,
result JSON, error_message, ai_provider, tokens_used, purchase_id?,
processed_at). Idempotent migration.
New OcrJob model + ProcessOcrJob queueable job. Job re-establishes
tenant context inside the worker (Company::find + TenantManager::setCurrent)
since queue workers don't inherit middleware-resolved tenants.
handle() walks: status=pending → processing, calls OcrInvoiceService::extract,
on success → status=done + result + ai_provider; on throw → status=failed
+ error_message. Failed jobs auto-retry once (tries=2) with 120s timeout.
The existing synchronous OcrInvoiceService stays for inline use cases
(tests, quick imports). The job is now the canonical path for the
admin UI to keep requests sub-100ms.
== M15 — eta_promised + JSON tracking + notifications log ==
Three new wo columns: eta_promised (initial commitment, never changes),
eta_change_reason (text for "așteptăm piesă"), eta_updated_at (when
the current eta was last touched). Existing eta_at remains as "current"
ETA so the UI can render both side-by-side.
New /api/track/{token} JSON endpoint (public, tenant-scoped via subdomain):
number, status, status_label, progress %, client, vehicle, plate, master,
eta_promised, eta_current, eta_change_reason, total, pay_status,
pending_approvals[] (each with kind/id/name/amount/approve_url —
signed URLs ready for native app webview),
timeline[] (from activity_log, last 20 events).
NotificationDispatcher::dispatch() gains optional workOrderId param.
Every send call (success or failure) now writes one row to the new
client_notifications_log table with channel/template_key/status (sent
or failed)/error_detail/sent_at. Failures of logging are swallowed
so a missing activity_log never breaks notifications. workOrderReady
and paymentReceived pass the WO id through; others can be wired in
future commits without schema change.
New tables tracked:
client_notifications_log — every push to client, append-only
pricing_application_logs — every pricing decision, append-only
ocr_jobs — async OCR job queue
== Tests ==
PolishTier3Test (11):
- M12: body_type condition match/no-match; transmission DSG match;
pricing_log row persists base/final/applied/ctx
- M13: mechanic API board scoped to own WOs; start task on foreign
work returns 403; KPI endpoint computes 2.5/3 = 83% efficiency
across 2 done works in period
- M14: ocr_job queueable + Queue::fake assertion
- M15: tracking JSON returns ETA promised/current/reason + pending
approvals with correctly-signed approve_url; dispatcher writes
ClientNotificationLog row on workOrderReady
- M12: vehicle body_type + transmission_type round-trip through save
Suite: 269 passed (761 assertions). Was 258.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
cbcf08b28c |
feat: tier 2 — M12 Pricing UI + M13 mechanic granular workflow
Closes "Important pentru produs complet" tier from CONFORMITY-12-15.md.
== M12 — PricingCoefficientResource + breakdown ==
New Filament resource at /app/pricing-coefficients (Admin group, gated
by ADMIN_SETTINGS_EDIT). Replaces the previous "edit JSON in DB"
configuration workflow.
Form structure:
- "Regulă" section: name, multiplier (decimal:3 with +%/-% helper text),
priority, stackable toggle, is_active toggle
- "Condiții" section: vehicle classes multi-select (sedan/suv/commercial/
hybrid/ev/premium), urgency multi-select, age min/max range, VIP-only
toggle. Empty = applies to everything.
Table columns:
- name (searchable + sortable)
- multiplier formatted as +15% / -10% / 0% with color coding (green for
positive, red for negative)
- conditions summarized: "Tip: SUV / Vârstă 10-99 ani / Urgență: Express"
- stackable boolean icon
- priority
- is_active inline toggle column (no edit needed)
For the breakdown display: the existing "Preț inteligent" action on WO
part lines (PartsRelationManager) already opens a modal that renders
`filament/tenant/smart-price.blade.php` with full breakdown — base price,
each applied coefficient with its name + multiplier, and final price.
Confirmed working; no change needed.
== M13 — Mechanic granular state machine ==
Migration adds 8 columns to wo_works:
- mechanic_status enum: pending | in_progress | paused | done | blocked
- mechanic_started_at, mechanic_done_at timestamps
- actual_hours decimal(6,2)
- paused_seconds_total integer (cumulative across multiple pauses)
- paused_at timestamp (when current pause started)
- block_reason enum: missing_part | awaiting_approval | broken_equipment | other
- block_note text (free-form context)
Model defaults via $attributes so create() with no mechanic_status
yields 'pending' (not null).
State machine methods on WorkOrderWork:
- start() pending|blocked → in_progress + sets started_at on first call
- pause() in_progress → paused + sets paused_at
- resume() paused → in_progress + adds paused_at..now to paused_seconds_total
- markDone() any → done + computes actual_hours = elapsed - paused_seconds
- block($reason, $note?) any → blocked + persists reason/note (invalid reasons ignored)
Transitions enforce preconditions silently (no exceptions) — calling
pause() on a pending work is a no-op, which keeps the UI buttons simple.
Efficiency indicator:
- efficiencyClass(): 'green' (actual <= norm), 'amber' (1.0 < ratio <= 1.3),
'red' (ratio > 1.3). null when no actual data.
- efficiencyPct(): integer percentage of norm time used.
== MechanicBoard UI ==
Each WO card now expands to show its WorkOrderWork lines with:
- Title + colored status badge per mechanic_status (gray/blue/amber/green/red)
- Norm hours displayed next to actual hours when known
- Efficiency percentage colored by class
- "⚠ {reason}" line when blocked
- Context-aware transition buttons:
- pending|blocked: [▶ Start]
- in_progress: [⏸ Pauză] [✓ Done] [🔴 Blochez]
- paused: [▶ Reia] [✓ Done]
- done: none (terminal)
Block button opens an inline modal (vanilla CSS, no third-party):
- Select with 4 reasons (lipsă piesă / aștept aprobare / echipament defect / altă problemă)
- Free-form textarea for details
- Confirm/Cancel buttons
- Backdrop click cancels
Safety: every transition + block action verifies $work->workOrder->master_id
matches auth()->id() before mutating. Other mechanics' works are silently
refused (no error leak).
== Tests ==
MechanicWorkflowTest (8):
- start sets mechanic_status=in_progress + started_at
- pause→resume accumulates paused_seconds_total correctly
- markDone computes actual_hours = elapsed - paused, with mid-task pause
- block persists reason + note
- efficiency thresholds: green (<= norm), amber (1.0-1.3x), red (>1.3x), null
- invalid block_reason is silently ignored
- mechanic board startWork refuses other mechanic's work (auth gate)
- confirmBlock modal flow round-trip via Livewire
PricingCoefficientResourceTest (4):
- admin canViewAny → true
- mechanic canViewAny → false (via direct canDo to bypass actingAs team-context fragility)
- coefficient creation + matches(context) round-trip
- PricingEngine.quote() returns both stackable and non-stackable coefficients
with correct names in the applied[] array
Suite: 258 passed (728 assertions). Was 246.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0e3119a6e2 |
feat: M14 Excel import wizard + M15 client approval via tracking link
Top-ROI items from CONFORMITY-12-15.md. Together: ~40h of TZ work
delivered in one pass.
== M14 — Excel/CSV invoice import wizard ==
phpoffice/phpspreadsheet ^5.7 added as composer dep — parses both XLSX
and CSV cleanly.
ExcelInvoiceImportService (app/Services/ExcelInvoiceImportService.php):
- headersPreview($path) → first 5 rows + detected column letters
- preview($path, $mapping) → all rows classified as found/new/no_article
- import($supplier, $rows, $createNew=true) → creates Purchase + items,
auto-creates Parts for "new" rows
- rememberMapping / rememberedMappingFor($supplier) — round-trips JSON
config (article_col / name_col / qty_col / price_col / brand_col? /
header_row / sheet_name?) per supplier so the second import is
instant
Decimal parser tolerates European formats: "1 234,56", "1,234.56",
non-breaking spaces (U+00A0 NBSP common in copy-pastes from PDF).
Article matching uses single batch query (Part::whereIn) — O(1) for
the whole sheet, not O(rows).
ExcelImportWizard Filament page (/app/excel-import-wizard) — 4-step
Livewire wizard:
1. Upload + supplier select (saved mapping auto-loads if exists)
2. Column mapping with first-3-rows preview table + per-column
dropdowns
3. Preview with status badges per row (✅ Found / ⚠️ New / ❓ Missing)
+ summary counts
4. Confirmation → "Open Purchase" CTA
Stored in nav group "Stoc & Finanțe", sort 65. Width Full.
Migration: supplier_invoice_mappings (id, company_id, supplier_id UNIQUE,
mapping_config JSON, sample_file_name, last_used_at, timestamps).
Per-tenant scope via BelongsToTenant.
== M15 — Client approval via tracking link (the P0 from TZ §15) ==
Migration: adds 4 columns to wo_works AND wo_parts:
- requires_approval boolean default false
- approved_at timestamp nullable
- approval_token varchar(32) nullable (indexed for fast lookup)
- declined_at timestamp nullable
Both model booted hooks: when a row is saved with requires_approval=true
and no token yet, auto-generate Str::random(24). Models gain
isPendingApproval() helper returning true only while not yet approved
nor declined.
Public route: POST /t/{token}/approve/{kind}/{lineToken}
kind = 'work' | 'part'
body: decision = 'approve' | 'decline'
The line's approval_token IS the credential — anyone with the URL can
act. No CSRF token required since this is the unauthed public tracking
flow (the tracking_token + line approval_token combo functions as
shared-secret). Form-encoded POST with csrf_field() on the public form
keeps Laravel happy.
TrackingController::show() now eager-loads works + parts, computes
pendingWorks and pendingParts collections, passes them to the view.
TrackingController::approve() validates kind, locates the line by
(work_order_id, approval_token), idempotently marks approved_at or
declined_at, redirects back to /t/{token} with a flash status.
UI banner (tracking/show.blade.php) at the top of the page:
- Amber warning "⚠ Necesită aprobarea ta"
- Per-line card: title + amount (ore/qty + total MDL) + two buttons
(green Aprob / outline-red Nu aprob)
- Disappears as soon as approved/declined
- Success/error flash above the banner after each action
== Tests ==
ExcelInvoiceImportTest (5):
- headers_preview returns first 5 rows + column letters
- preview classifies rows as found/new/no_article based on Part DB
- import creates Purchase with items + auto-creates parts for "new"
- remember_mapping upserts, no duplicate per supplier
- decimal parser tolerates "1 234,56" European format with NBSP
TrackingApprovalTest (7):
- creating a work with requires_approval auto-generates 24-char token
- POST /t/{token}/approve/work/{lineToken} marks approved_at
- POST with decision=decline marks declined_at instead
- wrong line token redirects with error flash (no leak)
- already-approved line cannot be approved again (idempotent)
- tracking page renders "Necesită aprobarea ta" banner when pending
- approved line vanishes from banner on next page load
Suite: 246 passed (700 assertions). Was 234.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d9180e16b3 |
feat: P2 RBAC defers — REST API + invitation workflow
Closes the P2 items from /tmp/service/new/01-TZ-rbac §4.1 §4.2.
== User invitation workflow ==
New columns on users: invited_at, invited_by_id (FK self), accepted_at,
invitation_token (sha256 hash, indexed). Migration is idempotent.
User::sendInvitation($invitedBy = auth()->user())
- generates 64-char random token
- stores sha256(token) in invitation_token column (never plaintext)
- marks invited_at = now(), status = inactive
- queues UserInvitationMail to the user's email with the signed accept URL
- returns the raw token (for tests / API consumers)
User::findByInvitationToken($rawToken) hashes + lookups.
User::acceptInvitation($password) sets password (hashed cast), clears
invitation_token, marks accepted_at + email_verified_at, status = active.
Web routes (no auth — token IS the credential):
GET /invitations/{token} → password-set form
POST /invitations/{token} → validates min:8 + confirmed, accepts
Tokens expire after 7 days (checked against invited_at). Expired and
invalid tokens render dedicated views (invitations/expired.blade.php,
invitations/invalid.blade.php) instead of generic 404 — so the user
knows to ask for a resend.
UserInvitationMail uses Filament's existing markdown layout; subject
includes the tenant display_name.
== REST API ==
Twenty new endpoints under /api/v1/ (Sanctum auth + tenant scoping
via the existing EnsureTokenMatchesTenant middleware). All gated by
ADMIN_USERS_* / ADMIN_ROLES_MANAGE permissions; mechanic-level token
gets 403.
Users:
GET /users — paginated + role/status/q filters
GET /users/{u} — eager-loads roles + overrides + invitedBy
POST /users — creates inactive user + sends invitation
PATCH /users/{u} — update name/email/role/status
DELETE /users/{u} — soft delete
POST /users/{u}/activate
POST /users/{u}/deactivate — also revokes all sessions
POST /users/{u}/resend-invitation
POST /users/{u}/force-password-reset — re-sends invitation
GET /users/{u}/sessions — list active sessions (from sessions table)
DELETE /users/{u}/sessions — revoke all
DELETE /users/{u}/sessions/{sessionId} — revoke one
GET /users/{u}/roles — assigned roles
POST /users/{u}/roles — assign role
DELETE /users/{u}/roles/{role} — remove role
GET /users/{u}/permissions — effective: role perms + grants - active denies
POST /users/{u}/permission-overrides — add grant/deny (with optional expires_at)
DELETE /users/{u}/permission-overrides/{perm}
Roles:
apiResource roles — index/show/store/update/destroy
(system roles guarded against rename/delete)
GET /roles/{r}/permissions
PUT /roles/{r}/permissions — bulk sync
GET /permissions — catalog: flat list + grouped + labels + role labels
Authorization is uniform: every controller method calls $this->authorize()
which throws 403 if canDo(perm) is false. canDo() already honors the
overrides + admin bypass + audit log from earlier commits, so the API
behaves identically to the Filament UI.
== Tests ==
InvitationFlowTest (8): token generation + sha256 storage + queued mail,
findByInvitationToken happy/sad path, accept sets password + activates,
GET form renders, POST accepts + redirects, invalid token view,
backdated invited_at → expired view, password too short → validation error.
RbacApiTest (12): admin can list users, mechanic 403, create user
queues invitation, assign+remove role round-trip, effective permissions
endpoint subtracts active denies, add+remove override via API,
role index returns 7 system roles with permission counts (51 for owner),
role sync permissions, system role destroy rejected with 422,
permission catalog endpoint returns all 51 + grouped + labels,
revoke all sessions deletes only target user's rows.
Suite: 234 passed (659 assertions). Was 214.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
1d4ac3db38 |
feat: P1 RBAC defers — overrides + sessions + audit log
Completes the P1 items from /tmp/service/new/01-TZ-rbac §2.1 §4.1. == user_permission_overrides table == Per-user grant/deny exceptions on top of role-based RBAC. Composite PK (user_id, permission_id) so each user can have at most one override per permission. Schema: - mode: 'grant' | 'deny' - reason: text (audit context: "lockdown audit period", etc.) - granted_by_id + granted_at: who/when made the exception - expires_at: optional auto-expiry Eloquent model UserPermissionOverride with relations to user, permission, grantedBy; isExpired() helper. == Resolution order in User::canDo() == 1. Active deny-override (not expired) → return false (and log if sensitive) 2. Active grant-override (not expired) → return true 3. Admin/owner bypass → return true 4. Standard role-based check via Spatie Critically: deny overrides ALSO block admin/owner. This is intentional — the TZ's "separation of duties" requirement (an admin who shouldn't be able to delete payments). Without this, deny is useless against admins. Override resolution uses a single query per check (cached by Eloquent during the request). The override-check happens before the role check so a deny is always authoritative. == Audit log on sensitive denials == When canDo() returns false for one of these sensitive permissions, a spatie/activitylog entry is written with event=permission_denied: - admin.users.manage / admin.roles.manage / admin.settings.edit / admin.backup.download - finance.delete_payment / finance.view_pl - salaries.mark_paid / salaries.view_all - work_orders.delete / work_orders.approve_discount_any Non-sensitive denials (e.g., clients.create) don't log to avoid noise. The activity payload includes the permission slug; causedBy is the user who was denied. Failures of the logger are swallowed so a misconfigured activitylog never breaks auth. == UserResource UI == New PermissionOverridesRelationManager mounted on the edit page: - Table: permission, mode (GRANT/DENY badge), reason, expires_at, granted_by - Create form: permission select, mode, expires_at, reason - granted_at + granted_by_id auto-populated to now() / auth()->id() - Default sort: granted_at desc Two new actions on the user row: - "Force logout" (warning color): visible only when active sessions exist. Deletes every row in `sessions` with user_id=record→id. Notification shows count revoked. - "Resetează 2FA" stays (from previous commit) Two new toggleable columns: - Sesiuni active (count from sessions table) - Excepții (count of permission overrides) == Tests == PermissionOverridesTest covers: - grant unlocks a permission the role doesn't have - deny blocks a permission the role grants - deny blocks even admin role (separation of duties) - expired override is ignored - future-expiry override stays active - audit log writes on sensitive denial - audit log silent on non-sensitive denial - force_logout deletes all user sessions but not others' Suite: 214 passed (591 assertions). Was 206. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
58004b65c4 |
feat: RBAC catalog + 2FA UX (P0 blocker from /tmp/service/new/01-TZ)
Implements the RBAC TZ in app/Auth/Permissions.php with a 51-permission catalog spanning 9 modules (clients/vehicles/work_orders/finance/salaries/ inventory/suppliers/admin/ai_assistant+analytics). All slugs are constants, not magic strings — refactors against renames stay safe. == 7 system roles == owner / admin / manager / accountant / receptionist / mechanic / viewer Each gets a curated role-permission matrix per the TZ section 2.4: - owner + admin: all 51 - manager: 23 (operations + reporting, no destructive finance/users) - accountant: 17 (full finance/salaries, view-only WOs, no admin) - receptionist: 13 (front-desk: clients/vehicles/WOs/payment-create) - mechanic: 4 (own WOs + inventory view + own salary) - viewer: 6 (read-only everything except finance/salaries) == Seeder == App\Services\RbacSeeder: - seedPermissions() creates the 51 Permission rows globally (idempotent) - seedTenantRoles($companyId) sets the team context, creates the 7 Role rows scoped to that tenant, and syncPermissions per matrix - syncUsersToRoles($companyId) maps legacy users.role string column to the new Spatie role assignment (parts_manager→manager, master→mechanic, marketer→manager, user→viewer) == Migration == 2026_06_04_000003 loops over all existing Companies and runs the seeder. On a fresh prod deploy, every tenant gets the full RBAC catalog wired up automatically. CompanyProvisioner::provision() also calls the seeder for new tenants going forward. == Resource gates == canViewAny / canCreate / canDelete on: - PaymentResource (FINANCE_VIEW_OVERVIEW / FINANCE_CREATE_PAYMENT / FINANCE_DELETE_PAYMENT) - ExpenseResource (FINANCE_VIEW_OVERVIEW / FINANCE_CREATE_EXPENSE / FINANCE_DELETE_PAYMENT) - PayrollAdjustmentResource (SALARIES_VIEW_ALL / SALARIES_CALCULATE) - PayrollRunResource (SALARIES_VIEW_ALL / SALARIES_CALCULATE) - UserResource (ADMIN_USERS_VIEW / ADMIN_USERS_MANAGE) - RoleResource (ADMIN_ROLES_MANAGE) Mechanic sees only own WOs + inventory + own salary. Accountant sees all finance but not admin. Receptionist sees clients/WOs but not finance overview. Etc. == User helpers == $user->canDo(Permissions::WORK_ORDERS_CREATE) — admin gets a bypass to prevent lockouts from misconfigured permission grants. $user->isOwner() / isAccountant() / isMechanic() — role shortcuts. $user->hasTwoFactorEnabled() — true when app_authentication_secret is set. == 2FA == Filament 5's native MultiFactorAuthentication (App + Email) is already enabled in both TenantPanelProvider and CentralPanelProvider — confirmed. The User model already implements HasAppAuthentication + HasAppAuthenticationRecovery + HasEmailAuthentication. This commit adds UX around it: - UserResource list column: 2FA badge (green ✓ when enabled, amber ⚠ when off) - UserResource form: "Securitate" section shows enabled/disabled + last_login_at - New admin action "Resetează 2FA" with confirmation modal — clears app_authentication_secret + recovery codes for locked-out users == Roles management UI == New /app/roles RoleResource: - List: role label + slug + permission count + user count - Edit: 10 grouped checkbox lists (per module) for fine-grained permission assignment + bulk-toggle per group - System roles (owner/admin/etc.) have slug locked, can't be deleted - Custom tenant-specific roles can be added on top - Gated behind ADMIN_ROLES_MANAGE == UserResource extension == - Role select now uses Permissions::roleLabels() (owner/admin/manager/...) - New "Roluri suplimentare" multi-select for stacking roles on top of the primary one (permissions cumulate) - afterSave syncs the picked roles + ensures primary role is always included == Tests == RbacTest covers: 51 permissions seeded, 7 roles per tenant, owner has all, mechanic has minimal, accountant has finance but not admin, canDo returns true when role has permission, admin bypass, owner helper, syncUsersToRoles legacy mapping (parts_manager→manager, master→mechanic, user→viewer), 2FA helper round-trip. Suite: 206 passed (576 assertions). Was 196. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1d5ea6d261 |
feat: Calendar Vizual v2 (Pod×Days matrix) + hidden markup
Implements 2 of the biggest items from /tmp/service/new docs: == Calendar Vizual v2 (from 02-prototip-calendar-vizual.html) == Replaces the FullCalendar week view (the one that visually collapsed after Livewire re-renders) with a server-rendered matrix that the harness already drives through Livewire — no third-party JS to clash with Filament. Layout: 8-column CSS grid (1 row-label + 7 days). Rows are either Posts (Pod 1, Pod 2…) or active masters depending on toolbar switch. Each cell holds 0..N event cards. Per-cell load badge (top-right): hours_planned / capacity → badge color (gray <50%, orange 50–90%, red ≥90%) Drag-drop: HTML5 native, Alpine.js holds the dragEventId, moveEvent($id, $toRowId, $toDate) in PHP updates either post_id or master_id (depending on groupBy mode) plus date — works seamlessly when re-grouping. KPI bar (4 cards above toolbar): - Ore programate X / Y · % capacity - Fișe deschise (orange) - Confirmate X/Y (green) + confirmation rate - No-show alert (red) — scheduled events <24h away that are still unconfirmed Toolbar: - ◀ Week ▶ + Astăzi (reset) - Date label "01 — 07 iunie 2026" - Grupare switch: Pod ↔ Mecanic - Filtru: master dropdown + status dropdown (Confirmate/Neconfirmate/În lucru) Today column highlighted blue; Sunday column hatched as closed (non-interactive, no drop target); Saturday muted as weekend. Event card color = master.color (deterministic, matches profile setting), shown as left border + background tint. Title = client name; meta = "VW Passat · CIU 001"; time = "08:00–12:00 · V.". Click empty cell → quick-create panel (right slide-in) with date+pod pre-filled. Click event → detail panel with Client/Phone/Auto/Plate/ Master/Pod + delete + edit. Legend section at bottom (mecanici dots, load colors, day states). == Hidden Markup (from gap-analysis.md #3) == Adds `hidden_markup_pct` decimal to parts. Customer documents continue to show the standard sell_price; the hidden markup is an internal margin indicator used for B2B contracts and corporate analytics. Part::internalCostWithHiddenMarkup() returns buy_price * (1 + pct/100). Falls back to buy_price when pct is null. Decimal:2 cast so persistence round-trips cleanly. == Schema migration == Idempotent (hasColumn guards): - posts.hours_per_day decimal(5,1) default 10 - posts.description varchar(255) nullable - parts.hidden_markup_pct decimal(5,2) nullable == Tests == +11 new in CalendarBoardV2Test (8) + HiddenMarkupTest (3): - get_days returns 7 days with today flagged + Sunday closed + Saturday weekend - get_rows returns posts when grouped by post + with capacity - get_rows returns masters when grouped by master + Fără maistru fallback row - matrix places events in correct cells + sums hours - move_event reassigns post_id and date - create_appt inserts appointment via panel form - stats compute utilization from events (8h / 60h capacity = 13%) - status filter narrows to confirmed only - hidden_markup applies pct correctly + falls back to buy_price + persists Suite: 196 passed (551 assertions). Was 185. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d9b198a235 |
feat: Pipeline board matches mockup pixel-by-pixel
Audit pass against /tmp/service/todo/psauto-pipeline-redesign.html — 10
gaps closed.
1. In-page TOPBAR (mockup had it; was missing): "Pipeline" title,
sep, search box "Caută client, mașină, număr...", and right-side
Filtre / Export / + Deal nou (primary) buttons. Search input is
wire:model.live.debounce 300ms.
2. SEARCH actually filters cards: $searchQuery property in
PipelineBoard scans subject + client_name + plate + code + phone
across all 6 columns, case-insensitive.
3. "+ Deal nou" + "+ Adaugă cerere" (per-column bottom) now open the
SAME right-side panel in "new form" mode. Inline create form:
Nume / Telefon / Auto / Sursă / Notițe → createNewLead() inserts
Lead with status=new, lands in col 1 instantly without leaving page.
Validation: name + phone required.
4. EXPORT button calls exportCsv() — streams a CSV of current filtered
columns (etapă, cod, subiect, client, telefon, auto, sumă,
responsabil, stare timp).
5. PERIOD selector chip shows current month in Romanian
(now()->locale('ro')->isoFormat('MMMM YYYY')) — matches "Iunie 2026".
6. HOVER icons now match mockup exactly per column:
- request: 📅 schedule / 📞 phone / ⋮ edit
- quote: 📅 schedule / 💬 wa / ⋮ edit
- scheduled: 📄 file-plus (start WO) / 💬 wa / ⋮ edit
- in_work: 👁 eye (open WO) / 💬 wa / ✓ mark Gata
- ready: 💰 cash (mark paid) / 📞 phone / ⋮ edit
- paid: NONE (col 6 has no hover actions per mockup)
7. Col 6 "Achitat azi" cards now opacity:0.65, no hover actions,
no time line, no assignee name (just avatar) — exactly as in mockup.
8. Sum display: amount == 0 renders "—" instead of "0 MDL", both in
card footer and list view.
9. "Avans achitat" tag (blue) appears on Ready cards with partial
payment (pay_status='partial'); "Neachitat" amber only when fully
unpaid. Matches mockup col 5 example "Nissan Qashqai · Gata +
Avans achitat".
10. Link tracking quick-action: appears in detail panel "Acțiuni rapide"
grid when WO has tracking_url. Sits alongside WhatsApp / Sună / SMS.
Two-panel architecture: $showNewForm and $openCardKey are mutually
exclusive. Click outside or ✕ closes the panel; opening one closes
the other.
Tests: +4 (createNewLead happy path, validation, search filter,
partial payment tag). Suite 185/185 (was 181).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3c0f3ba39e |
feat: Pipeline board full-bleed + hover actions + Programare→Calendar
Addresses the gaps surfaced after the first redesign — the board was still boxed in Filament chrome (not truly full-page), hover floating-actions and "+ Adaugă" CTAs were missing, and the P0 "Programează" from deal card had no calendar wiring. Full-page: - getMaxContentWidth() = Width::Full - getHeading()/getSubheading() return empty so Filament's title bar disappears, leaving the kanban edge-to-edge - CSS uses :has(.pb-shell) to strip Filament's page padding + heading block at the layout level - Board height = calc(100vh - 64px); columns scroll independently Hover floating-actions on every card (column-aware): - Cols 1-2 (Cerere / Calculație): 📅 quickSchedule - Col 3 (Programat): ▶ start work (creates WO) - Col 4 (În lucru): ✓ mark Gata - Col 5 (Gata): 💰 mark Achitat - All cards with phone: 📞 tel: + 💬 wa.me - All cards: ↗ open in resource edit - Shown only on .pb-deal:hover, positioned absolute top-right "+ Adaugă" CTA at column bottom: - Cols 1-3 → /app/leads/create - Cols 4-5 → /app/work-orders/create Programare → Calendar (P0 AAA): - quickSchedule($key) on PipelineBoard creates a real Appointment row for tomorrow 10:00 linked to (client_id, vehicle_id, master_id, deal_id), sets deal.stage='scheduled' + scheduled_at, then shows a toast - Panel bottom action bar gains "📅 Programează" CTA for lead/deal cards - "📅 Calendar" jump CTA for WO cards - calendarUrl() returns the canonical filament.tenant.pages.calendar-board route Empty column state now reads "Gol — trage un card aici" instead of just "Gol" so the drop affordance is explicit. Stat strip + filter bar sticky at top; board fills the remaining viewport. Tests: +1 (quickSchedule creates Appointment + moves deal). Suite 181/181. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3603c0e43b |
feat: rich Pipeline board — unified Lead/Deal/WO Kanban with SLA + drag-drop transitions
Replaces the bare 6-status WO Kanban with the unified Pipeline view from
/tmp/service/todo/psauto-pipeline-redesign.html. Six columns now span the
entire customer journey end-to-end:
Cerere nouă → Calculație → Programat → În lucru → Gata → Achitat azi
└─ Lead/Deal └─ Deal └─ Deal └─ WO └─ WO └─ WO+Payment
Cross-model drag-drop transitions:
- Lead → Calculație: Lead::convert() creates Deal at stage=contact, marks
quote_sent_at = now, quote_status = sent
- Deal (any earlier stage) → În lucru: spawns a WorkOrder from the deal
(client, vehicle, master, total, complaint), sets deal.stage=in_work,
links wo.deal_id
- WO → Gata: status=ready + fires NotificationDispatcher::workOrderReady
so client gets Telegram/email automatically
- WO → Achitat: creates Payment for remaining balance + status=done,
closed_at=today (pay_status syncs to paid via Payment booted hook)
Rich card content per the mockup:
- Red urgent stripe (left border) for Deal.urgent or WO.urgency!=normal
- Source tag (Instagram/Site/Apel/etc.) on lead/deal cards
- Quote status badge ("Trimis · fără răspuns" amber / "Văzut ✓" blue /
"A răspuns" green) based on deal.quote_status
- Scheduled time + bay tag ("05.06 · 09:00" + "Post 2")
- Fișă FL-NNN purple tag on WO cards
- "Necesită aprobare" amber tag when wo.status=agreement
- Progress bar (purple, 0-100%) on in-work cards: works_done + parts_installed
over total lines
- SLA time line per card with overdue red color:
* Lead 60+ min not contacted = overdue
* Quote 2h+ no response = overdue
* Ready 30+ min not paid = overdue (with phone icon)
* WO past ETA = overdue
- Assignee avatar (deterministic CRC32 color: blue/green/purple/amber)
- Amount in MDL, formatted
Stat strip (6 metrics computed live):
- Total deals active (sum of cols 1-5)
- MDL pipeline total
- MDL closed today (Payment sum where paid_at=today)
- Necesită acțiune (overdue + urgent + pending approval)
- Rata conversie 30d (won / (won+lost) %)
- Depășit termen (count WO past eta_at)
Filter chips wire-driven: Toate / Ale mele (assigned_to=me) /
Urgente (urgent=true OR wo.urgency!=normal) / Azi.
View toggle: Kanban ↔ Listă (table with all cards flat, sortable by stage).
Slide-in detail panel:
- 6-step stage stepper highlighting current
- Client / Telefon (blue clickable) / Auto / Sursă / Responsabil / Sumă /
De achitat (live computed balanceDue for WOs)
- Note / Reclamație
- Linked Fișă card with status badge, progress, ETA, "necesită aprobare"
alert + tracking link
- Activity timeline from Spatie activity-log
- Quick actions: WhatsApp (wa.me/<phone>), Sună (tel:), SMS (sms:),
Deschide (jumps to Filament resource edit)
DealResource hidden from nav (shouldRegisterNavigation=false) since
PipelineBoard is the canonical entry, but its edit/create routes stay
intact — the panel deep-links to them.
Auto-refresh: wire:poll.10s keeps the board live without WebSocket
dependency. Drag-drop is HTML5 native + Livewire wire:click for ops.
Dark mode supported via CSS variables overridden in .dark scope.
Migration: extend deals table with urgent, quote_sent_at, quote_status,
quote_seen_at, scheduled_at, bay, confirmed_at, confirmed_via,
last_action_at. Idempotent (hasColumn guards). Deal model auto-updates
last_action_at on saving.
Tests: 7 new + full suite 180/180 green (was 173).
- partition leads/deals/wos by column
- stats computation: active, pipeline_mdl, closed_today_mdl
- lead→quote transition converts lead into deal
- deal→in_work creates WorkOrder linked back to deal
- wo→paid creates payment for balance + marks done
- filter "mine" narrows to assigned user
- openCard loads panel detail with correct stepper position
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0620635abb |
test: full E2E audit + fix CsvImportExport vehicle.brand → make
Audit pass (33 new tests in tests/Feature/Audit/): - CrmFunnelE2ETest: full Lead→Deal→Appointment→WO→Payment journey, covering 5 previously-untested models. Verifies WO.balanceDue updates correctly after payments, including refunds (negative amount → balance increases). - WorkOrderTotalsTest: works+parts+subcontract+discount sum correctly, cancelled subcontract excluded, deleting lines triggers recalc, status=done consumes part reservations into issues, cancelled releases reservations. - ShopJourneyE2ETest: register→cart→checkout→email confirmation→tracking page reachable→admin fulfills→stock drops→warehouse event recorded. Also guest checkout still works without account. - CsvImportExportTest: round-trip, dedup-by-phone, **caught real bug** — Vehicle export wrote $row->brand (no such property) and import set 'brand' => row['brand'] in Vehicle::create (column is `make`). Fix applied to both paths. - TenantBackupServiceTest: zip contains valid manifest with counts + data/*.json per model + works embedded with WorkOrder. - WorkOrderPdfServiceTest: generated PDF starts with %PDF, includes WO data, non-trivial size, handles empty WO. - PayrollCalculatorTest: base + works_pct + parts_pct + bonus - fine - advance, scoped to user + period. - NotificationFallbackTest: Telegram wins when chat_id present, falls back to email when not, returns false when neither, tenant disable flag stops both. - AiProvidersCrossCheckTest: OpenAI request shape, Gemini URL with model, no-key friendly message, tenant model override propagates into HTTP body. - SettingsPersistenceTest: 25-key settings JSON round-trips, partial update via array_replace_recursive preserves other keys. - CompanyProvisionerTest: suspend / reactivate / archive behavior. Bug fixed: CsvImportExport used `brand` on Vehicle which has column `make`. The export silently emitted empty values, the import silently dropped the brand. Now both paths use `make`. Full suite: 173 passed (468 assertions). 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
439ef605a1 |
feat: production email (Resend) + offsite backup (B2)
Resend mail transport:
- composer require resend/resend-laravel (v1.4)
- Laravel 11 ships the 'resend' mailer config in config/mail.php + services
- To switch to production email: set MAIL_MAILER=resend + RESEND_API_KEY,
register the domain at resend.com/domains, and add the TXT + DKIM CNAME
records in Cloudflare. .env.example documents the required steps.
Backblaze B2 offsite backup:
- New filesystems 'b2' disk (S3-compatible, env: B2_KEY/SECRET/BUCKET/REGION/ENDPOINT)
- BackupAllTenantsCommand: after writing each tenant's zip to local disk, it
uploads the same file to the b2 disk under {YYYY-MM-DD}/{slug}.zip — only
when both B2_KEY and B2_BUCKET are set, so unconfigured installs are no-op
- Without offsite, backups live on the same VPS as production: a single
hardware failure loses everything. B2 + Resend together make the install
genuinely production-ready (people get email + offsite backup).
Tests (2 new):
- backup uploads to b2 (fake disk) when configured
- backup skips offsite when env vars not present
Full suite: 140 passed. Force-rebuild deploy required so composer install
picks up resend/resend-php.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
51917bcbaf |
feat: rate limiting + internal health monitor + secure VAPID note
Rate limiting: - Shop POST endpoints get per-IP throttles with distinct prefixes so login, register, password-email, and password-reset have separate buckets: login/register/pw-reset = 5/min, pw-email = 3/min - OcrInvoiceService gates per-tenant via RateLimiter (30/hour) so a runaway uploader can't burn Claude Vision spend Health monitor (poor-man's monitoring): - HealthCheckCommand probes DB (SELECT 1), cache write/read, public storage write/read, and most-recent backup age. On any failure, pushes a Telegram alert via HEALTH_ALERT_BOT_TOKEN/HEALTH_ALERT_CHAT_ID. Dedups identical failures within a 30-min window via cache. - Scheduled every 10 min. Pair with external uptime monitoring (UptimeRobot, Better Stack hitting /up) for total-outage coverage. - .env.example documents the two new env vars. VAPID secret hygiene: - credentials.md no longer stores the VAPID_PRIVATE_KEY; the source of truth is the Coolify env on the autocrm app. Doc points to where to read it (UI or API). Mitigates accidental git leak. Tests (4 new): - shop login throttles after 5 attempts (6th = 429); register throttle is independent of login (separate prefix); health command runs clean; dedup cache path exercised Full suite: 138 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0e3f9e8bca |
feat: AI model selector + i18n nav labels (RU/EN) on new modules
AI model selector:
- AiAssistantService::MODEL_DEFAULTS and MODEL_OPTIONS const tables (3 picks per
provider: Claude Opus 4.7 / Sonnet 4.6 / Haiku 4.5, OpenAI 4o / 4o-mini,
Gemini 1.5 Pro / Flash). Default upgraded from Sonnet 4.5 → Sonnet 4.6.
- modelFor(provider, company?) resolves tenant override > global default.
- All 8 hardcoded model strings replaced with modelFor() across callClaude
(chat with tool-use), callOpenAI, callGemini (chat), postClaude/postOpenAI/
postGemini (single-shot), and OcrInvoiceService.
- Settings page adds 3 model selectors per provider with persistence at
settings.ai.models.{claude,gpt,gemini}.
i18n nav labels:
- TireSet / Bodyshop / Subcontractor / SubcontractJob / PricingCoefficient /
ShopCustomer resources: getNavigationLabel / getNavigationGroup /
getModelLabel / getPluralModelLabel return __()-wrapped strings.
- 20 keys added to lang/ru.json and lang/en.json.
Tests (4 new): default model, tenant override wins, unknown provider falls
back to claude default, options dictionary contains each default key.
Full suite: 134 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3da1f5412a |
feat: shop UX polish — password reset / order email / multi-image / customer admin
Shop password reset:
- Configured 'shop_customers' password broker on the existing
password_reset_tokens table
- ShopCustomer::sendPasswordResetNotification overrides Laravel default to
send a ShopPasswordResetMail with a tenant-subdomain reset URL
- Routes /shop/password/forgot, /shop/password/email, /shop/password/reset/{token}
+ ShopAuthController showForgotPassword/sendResetLink/showResetPassword/
resetPassword. Forgot view stays generic ("if it exists, we sent…") to avoid
email enumeration. Login view links to "Am uitat parola".
Order confirmation email:
- ShopOrderConfirmationMail + nicely formatted HTML email template
- ShopOrderNotifier::placed now also emails customer_email (best-effort,
warning-only logged on failure) alongside existing Telegram + staff push
Multiple images per Part:
- Part media collection switched from singleFile to multiple (max 8 in form)
- imageUrls() helper for galleries; imageUrl() still returns first for cards
- PartResource form: reorderable multi-upload
- Shop part detail: vertical thumbnails switch the main image via vanilla JS
ShopCustomerResource (tenant Filament, "Magazin" nav group):
- List with name/phone/email/client_id/orders_count/last_login_at
- Edit (no password field exposed)
- "Trimite reset parolă" action uses the new broker
- OrdersRelationManager shows the customer's orders read-only
Tests (7 new):
- forgot sends mail; forgot doesn't disclose unknown email; reset with valid
token changes password; bad token rejected; order email when customer_email
set; email skipped without it; Part has imageUrls() collection
Full suite: 130 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
fca4f75e9c |
feat: OCR invoice import via Claude Vision
Upload an invoice photo → Claude extracts {supplier_name, date, currency,
items, total} as JSON → auto-create a draft Purchase + PurchaseItems →
redirect to edit so the user reviews before confirming/receiving.
OcrInvoiceService:
- Validates supported MIME (jpg/png/webp/gif)
- Reads tenant Claude key (settings.ai.claude_key) — friendly error if missing
- Calls /v1/messages with image content block + structured-output system prompt
- Tolerant parser: strips ```json fences, falls back to first {…} block
- normalize(): computes per-item total when absent, fills overall total
- All return shapes: {ok:bool, data?, error?, raw?, tokens?}
Filament:
- "Import factură (OCR)" header action on Purchases list
- Image file upload → service → matches Supplier by case-insensitive name
(notes the unmapped name if no match) → creates draft Purchase + items →
redirects to the Edit page
Tests (6 new):
- clean JSON parses; markdown fences stripped; malformed → graceful error;
missing key → friendly message + no HTTP; unsupported MIME rejected;
item total computed when missing
Full suite: 123 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
75386c354a |
feat: shop customer accounts (register/login + order history)
Schema: - shop_customers (company_id, name, phone unique-per-tenant, email, password, client_id auto-linked, last_login_at) - online_orders.shop_customer_id nullable FK Auth: - New 'shop' guard (session driver, shop_customers provider) in config/auth.php - ShopCustomer Authenticatable with hashed password cast and BelongsToTenant global scope — login attempts naturally scoped to current tenant subdomain Flow: - ShopAuthController: register / login / logout / account - Register auto-links to existing Client by phone match - /shop/account: order history (only the logged customer's orders) + profile - Checkout prefills name/phone/email from logged customer + sets shop_customer_id (and client_id from auto-link) on the placed order - Layout nav switches between Login/Register and "👤 Name + Ieșire" Tests (8 new): - register creates customer + auto-login - register auto-links existing Client by phone - duplicate phone rejected - login validates credentials - /account requires auth (redirects to /shop/login) - /account lists only the logged customer's orders - checkout attaches shop_customer_id - customers tenant-isolated Full suite: 117 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
b9ff9c6583 |
fix: scanner wire:ignore + Company custom columns
- Scanner page: wrap the html5-qrcode camera container (#reader) in wire:ignore so a Livewire DOM morph can't tear down the live camera stream (same class of bug as the calendar). - Company::getCustomColumns(): add `is_demo` and `default_warehouse_id`. Stancl Tenant treats columns absent from this list as virtual `data` JSON attributes, so editing a company could move default_warehouse_id into data and null the real column — breaking WarehouseService::defaultWarehouse. Full suite: 100 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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 |
||
|
|
d5680f78dc | fix: column names in globalSearch (leads.source, vehicles.make, parts.article, WO.complaint) | ||
|
|
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 |
||
|
|
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)
|
||
|
|
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) |
||
|
|
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) |
||
|
|
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 |
||
|
|
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
|
||
|
|
0ac42dde3d | fix: Forms\Get/Set->Schemas Get/Set on Central panel + rename Super-admini to Utilizatori | ||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
ce4e21220f | fix: SetLocale tolerant to early-pipeline missing session | ||
|
|
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)
|
||
|
|
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). |