Faza 2 (din continuare): Email notifications
4 Mailables auto-trigger pe model events: - WorkOrderReadyMail: la WO.status → 'ready', către client.email • Atașat PDF fișa lucru (via WorkOrderPdfService) • Total/achitat/rest, recomandări (warning box) - PaymentReceivedMail: la Payment::created, confirmare cu sumă/metodă/ref - AppointmentConfirmedMail: la Appointment::created status='scheduled' - ServiceReminderMail: dispatch manual (vehicle, type=itp/oil/general, note) Layout email branded (resources/views/emails/layout.blade.php): - Header cu logo tenant + theme_color border-bottom - Footer cu telefon/email/disclaimer - Stiluri inline (compatibil tot mail client) Settings page extins cu 4 toggle: - 'Mașina e gata de ridicat' - 'Confirmare plată primită' - 'Programare confirmată' - 'Reminder ITP / revizie' Salvate în companies.settings.notify (JSON), default true. NotificationDispatcher service centralizat: - Verifică isEnabled() pe settings.notify[$key] - Skip dacă client n-are email - Try/catch + Log::warning pe eșec (nu crapă request-ul) Mailables folosesc UsesTenantBranding trait pentru context unitar. Test prin Mailpit: https://mailpit.service.mir.md (capturează toate).
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Mail\Concerns\UsesTenantBranding;
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Tenant\Appointment;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class AppointmentConfirmedMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels, UsesTenantBranding;
|
||||
|
||||
public function __construct(public Appointment $appointment, Company $company)
|
||||
{
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: "[{$this->company->name}] Programare confirmată — " . $this->appointment->date?->format('d.m.Y'),
|
||||
from: new \Illuminate\Mail\Mailables\Address(
|
||||
config('mail.from.address'),
|
||||
$this->company->display_name ?? $this->company->name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.appointment-confirmed',
|
||||
with: array_merge($this->buildBrandingContext(), [
|
||||
'appointment' => $this->appointment,
|
||||
'client' => $this->appointment->client,
|
||||
'vehicle' => $this->appointment->vehicle,
|
||||
'master' => $this->appointment->master,
|
||||
'post' => $this->appointment->post,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail\Concerns;
|
||||
|
||||
use App\Models\Central\Company;
|
||||
|
||||
trait UsesTenantBranding
|
||||
{
|
||||
public Company $company;
|
||||
|
||||
public function buildBrandingContext(): array
|
||||
{
|
||||
return [
|
||||
'companyName' => $this->company->display_name ?? $this->company->name,
|
||||
'themeColor' => $this->company->settings['theme_color'] ?? '#3B82F6',
|
||||
'phone' => $this->company->phone,
|
||||
'email' => $this->company->email,
|
||||
'city' => $this->company->city,
|
||||
'logoUrl' => $this->company->getLogoUrl(),
|
||||
'tenantUrl' => $this->company->url('/app'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Mail\Concerns\UsesTenantBranding;
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Tenant\Payment;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PaymentReceivedMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels, UsesTenantBranding;
|
||||
|
||||
public function __construct(public Payment $payment, Company $company)
|
||||
{
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: "[{$this->company->name}] Confirmare plată — " . number_format((float) $this->payment->amount, 2, '.', ' ') . ' ' . ($this->company->settings['currency'] ?? 'MDL'),
|
||||
from: new \Illuminate\Mail\Mailables\Address(
|
||||
config('mail.from.address'),
|
||||
$this->company->display_name ?? $this->company->name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.payment-received',
|
||||
with: array_merge($this->buildBrandingContext(), [
|
||||
'payment' => $this->payment,
|
||||
'client' => $this->payment->client,
|
||||
'workOrder' => $this->payment->workOrder,
|
||||
'currency' => $this->company->settings['currency'] ?? 'MDL',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Mail\Concerns\UsesTenantBranding;
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Tenant\Vehicle;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ServiceReminderMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels, UsesTenantBranding;
|
||||
|
||||
public function __construct(
|
||||
public Vehicle $vehicle,
|
||||
public string $reminderType, // 'itp' / 'oil' / 'general'
|
||||
public ?string $note,
|
||||
Company $company,
|
||||
) {
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$titles = ['itp' => 'Reminder ITP', 'oil' => 'Reminder schimb ulei', 'general' => 'Reminder revizie'];
|
||||
$title = $titles[$this->reminderType] ?? 'Reminder';
|
||||
|
||||
return new Envelope(
|
||||
subject: "[{$this->company->name}] {$title} — " . trim(($this->vehicle->make ?? '') . ' ' . ($this->vehicle->model ?? '')),
|
||||
from: new \Illuminate\Mail\Mailables\Address(
|
||||
config('mail.from.address'),
|
||||
$this->company->display_name ?? $this->company->name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.service-reminder',
|
||||
with: array_merge($this->buildBrandingContext(), [
|
||||
'vehicle' => $this->vehicle,
|
||||
'client' => $this->vehicle->client,
|
||||
'reminderType' => $this->reminderType,
|
||||
'note' => $this->note,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Mail\Concerns\UsesTenantBranding;
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Tenant\WorkOrder;
|
||||
use App\Services\WorkOrderPdfService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Attachment;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class WorkOrderReadyMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels, UsesTenantBranding;
|
||||
|
||||
public function __construct(public WorkOrder $workOrder, Company $company)
|
||||
{
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: "[{$this->company->name}] Mașina dvs. este gata — fișa {$this->workOrder->number}",
|
||||
from: new \Illuminate\Mail\Mailables\Address(
|
||||
config('mail.from.address'),
|
||||
$this->company->display_name ?? $this->company->name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.work-order-ready',
|
||||
with: array_merge($this->buildBrandingContext(), [
|
||||
'workOrder' => $this->workOrder,
|
||||
'client' => $this->workOrder->client,
|
||||
'vehicle' => $this->workOrder->vehicle,
|
||||
'paid' => (float) $this->workOrder->payments->sum('amount'),
|
||||
'total' => (float) $this->workOrder->total,
|
||||
'currency' => $this->company->settings['currency'] ?? 'MDL',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
try {
|
||||
$pdf = app(WorkOrderPdfService::class)->generate($this->workOrder);
|
||||
return [
|
||||
Attachment::fromData(fn () => $pdf->output(), app(WorkOrderPdfService::class)->filename($this->workOrder))
|
||||
->withMime('application/pdf'),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user