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); } }