153 lines
5.3 KiB
PHP
153 lines
5.3 KiB
PHP
<?php
|
||
|
||
namespace Modules\ShopFloor\Services;
|
||
|
||
use Carbon\Carbon;
|
||
use Modules\ShopFloor\Models\DowntimeEvent;
|
||
use Modules\ShopFloor\Models\OeeSnapshot;
|
||
use Modules\ShopFloor\Models\ProductionEvent;
|
||
use Modules\ShopFloor\Models\WorkCenter;
|
||
|
||
class OeeCalculationService
|
||
{
|
||
/**
|
||
* OEE = Availability × Performance × Quality
|
||
*/
|
||
public function calculateForWorkCenter(int $businessId, int $workCenterId, Carbon $date): OeeSnapshot
|
||
{
|
||
$workCenter = WorkCenter::forBusiness($businessId)->findOrFail($workCenterId);
|
||
$start = $date->copy()->startOfDay();
|
||
$end = $date->copy()->endOfDay();
|
||
|
||
$plannedMinutes = $this->plannedMinutes($date);
|
||
$downtimeMinutes = $this->downtimeMinutes($businessId, $workCenterId, $start, $end);
|
||
$runMinutes = max(0, $plannedMinutes - $downtimeMinutes);
|
||
|
||
$availability = $plannedMinutes > 0
|
||
? round(($runMinutes / $plannedMinutes) * 100, 4)
|
||
: 0;
|
||
|
||
$output = $this->outputTotals($businessId, $workCenterId, $start, $end);
|
||
$idealOutput = $this->idealOutput($workCenter, $runMinutes);
|
||
|
||
$performance = $idealOutput > 0
|
||
? round(min(100, ($output['actual'] / $idealOutput) * 100), 4)
|
||
: 0;
|
||
|
||
$quality = $output['actual'] > 0
|
||
? round(($output['good'] / $output['actual']) * 100, 4)
|
||
: 0;
|
||
|
||
$oee = round(($availability / 100) * ($performance / 100) * ($quality / 100) * 100, 4);
|
||
|
||
return OeeSnapshot::updateOrCreate(
|
||
[
|
||
'business_id' => $businessId,
|
||
'work_center_id' => $workCenterId,
|
||
'snapshot_date' => $date->toDateString(),
|
||
],
|
||
[
|
||
'availability_pct' => $availability,
|
||
'performance_pct' => $performance,
|
||
'quality_pct' => $quality,
|
||
'oee_pct' => $oee,
|
||
'planned_minutes' => $plannedMinutes,
|
||
'run_minutes' => $runMinutes,
|
||
'downtime_minutes' => $downtimeMinutes,
|
||
'ideal_output' => $idealOutput,
|
||
'actual_output' => $output['actual'],
|
||
'good_output' => $output['good'],
|
||
]
|
||
);
|
||
}
|
||
|
||
public function calculateAllForDate(int $businessId, Carbon $date): array
|
||
{
|
||
$snapshots = [];
|
||
$workCenters = WorkCenter::forBusiness($businessId)->where('status', 'active')->get();
|
||
|
||
foreach ($workCenters as $workCenter) {
|
||
$snapshots[] = $this->calculateForWorkCenter($businessId, $workCenter->id, $date);
|
||
}
|
||
|
||
return $snapshots;
|
||
}
|
||
|
||
public function dashboardStats(int $businessId): array
|
||
{
|
||
$today = now()->toDateString();
|
||
$snapshots = OeeSnapshot::forBusiness($businessId)
|
||
->where('snapshot_date', $today)
|
||
->get();
|
||
|
||
$avgOee = $snapshots->avg('oee_pct') ?? 0;
|
||
$activeCenters = WorkCenter::forBusiness($businessId)->where('status', 'active')->count();
|
||
$openDowntime = DowntimeEvent::forBusiness($businessId)->whereNull('ended_at')->count();
|
||
$todayEvents = ProductionEvent::forBusiness($businessId)
|
||
->whereDate('recorded_at', $today)
|
||
->count();
|
||
|
||
return [
|
||
'avg_oee' => round($avgOee, 2),
|
||
'active_work_centers' => $activeCenters,
|
||
'open_downtime' => $openDowntime,
|
||
'today_events' => $todayEvents,
|
||
];
|
||
}
|
||
|
||
protected function plannedMinutes(Carbon $date): int
|
||
{
|
||
return 8 * 60;
|
||
}
|
||
|
||
protected function downtimeMinutes(int $businessId, int $workCenterId, Carbon $start, Carbon $end): int
|
||
{
|
||
$events = DowntimeEvent::forBusiness($businessId)
|
||
->where('work_center_id', $workCenterId)
|
||
->where('started_at', '<=', $end)
|
||
->where(function ($q) use ($start) {
|
||
$q->whereNull('ended_at')->orWhere('ended_at', '>=', $start);
|
||
})
|
||
->get();
|
||
|
||
$total = 0;
|
||
foreach ($events as $event) {
|
||
$eventStart = $event->started_at->greaterThan($start) ? $event->started_at : $start;
|
||
$eventEnd = $event->ended_at ? ($event->ended_at->lessThan($end) ? $event->ended_at : $end) : $end;
|
||
$total += max(0, $eventStart->diffInMinutes($eventEnd));
|
||
}
|
||
|
||
return (int) $total;
|
||
}
|
||
|
||
protected function outputTotals(int $businessId, int $workCenterId, Carbon $start, Carbon $end): array
|
||
{
|
||
$events = ProductionEvent::forBusiness($businessId)
|
||
->where('work_center_id', $workCenterId)
|
||
->whereBetween('recorded_at', [$start, $end])
|
||
->whereIn('event_type', ['complete', 'scrap'])
|
||
->get();
|
||
|
||
$actual = (float) $events->sum('quantity');
|
||
$good = (float) $events->sum('good_quantity');
|
||
|
||
if ($actual <= 0) {
|
||
$good = (float) $events->where('event_type', 'complete')->sum('quantity');
|
||
$actual = $good + (float) $events->sum('scrap_quantity');
|
||
}
|
||
|
||
return ['actual' => $actual, 'good' => $good];
|
||
}
|
||
|
||
protected function idealOutput(WorkCenter $workCenter, int $runMinutes): float
|
||
{
|
||
$capacityPerHour = (float) $workCenter->capacity_per_hour;
|
||
|
||
if ($capacityPerHour <= 0 || $runMinutes <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
return round(($capacityPerHour / 60) * $runMinutes, 4);
|
||
}
|
||
}
|