11 Commits

Author SHA1 Message Date
Vasyka 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>
2026-06-03 06:43:39 +00:00
Vasyka 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>
2026-06-03 06:14:45 +00:00
Vasyka 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>
2026-06-02 19:43:39 +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 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 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 06696727dd Faza 6: Activity log + Kanban + Payroll + cleanup
══════ Activity log (Spatie) ══════
- spatie/laravel-activitylog v5 instalat
- Migration cu company_id pentru tenant scoping
- Trait Auditable (App\Models\Concerns\Auditable):
  - LogOptions cu logFillable + logOnlyDirty + dontSubmitEmptyLogs
  - tapActivity auto-fill company_id + causer
  - Descrieri RO (creat/modificat/șters/restaurat)
- Aplicat pe: Client, Vehicle, Lead, Deal, WorkOrder, Payment, Expense
- ActivityResource (group Admin → Jurnal activitate)
  - Listă read-only, scope pe tenant, filtre by description/today

══════ Kanban Work Orders ══════
- Custom Filament page la /app/kanban (group Service)
- 6 coloane (new → diagnosis → agreement → in_work → awaiting_parts → ready)
- Drag-drop nativ HTML5 cu wire:click moveCard()
- Cards arată: număr fișă, client, auto, plate, master, total
- Link 'Deschide' direct la editare WO

══════ Payroll (Salarii) ══════
Schema:
- employee_profiles: user_id, position, base_salary, works_pct, parts_pct
- payroll_runs: period (YYYY-MM), base, works_revenue/pct, parts_margin/pct,
  bonus, fines, advance, total auto-calculat
- payroll_adjustments: bonus/fine/advance per period

PayrollCalculator service:
- compute($userId, $period) — calculează auto:
  - Manopere finalizate de mecanic în luna respectivă (sum total)
  - Marja pieselor montate de el (sell-buy * qty)
  - Bonus + fines + advance from adjustments
  - Total = base + works% + parts% + bonus - fines - advance

Resources Filament (group Finanțe):
- EmployeeProfileResource: profil cu % comisioane
- PayrollRunResource: salarii cu action 'Calculează luna curentă' (toți userii)
  + per-row 'Recalculează'; Sum summary pe total
- PayrollAdjustmentResource: gestionare bonus/penalizări/avansuri

══════ Cleanup ══════
- Șterse toate /__debug, /__seed, /__try-login, /__force-login, /__whoami,
  /__coolify-check (security)
- Routes/web.php conține doar / redirect, /manifest.json, /sw.js

Total Filament tenant routes: 92.
2026-05-07 09:52:01 +00:00
Vasyka 0620e08351 fix: read Coolify config via config() (env() unreliable with config:cache + Octane) 2026-05-07 07:02:05 +00:00
Vasyka c9cb3560ef Faza 3.1: CRM core — Leads, Deals, Appointments, Settings, Widgets, Users
Spatie Permission cu teams (team_foreign_key=company_id, teams=true):
- migrations create_permission_tables (model_has_roles cu company_id scope)
- HasRoles trait pe User
- ResolveTenant middleware setează permissions team_id la tenant.id
- Seed: 7 roluri default per tenant (admin/manager/receptionist/mechanic/parts_manager/accountant/marketer)

Module noi:
- Leads (cereri): name, phone, car/model, source, UTM, status, budget, assigned_to,
  acțiune "Convertește" → creează automat Client + Deal
- Deals (pipeline): client/vehicle, stage (8 stage-uri), price, source, lost_reason
- Posts + Appointments: post_id (boxă), master_id, date+time_start+time_end, status, color
- UserResource (tenant): CRUD users cu role/status/locale; canViewAny doar pentru admin

Custom Filament page "Setări" (tenant):
- Brand & contact (display_name, city, phone, email)
- Localizare (limba RO/RU/EN, currency, theme color picker)
- Servicii & tarif (labor_rate)
- Liste configurabile (services, cars) — păstrate în companies.settings JSON

Widgets dashboard:
- Tenant: StatsOverview (Clienți, Mașini, Cereri noi, Deal-uri active, Programări azi)
- Central: PlatformStats (Companii total/active/trial, Expiră în 7 zile)

Seed extins demo PSauto:
- 3 posturi (Pod 1/2/3 cu culori)
- 2 lead-uri demo (Alex Grosu Telegram, Irina Cojocaru WhatsApp)
- 3 deal-uri demo (BMW done, Audi in_work, Porsche agree)
- 2 programări (azi + mâine)

Filament v5 fixes:
- $navigationGroup type → string|UnitEnum|null (parent stricter signature)
- Toate resources noi au tipurile corecte
2026-05-06 17:36:32 +00:00
Vasyka 4b1635d045 Faza 2: multi-tenancy + Filament dual panels + seed PSauto
Schema centrală:
- companies (slug unique, status, plan_id, settings JSON, trial/active dates)
- super_admins (operator platform)
- plans (free/basic/pro)

Schema tenant (toate cu company_id NOT NULL):
- users (UNIQUE company_id+email)
- clients
- vehicles

Tenancy core:
- App\Tenancy\TenantManager singleton
- App\Models\Concerns\BelongsToTenant trait + TenantScope
- ResolveTenant middleware (slug → Company, 404 pentru rezervate/missing)
- CheckTenantStatus middleware (suspended/expired/archived)
- Fail-safe: TenantScope returns 0 rows când tenant nu e rezolvat

Auth guards:
- 'central' guard cu super_admins provider (panou platform)
- 'web' guard cu users provider (per-tenant)

Filament panels:
- CentralPanelProvider la service.mir.md/admin
- TenantPanelProvider la <slug>.service.mir.md/app
- CompanyResource (central): CRUD companii cu status badge + filtre
- ClientResource (tenant): CRUD clienți cu status, sursă, sold
- VehicleResource (tenant): CRUD mașini cu marcă/model/VIN

Seed:
- 3 plans (free/basic/pro)
- super-admin: vasyka.moraru@gmail.com / admin123
- demo company 'psauto' cu admin user admin@psauto.md / admin123
- 3 clienți + 3 mașini preluate din AutoCRM.html

Bootstrap:
- TrustProxies (Cloudflare→Traefik HTTPS detection)
- forceScheme/forceRootUrl când APP_URL e HTTPS
- Helper global tenant() în app/helpers.php (autoload via composer)
- RUN_SEED env var în entrypoint pentru db:seed condiționat
2026-05-05 21:29:52 +00:00
Vasyka 5e32f82b3a Initial Laravel 12 + Filament 5 + Octane skeleton
- Laravel 12 base
- Filament 5 (default admin panel)
- Stancl/Tenancy v3 (config + migrations only)
- Spatie Permission
- Octane FrankenPHP runtime
- Sanctum
- Dockerfile multi-stage (composer + node + frankenphp:8.4)
- Entrypoint runs migrations + caches on boot
- .env.example pre-completat cu hosturi interne Coolify
- Health endpoint /up

Repo init pentru multi-tenant SaaS pe Coolify Hetzner.
2026-05-04 12:19:55 +00:00