isIndustrialEngineeringAvailable()) { $result = $this->syncFromIndustrialEngineering($businessId); $created += $result['created']; $updated += $result['updated']; } if ($this->isMaintenanceAvailable()) { $result = $this->syncFromMaintenanceEquipment($businessId); $created += $result['created']; $updated += $result['updated']; } if ($this->isShopFloorAvailable()) { $result = $this->syncFromShopFloor($businessId); $created += $result['created']; $updated += $result['updated']; } return ['created' => $created, 'updated' => $updated]; } /** * @return Collection */ public function loadSchedulableWorkOrders( int $businessId, Carbon $horizonStart, Carbon $horizonEnd, array $options = [] ): Collection { if (! $this->isIndustrialEngineeringAvailable()) { return collect(); } $woClass = \Modules\IndustrialEngineering\Models\ManufacturingWorkOrder::class; $query = $woClass::forBusiness($businessId) ->whereIn('status', $options['statuses'] ?? ['planned', 'released', 'in_progress']) ->with(['lineRevision']); if (! ($options['ignore_dates'] ?? false)) { $query->where(function ($q) use ($horizonStart, $horizonEnd) { $q->whereNull('planned_end_date') ->orWhereBetween('planned_start_date', [$horizonStart, $horizonEnd]) ->orWhereBetween('planned_end_date', [$horizonStart, $horizonEnd]); }); } if (! empty($options['work_order_ids'])) { $query->whereIn('id', $options['work_order_ids']); } return $query->orderBy('planned_start_date')->orderBy('id')->get(); } /** * @return array> */ public function extractRoutingOperations($workOrder): array { $snapshot = $workOrder->routing_snapshot ?? []; if (is_string($snapshot)) { $snapshot = json_decode($snapshot, true) ?: []; } if (! is_array($snapshot) || empty($snapshot)) { return []; } return collect($snapshot) ->sortBy('sequence') ->values() ->all(); } /** * @return array> */ public function loadMrpDemandForPeriod(int $businessId, Carbon $monthStart, Carbon $monthEnd): array { if (! class_exists(\Modules\IndustrialEngineering\Models\MrpRun::class)) { return []; } $runs = \Modules\IndustrialEngineering\Models\MrpRun::forBusiness($businessId) ->whereBetween('created_at', [$monthStart, $monthEnd]) ->with('workOrder') ->latest('id') ->limit(50) ->get(); return $runs->map(fn ($run) => [ 'mrp_run_id' => $run->id, 'run_code' => $run->run_code, 'planned_quantity' => (float) $run->planned_quantity, 'work_order_id' => $run->work_order_id, 'work_order_number' => $run->workOrder?->work_order_number, ])->values()->all(); } /** * @return array{created: int, updated: int} */ protected function syncFromIndustrialEngineering(int $businessId): array { $created = 0; $updated = 0; $defaultHours = (float) config('advancedplanning.default_daily_capacity_hours', 8); $defaultEff = (float) config('advancedplanning.default_efficiency_pct', 85); if (class_exists(\Modules\IndustrialEngineering\Models\LineTemplate::class)) { $lines = \Modules\IndustrialEngineering\Models\LineTemplate::forBusiness($businessId) ->productionLines() ->get(); foreach ($lines as $line) { $attrs = [ 'name' => $line->name, 'resource_type' => 'line', 'ie_line_template_id' => $line->id, 'maintenance_equipment_id' => $line->maintenance_equipment_id, 'daily_capacity_hours' => $defaultHours, 'efficiency_pct' => $defaultEff, 'is_active' => true, ]; $resource = CapacityResource::forBusiness($businessId) ->where('ie_line_template_id', $line->id) ->first(); if ($resource) { $resource->update($attrs); $updated++; } else { CapacityResource::create(array_merge($attrs, ['business_id' => $businessId])); $created++; } } } if (class_exists(\Modules\IndustrialEngineering\Models\WorkCenter::class)) { $centers = \Modules\IndustrialEngineering\Models\WorkCenter::forBusiness($businessId) ->where('is_active', true) ->get(); foreach ($centers as $center) { $type = ((float) $center->machine_rate_per_hour > 0) ? 'equipment' : 'labor'; $attrs = [ 'name' => $center->name, 'resource_type' => $type, 'ie_work_center_id' => $center->id, 'daily_capacity_hours' => $defaultHours, 'efficiency_pct' => $defaultEff, 'is_active' => true, ]; $resource = CapacityResource::forBusiness($businessId) ->where('ie_work_center_id', $center->id) ->first(); if ($resource) { $resource->update($attrs); $updated++; } else { CapacityResource::create(array_merge($attrs, ['business_id' => $businessId])); $created++; } } } return ['created' => $created, 'updated' => $updated]; } /** * @return array{created: int, updated: int} */ protected function syncFromMaintenanceEquipment(int $businessId): array { $created = 0; $updated = 0; $equipment = \Modules\Maintenance\Models\Equipment::where('business_id', $businessId)->get(); $defaultHours = (float) config('advancedplanning.default_daily_capacity_hours', 8); $defaultEff = (float) config('advancedplanning.default_efficiency_pct', 85); foreach ($equipment as $item) { $existing = CapacityResource::forBusiness($businessId) ->where('maintenance_equipment_id', $item->id) ->whereNull('ie_line_template_id') ->first(); $attrs = [ 'name' => $item->name ?? $item->equipment_code ?? 'Equipment #'.$item->id, 'resource_type' => 'equipment', 'maintenance_equipment_id' => $item->id, 'daily_capacity_hours' => $defaultHours, 'efficiency_pct' => $defaultEff, 'is_active' => true, ]; if ($existing) { $existing->update($attrs); $updated++; } else { CapacityResource::create(array_merge($attrs, ['business_id' => $businessId])); $created++; } } return ['created' => $created, 'updated' => $updated]; } /** * Schedule work orders created by an MRP run within the given horizon. */ public function scheduleMrpWorkOrders( int $businessId, Carbon $horizonStart, Carbon $horizonEnd, FiniteCapacitySchedulerService $scheduler, ?int $mrpRunId = null ): ?\Modules\AdvancedPlanning\Models\ScheduleRun { if (! $this->isIndustrialEngineeringAvailable()) { return null; } $options = ['ignore_dates' => true]; if ($mrpRunId) { $run = \Modules\IndustrialEngineering\Models\MrpRun::forBusiness($businessId)->find($mrpRunId); if ($run?->work_order_id) { $options['work_order_ids'] = [$run->work_order_id]; } } return $scheduler->run($businessId, $horizonStart, $horizonEnd, 'MRP Follow-up', $options); } /** * @return array{created: int, updated: int} */ protected function syncFromShopFloor(int $businessId): array { $created = 0; $updated = 0; if (! class_exists(\Modules\ShopFloor\Models\WorkCenter::class)) { return ['created' => 0, 'updated' => 0]; } $defaultHours = (float) config('advancedplanning.default_daily_capacity_hours', 8); $defaultEff = (float) config('advancedplanning.default_efficiency_pct', 85); $centers = \Modules\ShopFloor\Models\WorkCenter::where('business_id', $businessId) ->where('status', 'active') ->get(); foreach ($centers as $center) { $dailyHours = $defaultHours; if ((float) $center->capacity_per_hour > 0) { $dailyHours = min(24, (float) $center->capacity_per_hour * $defaultHours); } $efficiency = $this->shopFloorEfficiencyForCenter($businessId, (int) $center->id, $defaultEff); $attrs = [ 'name' => $center->name.' ('.$center->code.')', 'resource_type' => 'equipment', 'ie_line_template_id' => $center->ie_line_template_id, 'maintenance_equipment_id' => $center->maintenance_equipment_id, 'daily_capacity_hours' => round($dailyHours, 2), 'efficiency_pct' => $efficiency, 'is_active' => true, ]; $resourceQuery = CapacityResource::forBusiness($businessId); if ($center->ie_line_template_id) { $resource = (clone $resourceQuery)->where('ie_line_template_id', $center->ie_line_template_id)->first(); } elseif ($center->maintenance_equipment_id) { $resource = (clone $resourceQuery)->where('maintenance_equipment_id', $center->maintenance_equipment_id)->first(); } else { $resource = (clone $resourceQuery)->where('name', $attrs['name'])->first(); } if ($resource) { $resource->update($attrs); $updated++; } else { CapacityResource::create(array_merge($attrs, ['business_id' => $businessId])); $created++; } } return ['created' => $created, 'updated' => $updated]; } protected function shopFloorEfficiencyForCenter(int $businessId, int $workCenterId, float $fallback): float { if (! class_exists(\Modules\ShopFloor\Models\OeeSnapshot::class)) { return $fallback; } $oee = \Modules\ShopFloor\Models\OeeSnapshot::where('business_id', $businessId) ->where('work_center_id', $workCenterId) ->orderByDesc('snapshot_date') ->value('oee_pct'); if ($oee === null || (float) $oee <= 0) { return $fallback; } return max(1, min(100, round((float) $oee, 2))); } }