ultimatepos/Modules/Connector/Http/Controllers/Api/V2/EnterpriseModulesController.php

303 lines
12 KiB
PHP

<?php
namespace Modules\Connector\Http\Controllers\Api\V2;
use App\Services\Api\BusinessContextService;
use App\Utils\ModuleUtil;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\QualityManagement\Models\NcrReport;
use Modules\QualityManagement\Services\NcrWorkflowService;
use Modules\QualityManagement\Services\QmsIntegrationService;
use Modules\ShopFloor\Models\OeeSnapshot;
use Modules\ShopFloor\Models\ProductionEvent;
use Modules\ShopFloor\Models\WorkCenter;
use Modules\ShopFloor\Services\OeeCalculationService;
use Modules\ShopFloor\Services\ProductionEventService;
use Modules\SupplyChain\Models\RfqRequest;
use Modules\SupplyChain\Models\RfqResponse;
use Modules\SupplyChain\Services\RfqService;
class EnterpriseModulesController extends BaseController
{
public function __construct(
protected BusinessContextService $contextService,
protected ModuleUtil $moduleUtil
) {
}
public function qualityDashboard(Request $request, QmsIntegrationService $qms)
{
$this->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.');
}
}
}