ensureModule('QualityManagement'); $businessId = $this->contextService->getBusinessId($request); return $this->success([ 'stats' => $qms->dashboardStats($businessId), 'recent_ncrs' => NcrReport::forBusiness($businessId)->latest('id')->limit(10)->get(['id', 'ref_no', 'title', 'severity', 'status']), ]); } public function qualityNcrIndex(Request $request) { $this->ensureModule('QualityManagement'); $businessId = $this->contextService->getBusinessId($request); $ncrs = NcrReport::forBusiness($businessId)->latest('id')->paginate((int) $request->get('per_page', 20)); return $this->paginated($ncrs); } public function storeQualityNcr(Request $request, NcrWorkflowService $workflow, QmsIntegrationService $integration) { $this->ensureModule('QualityManagement'); $businessId = $this->contextService->getBusinessId($request); $validated = $request->validate([ 'title' => 'required|string|max:255', 'description' => 'nullable|string', 'severity' => 'required|in:minor,major,critical', 'source' => 'nullable|in:internal,customer,supplier,audit,inspection,production', 'contact_id' => 'nullable|integer', 'ie_bom_id' => 'nullable|integer', 'maintenance_equipment_id' => 'nullable|integer', ]); $ncr = $workflow->createNcr($businessId, array_merge($validated, [ 'source' => $validated['source'] ?? 'internal', ]), $integration); return $this->success($ncr, 'NCR created.', 201); } public function storeProductionEvent(Request $request, ProductionEventService $eventService) { $this->ensureModule('ShopFloor'); $businessId = $this->contextService->getBusinessId($request); $validated = $request->validate([ 'work_center_id' => 'required|integer', 'ie_work_order_id' => 'nullable|integer', 'event_type' => 'required|in:start,stop,complete,scrap', 'quantity' => 'nullable|numeric|min:0', 'good_quantity' => 'nullable|numeric|min:0', 'scrap_quantity' => 'nullable|numeric|min:0', 'lot_no' => 'nullable|string|max:100', 'serial_no' => 'nullable|string|max:100', 'recorded_at' => 'nullable|date', ]); WorkCenter::forBusiness($businessId)->findOrFail($validated['work_center_id']); $event = $eventService->recordEvent($businessId, $validated); return $this->success($event->load('workCenter'), 'Production event recorded.', 201); } public function submitRfqResponse($id, Request $request, RfqService $rfqService) { $this->ensureModule('SupplyChain'); $businessId = $this->contextService->getBusinessId($request); $validated = $request->validate([ 'contact_id' => 'required|integer', 'quoted_price' => 'required|numeric|min:0', 'lead_days' => 'required|integer|min:1', 'notes' => 'nullable|string|max:2000', ]); $rfq = RfqRequest::forBusiness($businessId)->findOrFail($id); $hasInvite = RfqResponse::where('rfq_id', $rfq->id) ->where('contact_id', $validated['contact_id']) ->exists(); if (! $hasInvite) { return $this->error('Supplier not invited to this RFQ.', 404); } if ($rfq->status !== 'open') { return $this->error('RFQ is not open for responses.', 422); } $response = $rfqService->submitResponse( $rfq, (int) $validated['contact_id'], (float) $validated['quoted_price'], (int) $validated['lead_days'], $validated['notes'] ?? null ); return $this->success($response, 'RFQ response submitted.'); } public function shopFloorDashboard(Request $request) { $this->ensureModule('ShopFloor'); $businessId = $this->contextService->getBusinessId($request); $workCenters = WorkCenter::forBusiness($businessId)->count(); $eventsToday = ProductionEvent::forBusiness($businessId) ->whereDate('recorded_at', today()) ->count(); $avgOee = OeeSnapshot::forBusiness($businessId) ->where('snapshot_date', '>=', now()->subDays(30)->toDateString()) ->avg('oee_pct'); return $this->success([ 'work_centers' => $workCenters, 'production_events_today' => $eventsToday, 'avg_oee_30d' => round((float) $avgOee, 2), 'recent_events' => ProductionEvent::forBusiness($businessId)->with('workCenter')->latest('recorded_at')->limit(10)->get(), ]); } public function shopFloorOee(Request $request, OeeCalculationService $oee) { $this->ensureModule('ShopFloor'); $businessId = $this->contextService->getBusinessId($request); $workCenterId = (int) $request->get('work_center_id'); if ($workCenterId) { WorkCenter::forBusiness($businessId)->findOrFail($workCenterId); $date = Carbon::parse($request->get('date', today()->toDateString())); $snapshot = $oee->calculateForWorkCenter($businessId, $workCenterId, $date); return $this->success($snapshot); } $snapshots = OeeSnapshot::forBusiness($businessId) ->where('snapshot_date', '>=', $request->get('from', now()->subDays(7)->toDateString())) ->orderByDesc('snapshot_date') ->limit(50) ->get(); return $this->success(['snapshots' => $snapshots]); } public function supplyChainDashboard(Request $request) { $this->ensureModule('SupplyChain'); $businessId = $this->contextService->getBusinessId($request); return $this->success([ 'scorecards' => Schema::hasTable('sc_supplier_scorecards') ? DB::table('sc_supplier_scorecards')->where('business_id', $businessId)->count() : 0, 'forecasts' => Schema::hasTable('sc_demand_forecasts') ? DB::table('sc_demand_forecasts')->where('business_id', $businessId)->count() : 0, 'open_rfqs' => Schema::hasTable('sc_rfq_requests') ? DB::table('sc_rfq_requests')->where('business_id', $businessId)->whereIn('status', ['draft', 'open'])->count() : 0, 'avg_supplier_score' => Schema::hasTable('sc_supplier_scorecards') ? round((float) DB::table('sc_supplier_scorecards')->where('business_id', $businessId)->avg('overall_score'), 2) : 0, ]); } public function supplyChainScorecards(Request $request) { $this->ensureModule('SupplyChain'); $businessId = $this->contextService->getBusinessId($request); if (! Schema::hasTable('sc_supplier_scorecards')) { return $this->success([]); } $rows = DB::table('sc_supplier_scorecards') ->where('business_id', $businessId) ->orderByDesc('overall_score') ->paginate((int) $request->get('per_page', 20)); return $this->paginated($rows); } public function advancedPlanningDashboard(Request $request) { $this->ensureModule('AdvancedPlanning'); $businessId = $this->contextService->getBusinessId($request); return $this->success([ 'capacity_resources' => Schema::hasTable('ap_capacity_resources') ? DB::table('ap_capacity_resources')->where('business_id', $businessId)->count() : 0, 'schedule_runs' => Schema::hasTable('ap_schedule_runs') ? DB::table('ap_schedule_runs')->where('business_id', $businessId)->count() : 0, 'sop_cycles' => Schema::hasTable('ap_sop_cycles') ? DB::table('ap_sop_cycles')->where('business_id', $businessId)->count() : 0, 'latest_schedule' => Schema::hasTable('ap_schedule_runs') ? DB::table('ap_schedule_runs')->where('business_id', $businessId)->orderByDesc('id')->first() : null, ]); } public function cpqIndex(Request $request) { $this->ensureModule('IndustrialEngineering'); $businessId = $this->contextService->getBusinessId($request); if (! Schema::hasTable('ie_cpq_configurations')) { return $this->success([]); } $configs = DB::table('ie_cpq_configurations') ->where('business_id', $businessId) ->whereNull('deleted_at') ->orderByDesc('id') ->paginate((int) $request->get('per_page', 20)); return $this->paginated($configs); } public function wmsDashboard(Request $request) { $this->ensureModule('InventoryManagement'); $businessId = $this->contextService->getBusinessId($request); return $this->success([ 'active_waves' => Schema::hasTable('im_wms_waves') ? DB::table('im_wms_waves')->where('business_id', $businessId)->whereIn('status', ['planned', 'released', 'picking'])->count() : 0, 'yard_locations' => Schema::hasTable('im_wms_yard_locations') ? DB::table('im_wms_yard_locations')->where('business_id', $businessId)->count() : 0, 'rf_scans_today' => Schema::hasTable('im_wms_rf_scans') ? DB::table('im_wms_rf_scans')->where('business_id', $businessId)->whereDate('scanned_at', today())->count() : 0, ]); } public function integrationHubDashboard(Request $request) { $this->ensureModule('IntegrationHub'); $businessId = $this->contextService->getBusinessId($request); return $this->success([ 'webhooks' => Schema::hasTable('ih_webhook_endpoints') ? DB::table('ih_webhook_endpoints')->where('business_id', $businessId)->count() : 0, 'odata_tokens' => Schema::hasTable('ih_odata_tokens') ? DB::table('ih_odata_tokens')->where('business_id', $businessId)->where('is_active', 1)->count() : 0, 'export_jobs' => Schema::hasTable('ih_export_jobs') ? DB::table('ih_export_jobs')->where('business_id', $businessId)->count() : 0, 'recent_exports' => Schema::hasTable('ih_export_jobs') ? DB::table('ih_export_jobs')->where('business_id', $businessId)->orderByDesc('id')->limit(5)->get() : [], ]); } protected function ensureModule(string $module): void { if (! $this->moduleUtil->isModuleInstalled($module)) { abort(403, 'Unauthorized action.'); } } }