Files
autocrm/app/Models/Tenant/WorkOrder.php
T
Vasyka 09fd0bada2 Faza 2 (din continuare): Email notifications
4 Mailables auto-trigger pe model events:
- WorkOrderReadyMail: la WO.status → 'ready', către client.email
  • Atașat PDF fișa lucru (via WorkOrderPdfService)
  • Total/achitat/rest, recomandări (warning box)
- PaymentReceivedMail: la Payment::created, confirmare cu sumă/metodă/ref
- AppointmentConfirmedMail: la Appointment::created status='scheduled'
- ServiceReminderMail: dispatch manual (vehicle, type=itp/oil/general, note)

Layout email branded (resources/views/emails/layout.blade.php):
- Header cu logo tenant + theme_color border-bottom
- Footer cu telefon/email/disclaimer
- Stiluri inline (compatibil tot mail client)

Settings page extins cu 4 toggle:
- 'Mașina e gata de ridicat'
- 'Confirmare plată primită'
- 'Programare confirmată'
- 'Reminder ITP / revizie'
Salvate în companies.settings.notify (JSON), default true.

NotificationDispatcher service centralizat:
- Verifică isEnabled() pe settings.notify[$key]
- Skip dacă client n-are email
- Try/catch + Log::warning pe eșec (nu crapă request-ul)

Mailables folosesc UsesTenantBranding trait pentru context unitar.
Test prin Mailpit: https://mailpit.service.mir.md (capturează toate).
2026-05-07 13:20:19 +00:00

126 lines
3.4 KiB
PHP

<?php
namespace App\Models\Tenant;
use App\Models\Concerns\BelongsToTenant;
use App\Models\Concerns\Auditable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class WorkOrder extends Model
{
use Auditable, BelongsToTenant, SoftDeletes;
public const STATUSES = [
'new' => 'Nou',
'diagnosis' => 'Diagnosticare',
'agreement' => 'Aprobare client',
'approved' => 'Aprobat',
'in_work' => 'În lucru',
'awaiting_parts' => 'Așteaptă piese',
'ready' => 'Gata de ridicare',
'done' => 'Predat',
'cancelled' => 'Anulat',
];
public const PAY_STATUSES = [
'unpaid' => 'Neplătit',
'partial' => 'Parțial',
'paid' => 'Plătit',
];
protected $fillable = [
'company_id', 'number',
'client_id', 'vehicle_id', 'master_id', 'deal_id', 'appointment_id',
'opened_at', 'closed_at', 'mileage_in', 'mileage_out',
'complaint', 'diagnosis', 'recommendations',
'status', 'pay_status', 'approved', 'approved_at',
'discount_pct', 'total',
];
protected $casts = [
'opened_at' => 'date',
'closed_at' => 'date',
'approved_at' => 'datetime',
'approved' => 'boolean',
'discount_pct' => 'decimal:2',
'total' => 'decimal:2',
];
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
public function vehicle(): BelongsTo
{
return $this->belongsTo(Vehicle::class);
}
public function master(): BelongsTo
{
return $this->belongsTo(User::class, 'master_id');
}
public function works(): HasMany
{
return $this->hasMany(WorkOrderWork::class);
}
public function parts(): HasMany
{
return $this->hasMany(WorkOrderPart::class);
}
public function payments(): HasMany
{
return $this->hasMany(Payment::class);
}
public function paidAmount(): float
{
return (float) $this->payments()->sum('amount');
}
public function balanceDue(): float
{
return max(0.0, (float) $this->total - $this->paidAmount());
}
public function recalcTotal(): void
{
$worksTotal = $this->works()->sum('total');
$partsTotal = $this->parts()->sum('total');
$sub = (float) $worksTotal + (float) $partsTotal;
$disc = (float) $this->discount_pct;
$this->total = round($sub * (1 - $disc / 100), 2);
$this->save();
}
public static function generateNumber(int $companyId): string
{
$year = date('y');
$count = static::withoutGlobalScopes()
->where('company_id', $companyId)
->whereYear('created_at', date('Y'))
->count();
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);
}
});
}
}