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:
@@ -34,6 +34,7 @@ class Settings extends Page
|
|||||||
$settings = (array) ($company->settings ?? []);
|
$settings = (array) ($company->settings ?? []);
|
||||||
|
|
||||||
// Filament v5: fill via $this->form->fill() (initializes the schema state).
|
// Filament v5: fill via $this->form->fill() (initializes the schema state).
|
||||||
|
$notify = (array) ($settings['notify'] ?? []);
|
||||||
$this->form->fill([
|
$this->form->fill([
|
||||||
'display_name' => $company->display_name ?? $company->name,
|
'display_name' => $company->display_name ?? $company->name,
|
||||||
'city' => $company->city,
|
'city' => $company->city,
|
||||||
@@ -45,6 +46,10 @@ class Settings extends Page
|
|||||||
'labor_rate' => $settings['labor_rate'] ?? 400,
|
'labor_rate' => $settings['labor_rate'] ?? 400,
|
||||||
'services' => isset($settings['services']) ? implode(', ', (array) $settings['services']) : '',
|
'services' => isset($settings['services']) ? implode(', ', (array) $settings['services']) : '',
|
||||||
'cars' => isset($settings['cars']) ? implode(', ', (array) $settings['cars']) : '',
|
'cars' => isset($settings['cars']) ? implode(', ', (array) $settings['cars']) : '',
|
||||||
|
'notify_wo_ready' => $notify['wo_ready'] ?? true,
|
||||||
|
'notify_payment' => $notify['payment'] ?? true,
|
||||||
|
'notify_appointment' => $notify['appointment'] ?? true,
|
||||||
|
'notify_reminder' => $notify['reminder'] ?? true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +111,15 @@ class Settings extends Page
|
|||||||
->maxSize(512)
|
->maxSize(512)
|
||||||
->helperText('PNG/ICO, max 512 KB.'),
|
->helperText('PNG/ICO, max 512 KB.'),
|
||||||
]),
|
]),
|
||||||
|
Schemas\Components\Section::make('Notificări email')
|
||||||
|
->description('Activează / dezactivează emailurile auto către clienți.')
|
||||||
|
->columns(2)
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Toggle::make('notify_wo_ready')->label('Mașina e gata de ridicat')->default(true),
|
||||||
|
Forms\Components\Toggle::make('notify_payment')->label('Confirmare plată primită')->default(true),
|
||||||
|
Forms\Components\Toggle::make('notify_appointment')->label('Programare confirmată')->default(true),
|
||||||
|
Forms\Components\Toggle::make('notify_reminder')->label('Reminder ITP / revizie')->default(true),
|
||||||
|
]),
|
||||||
])
|
])
|
||||||
->statePath('data');
|
->statePath('data');
|
||||||
}
|
}
|
||||||
@@ -131,6 +145,12 @@ class Settings extends Page
|
|||||||
'labor_rate' => (float) ($data['labor_rate'] ?? 400),
|
'labor_rate' => (float) ($data['labor_rate'] ?? 400),
|
||||||
'services' => array_values(array_filter(array_map('trim', explode(',', (string) ($data['services'] ?? ''))))),
|
'services' => array_values(array_filter(array_map('trim', explode(',', (string) ($data['services'] ?? ''))))),
|
||||||
'cars' => array_values(array_filter(array_map('trim', explode(',', (string) ($data['cars'] ?? ''))))),
|
'cars' => array_values(array_filter(array_map('trim', explode(',', (string) ($data['cars'] ?? ''))))),
|
||||||
|
'notify' => [
|
||||||
|
'wo_ready' => (bool) ($data['notify_wo_ready'] ?? true),
|
||||||
|
'payment' => (bool) ($data['notify_payment'] ?? true),
|
||||||
|
'appointment' => (bool) ($data['notify_appointment'] ?? true),
|
||||||
|
'reminder' => (bool) ($data['notify_reminder'] ?? true),
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,4 +52,14 @@ class Appointment extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Deal::class);
|
return $this->belongsTo(Deal::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Auto-send confirmation email when a 'scheduled' appointment is created. */
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
static::created(function (self $a) {
|
||||||
|
if ($a->status === 'scheduled') {
|
||||||
|
app(\App\Services\NotificationDispatcher::class)->appointmentConfirmed($a);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,5 +75,10 @@ class Payment extends Model
|
|||||||
|
|
||||||
static::saved($sync);
|
static::saved($sync);
|
||||||
static::deleted($sync);
|
static::deleted($sync);
|
||||||
|
|
||||||
|
// Auto-send confirmation email on new payment.
|
||||||
|
static::created(function (self $payment) {
|
||||||
|
app(\App\Services\NotificationDispatcher::class)->paymentReceived($payment);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,4 +108,18 @@ class WorkOrder extends Model
|
|||||||
->count();
|
->count();
|
||||||
return sprintf('WO-%s-%04d', $year, $count + 1);
|
return sprintf('WO-%s-%04d', $year, $count + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Auto-send 'ready' email when status transitions to 'ready'. */
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
static::updated(function (self $wo) {
|
||||||
|
if (
|
||||||
|
$wo->wasChanged('status')
|
||||||
|
&& $wo->status === 'ready'
|
||||||
|
&& $wo->getOriginal('status') !== 'ready'
|
||||||
|
) {
|
||||||
|
app(\App\Services\NotificationDispatcher::class)->workOrderReady($wo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Mail\AppointmentConfirmedMail;
|
||||||
|
use App\Mail\PaymentReceivedMail;
|
||||||
|
use App\Mail\ServiceReminderMail;
|
||||||
|
use App\Mail\WorkOrderReadyMail;
|
||||||
|
use App\Models\Central\Company;
|
||||||
|
use App\Models\Tenant\Appointment;
|
||||||
|
use App\Models\Tenant\Payment;
|
||||||
|
use App\Models\Tenant\Vehicle;
|
||||||
|
use App\Models\Tenant\WorkOrder;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralizes outbound email notifications, with per-tenant feature toggles
|
||||||
|
* stored in companies.settings.notify (e.g., 'wo_ready' => true).
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* app(NotificationDispatcher::class)->workOrderReady($workOrder);
|
||||||
|
*/
|
||||||
|
class NotificationDispatcher
|
||||||
|
{
|
||||||
|
public function workOrderReady(WorkOrder $wo): bool
|
||||||
|
{
|
||||||
|
$company = $this->companyFor($wo);
|
||||||
|
if (! $this->isEnabled($company, 'wo_ready')) return false;
|
||||||
|
|
||||||
|
$email = $wo->client?->email;
|
||||||
|
if (! $email) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Mail::to($email)->send(new WorkOrderReadyMail($wo, $company));
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('workOrderReady mail failed', ['wo' => $wo->id, 'err' => $e->getMessage()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paymentReceived(Payment $payment): bool
|
||||||
|
{
|
||||||
|
$company = $this->companyFor($payment);
|
||||||
|
if (! $this->isEnabled($company, 'payment')) return false;
|
||||||
|
|
||||||
|
$email = $payment->client?->email;
|
||||||
|
if (! $email) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Mail::to($email)->send(new PaymentReceivedMail($payment, $company));
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('paymentReceived mail failed', ['payment' => $payment->id, 'err' => $e->getMessage()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function appointmentConfirmed(Appointment $a): bool
|
||||||
|
{
|
||||||
|
$company = $this->companyFor($a);
|
||||||
|
if (! $this->isEnabled($company, 'appointment')) return false;
|
||||||
|
|
||||||
|
$email = $a->client?->email;
|
||||||
|
if (! $email) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Mail::to($email)->send(new AppointmentConfirmedMail($a, $company));
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('appointmentConfirmed mail failed', ['appt' => $a->id, 'err' => $e->getMessage()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function serviceReminder(Vehicle $v, string $type = 'general', ?string $note = null): bool
|
||||||
|
{
|
||||||
|
$company = $this->companyFor($v);
|
||||||
|
if (! $this->isEnabled($company, 'reminder')) return false;
|
||||||
|
|
||||||
|
$email = $v->client?->email;
|
||||||
|
if (! $email) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Mail::to($email)->send(new ServiceReminderMail($v, $type, $note, $company));
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('serviceReminder mail failed', ['vehicle' => $v->id, 'err' => $e->getMessage()]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function companyFor($model): Company
|
||||||
|
{
|
||||||
|
return Company::withoutGlobalScopes()->findOrFail($model->company_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isEnabled(Company $company, string $key): bool
|
||||||
|
{
|
||||||
|
$settings = (array) ($company->settings ?? []);
|
||||||
|
$notify = (array) ($settings['notify'] ?? []);
|
||||||
|
// default: enabled (toate notificările active by default)
|
||||||
|
return ($notify[$key] ?? true) === true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
@component('emails.layout', [
|
||||||
|
'companyName' => $companyName,
|
||||||
|
'themeColor' => $themeColor,
|
||||||
|
'phone' => $phone ?? null,
|
||||||
|
'email' => $email ?? null,
|
||||||
|
'city' => $city ?? null,
|
||||||
|
'logoUrl' => $logoUrl ?? null,
|
||||||
|
'title' => 'Programare confirmată',
|
||||||
|
])
|
||||||
|
<h2 style="margin:0 0 12px;font-size:20px;color:{{ $themeColor }};">📅 Programare confirmată</h2>
|
||||||
|
<p>Bună ziua{{ $client?->name ? ', ' . $client->name : '' }},</p>
|
||||||
|
<p>Programarea dvs. este confirmată:</p>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:16px 0;background:#f9fafb;border-radius:8px;">
|
||||||
|
<tr><td style="padding:14px 18px;font-size:13px;color:#374151;">Data</td>
|
||||||
|
<td style="padding:14px 18px;font-size:18px;text-align:right;font-weight:700;color:{{ $themeColor }};">
|
||||||
|
{{ $appointment->date?->format('d.m.Y') }}
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:8px 18px;font-size:13px;color:#374151;">Ora</td>
|
||||||
|
<td style="padding:8px 18px;font-size:14px;text-align:right;font-weight:600;">
|
||||||
|
{{ \Illuminate\Support\Str::substr($appointment->time_start, 0, 5) }} – {{ \Illuminate\Support\Str::substr($appointment->time_end, 0, 5) }}
|
||||||
|
</td></tr>
|
||||||
|
@if ($appointment->title)
|
||||||
|
<tr><td style="padding:8px 18px;font-size:12px;color:#6b7280;">Subiect</td>
|
||||||
|
<td style="padding:8px 18px;font-size:12px;text-align:right;">{{ $appointment->title }}</td></tr>
|
||||||
|
@endif
|
||||||
|
@if ($vehicle)
|
||||||
|
<tr><td style="padding:8px 18px;font-size:12px;color:#6b7280;">Auto</td>
|
||||||
|
<td style="padding:8px 18px;font-size:12px;text-align:right;">
|
||||||
|
{{ $vehicle->make }} {{ $vehicle->model }}
|
||||||
|
@if ($vehicle->plate) <b>[{{ $vehicle->plate }}]</b> @endif
|
||||||
|
</td></tr>
|
||||||
|
@endif
|
||||||
|
@if ($master)
|
||||||
|
<tr><td style="padding:8px 18px;font-size:12px;color:#6b7280;">Maistru</td>
|
||||||
|
<td style="padding:8px 18px;font-size:12px;text-align:right;">{{ $master->name }}</td></tr>
|
||||||
|
@endif
|
||||||
|
@if ($post)
|
||||||
|
<tr><td style="padding:8px 18px;font-size:12px;color:#6b7280;">Pod</td>
|
||||||
|
<td style="padding:8px 18px;font-size:12px;text-align:right;">{{ $post->name }}</td></tr>
|
||||||
|
@endif
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>Vă așteptăm! 🚗</p>
|
||||||
|
<p style="font-size:12px;color:#6b7280;">Pentru reprogramare sau anulare, sunați-ne la {{ $phone ?? '—' }}.</p>
|
||||||
|
@endcomponent
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ro">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>{{ $title ?? 'AutoCRM' }}</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;background:#f3f4f6;font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;color:#1f2937;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f3f4f6;padding:24px 0;">
|
||||||
|
<tr><td align="center">
|
||||||
|
<table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0" style="background:#fff;border-radius:10px;box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden;max-width:600px;width:100%;">
|
||||||
|
<tr><td style="padding:24px 28px;border-bottom:3px solid {{ $themeColor ?? '#3b82f6' }};">
|
||||||
|
@if (!empty($logoUrl))
|
||||||
|
<img src="{{ $logoUrl }}" alt="logo" style="max-height:42px;display:block;">
|
||||||
|
@else
|
||||||
|
<div style="font-size:20px;font-weight:700;color:{{ $themeColor ?? '#3b82f6' }};">{{ $companyName ?? 'AutoCRM' }}</div>
|
||||||
|
@endif
|
||||||
|
@if (!empty($city))
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-top:4px;">{{ $city }}</div>
|
||||||
|
@endif
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<tr><td style="padding:24px 28px;font-size:14px;line-height:1.6;">
|
||||||
|
{{ $slot }}
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<tr><td style="padding:16px 28px;border-top:1px solid #e5e7eb;background:#f9fafb;font-size:11px;color:#6b7280;">
|
||||||
|
<div>{{ $companyName ?? 'AutoCRM' }}</div>
|
||||||
|
@if (!empty($phone)) <div>📞 {{ $phone }}</div> @endif
|
||||||
|
@if (!empty($email)) <div>✉ {{ $email }}</div> @endif
|
||||||
|
<div style="margin-top:8px;color:#9ca3af;">Acest email a fost generat automat. Vă rugăm nu răspundeți.</div>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@component('emails.layout', [
|
||||||
|
'companyName' => $companyName,
|
||||||
|
'themeColor' => $themeColor,
|
||||||
|
'phone' => $phone ?? null,
|
||||||
|
'email' => $email ?? null,
|
||||||
|
'city' => $city ?? null,
|
||||||
|
'logoUrl' => $logoUrl ?? null,
|
||||||
|
'title' => 'Plată confirmată',
|
||||||
|
])
|
||||||
|
<h2 style="margin:0 0 12px;font-size:20px;color:{{ $themeColor }};">💳 Plată confirmată</h2>
|
||||||
|
<p>Bună ziua{{ $client?->name ? ', ' . $client->name : '' }},</p>
|
||||||
|
<p>Confirmăm primirea plății dvs.:</p>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:16px 0;background:#f9fafb;border-radius:8px;">
|
||||||
|
<tr><td style="padding:14px 18px;font-size:13px;color:#374151;">Sumă achitată</td>
|
||||||
|
<td style="padding:14px 18px;font-size:18px;text-align:right;font-weight:700;color:#059669;">
|
||||||
|
{{ number_format((float) $payment->amount, 2, '.', ' ') }} {{ $currency }}
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:8px 18px;font-size:12px;color:#6b7280;">Metoda</td>
|
||||||
|
<td style="padding:8px 18px;font-size:12px;text-align:right;">
|
||||||
|
{{ \App\Models\Tenant\Payment::METHODS[$payment->method] ?? $payment->method }}
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:8px 18px;font-size:12px;color:#6b7280;">Data</td>
|
||||||
|
<td style="padding:8px 18px;font-size:12px;text-align:right;">{{ $payment->paid_at?->format('d.m.Y') }}</td></tr>
|
||||||
|
@if ($payment->reference)
|
||||||
|
<tr><td style="padding:8px 18px;font-size:12px;color:#6b7280;">Referință</td>
|
||||||
|
<td style="padding:8px 18px;font-size:12px;text-align:right;font-family:monospace;">{{ $payment->reference }}</td></tr>
|
||||||
|
@endif
|
||||||
|
@if ($workOrder)
|
||||||
|
<tr><td style="padding:8px 18px;font-size:12px;color:#6b7280;">Fișă lucru</td>
|
||||||
|
<td style="padding:8px 18px;font-size:12px;text-align:right;font-weight:600;">{{ $workOrder->number }}</td></tr>
|
||||||
|
@endif
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>Vă mulțumim!</p>
|
||||||
|
@endcomponent
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
@php
|
||||||
|
$titles = [
|
||||||
|
'itp' => '🛡️ Reminder ITP',
|
||||||
|
'oil' => '🛢️ Reminder schimb ulei',
|
||||||
|
'general' => '🔧 Reminder revizie',
|
||||||
|
];
|
||||||
|
$body = [
|
||||||
|
'itp' => 'ITP-ul autoturismului dvs. expiră în curând. Programați-vă din timp pentru a evita amenzile.',
|
||||||
|
'oil' => 'A trecut o perioadă de la ultimul schimb de ulei. Recomandăm o nouă verificare.',
|
||||||
|
'general' => 'Vă reamintim că autoturismul dvs. necesită o verificare programată.',
|
||||||
|
];
|
||||||
|
@endphp
|
||||||
|
@component('emails.layout', [
|
||||||
|
'companyName' => $companyName,
|
||||||
|
'themeColor' => $themeColor,
|
||||||
|
'phone' => $phone ?? null,
|
||||||
|
'email' => $email ?? null,
|
||||||
|
'city' => $city ?? null,
|
||||||
|
'logoUrl' => $logoUrl ?? null,
|
||||||
|
'title' => $titles[$reminderType] ?? 'Reminder',
|
||||||
|
])
|
||||||
|
<h2 style="margin:0 0 12px;font-size:20px;color:{{ $themeColor }};">{{ $titles[$reminderType] ?? 'Reminder' }}</h2>
|
||||||
|
<p>Bună ziua{{ $client?->name ? ', ' . $client->name : '' }},</p>
|
||||||
|
<p>{{ $body[$reminderType] ?? 'Vă reamintim despre vehiculul dvs.' }}</p>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:16px 0;border:1px solid #e5e7eb;border-radius:8px;">
|
||||||
|
<tr><td style="padding:14px;">
|
||||||
|
<div style="font-size:11px;text-transform:uppercase;color:#6b7280;letter-spacing:.5px;">Vehicul</div>
|
||||||
|
<div style="font-size:16px;font-weight:700;margin-top:4px;">
|
||||||
|
{{ $vehicle->make }} {{ $vehicle->model }} {{ $vehicle->year ?? '' }}
|
||||||
|
@if ($vehicle->plate) <span style="color:{{ $themeColor }};">[{{ $vehicle->plate }}]</span> @endif
|
||||||
|
</div>
|
||||||
|
@if ($vehicle->mileage)
|
||||||
|
<div style="font-size:12px;color:#6b7280;margin-top:4px;">Kilometraj: {{ number_format($vehicle->mileage, 0, '.', ' ') }} km</div>
|
||||||
|
@endif
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
@if ($note)
|
||||||
|
<div style="background:#fef3c7;border-left:4px solid #f59e0b;padding:12px;margin:16px 0;border-radius:6px;font-size:13px;">
|
||||||
|
{{ $note }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<p>Programați-vă online sau sunați la <b>{{ $phone ?? '—' }}</b>.</p>
|
||||||
|
@endcomponent
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@php $balance = max(0, $total - $paid); @endphp
|
||||||
|
@component('emails.layout', [
|
||||||
|
'companyName' => $companyName,
|
||||||
|
'themeColor' => $themeColor,
|
||||||
|
'phone' => $phone ?? null,
|
||||||
|
'email' => $email ?? null,
|
||||||
|
'city' => $city ?? null,
|
||||||
|
'logoUrl' => $logoUrl ?? null,
|
||||||
|
'title' => 'Mașina e gata',
|
||||||
|
])
|
||||||
|
<h2 style="margin:0 0 12px;font-size:20px;color:{{ $themeColor }};">✅ Mașina dvs. este gata!</h2>
|
||||||
|
<p>Bună ziua{{ $client?->name ? ', ' . $client->name : '' }},</p>
|
||||||
|
<p>Vă informăm că lucrările pentru autoturismul dvs. au fost finalizate. Puteți veni să-l ridicați.</p>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:16px 0;border:1px solid #e5e7eb;border-radius:8px;">
|
||||||
|
<tr><td style="padding:14px;">
|
||||||
|
<div style="font-size:11px;text-transform:uppercase;color:#6b7280;letter-spacing:.5px;">Fișă lucru</div>
|
||||||
|
<div style="font-size:16px;font-weight:700;margin-top:4px;">{{ $workOrder->number }}</div>
|
||||||
|
@if ($vehicle)
|
||||||
|
<div style="margin-top:10px;font-size:13px;">
|
||||||
|
{{ $vehicle->make }} {{ $vehicle->model }} {{ $vehicle->year ?? '' }}
|
||||||
|
@if ($vehicle->plate) <b style="color:{{ $themeColor }};">[{{ $vehicle->plate }}]</b> @endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:16px 0;background:#f9fafb;border-radius:8px;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding:12px 16px;font-size:13px;color:#374151;">Total</td>
|
||||||
|
<td style="padding:12px 16px;font-size:13px;text-align:right;font-weight:600;">{{ number_format($total, 2, '.', ' ') }} {{ $currency }}</td>
|
||||||
|
</tr>
|
||||||
|
@if ($paid > 0)
|
||||||
|
<tr>
|
||||||
|
<td style="padding:6px 16px;font-size:12px;color:#6b7280;">Achitat</td>
|
||||||
|
<td style="padding:6px 16px;font-size:12px;text-align:right;color:#059669;">{{ number_format($paid, 2, '.', ' ') }} {{ $currency }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
@if ($balance > 0)
|
||||||
|
<tr>
|
||||||
|
<td style="padding:12px 16px;font-size:14px;font-weight:700;border-top:1px solid #e5e7eb;color:{{ $themeColor }};">Rest de plată</td>
|
||||||
|
<td style="padding:12px 16px;font-size:16px;text-align:right;font-weight:700;border-top:1px solid #e5e7eb;color:{{ $themeColor }};">{{ number_format($balance, 2, '.', ' ') }} {{ $currency }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</table>
|
||||||
|
|
||||||
|
@if ($workOrder->recommendations)
|
||||||
|
<div style="background:#fef3c7;border-left:4px solid #f59e0b;padding:12px;margin:16px 0;border-radius:6px;font-size:13px;">
|
||||||
|
<b style="color:#92400e;">⚠ Recomandări:</b><br>
|
||||||
|
{{ $workOrder->recommendations }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<p style="margin-top:20px;">Atașat veți găsi <b>fișa de lucru completă</b> (PDF).</p>
|
||||||
|
<p>Vă mulțumim pentru încredere!</p>
|
||||||
|
@endcomponent
|
||||||
Reference in New Issue
Block a user