7ce78c350c
- 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.
138 lines
4.0 KiB
PHP
138 lines
4.0 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 + broadcast WS event on status change. */
|
|
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);
|
|
}
|
|
|
|
// Broadcast real-time update on any field change (skip if broadcasting=log).
|
|
if (config('broadcasting.default') !== 'log') {
|
|
try {
|
|
$company = \App\Models\Central\Company::withoutGlobalScopes()->find($wo->company_id);
|
|
if ($company) {
|
|
\App\Events\WorkOrderUpdated::dispatch($wo, $company->slug);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
\Illuminate\Support\Facades\Log::debug('WO broadcast skipped: ' . $e->getMessage());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|