ultimatepos/Modules/IndustrialEngineering/Services/DigitalThreadAnalyticsService.php

130 lines
5.4 KiB
PHP

<?php
namespace Modules\IndustrialEngineering\Services;
use Modules\IndustrialEngineering\Models\Bom;
use Modules\IndustrialEngineering\Models\EngineeringChangeOrder;
use Modules\IndustrialEngineering\Models\ItemMaster;
use Modules\IndustrialEngineering\Models\LineRevision;
use Modules\IndustrialEngineering\Models\LineTemplate;
use Modules\IndustrialEngineering\Models\MrpRun;
use Modules\IndustrialEngineering\Models\ProductPlatform;
use Modules\IndustrialEngineering\Models\ReleasePackage;
use Modules\IndustrialEngineering\Models\ResearchProject;
use Modules\IndustrialEngineering\Support\CmmsAvailability;
/**
* Enterprise PLM analytics — digital thread health across ERP, CMMS, quality, time, R&D.
*/
class DigitalThreadAnalyticsService
{
public function __construct(
protected ConnectionGateService $connectionGate,
protected LiveBomCostingService $liveCosting
) {
}
public function dashboard(int $businessId): array
{
$items = ItemMaster::forBusiness($businessId)->get(['id', 'variation_id', 'product_id']);
$erpLinked = $items->filter(fn ($i) => $i->variation_id || $i->product_id)->count();
$itemTotal = max(1, $items->count());
$lines = LineTemplate::forBusiness($businessId)->productionLines()->get(['id', 'maintenance_equipment_id', 'cmms_synced_at']);
$cmmsLinked = CmmsAvailability::isReady()
? $lines->filter(fn ($l) => $l->maintenance_equipment_id)->count()
: 0;
$lineTotal = max(1, $lines->count());
$mboms = Bom::forBusiness($businessId)->where('bom_type', 'mbom')->with('lines.itemMaster')->get();
$gateScores = [];
$totalShortageValue = 0;
foreach ($mboms as $mbom) {
$gate = $this->connectionGate->assessBom($mbom, 1);
$gateScores[] = $gate['stats']['erp_linked'] / max(1, $gate['stats']['total_leaves']);
$totalShortageValue += $gate['analysis']['totals']['shortage_value'] ?? 0;
}
$bomHealth = $gateScores ? round(array_sum($gateScores) / count($gateScores) * 100) : 100;
return [
'erp_link_pct' => round($erpLinked / $itemTotal * 100),
'cmms_link_pct' => CmmsAvailability::isReady() ? round($cmmsLinked / $lineTotal * 100) : 0,
'cmms_available' => CmmsAvailability::isReady(),
'bom_integration_health' => $bomHealth,
'total_shortage_value' => $totalShortageValue,
'open_ecos' => EngineeringChangeOrder::forBusiness($businessId)->whereIn('status', ['draft', 'review'])->count(),
'draft_revisions' => LineRevision::forBusiness($businessId)->where('status', 'draft')->count(),
'pending_releases' => ReleasePackage::forBusiness($businessId)->whereIn('status', ['draft', 'pending_approval'])->count(),
'active_research' => ResearchProject::forBusiness($businessId)->where('status', 'active')->count(),
'mrp_runs_month' => MrpRun::forBusiness($businessId)->where('created_at', '>=', now()->startOfMonth())->count(),
'products' => ProductPlatform::forBusiness($businessId)->count(),
'production_lines' => $lineTotal,
'recent_ecos' => EngineeringChangeOrder::forBusiness($businessId)
->with(['lineRevision', 'requestedBy'])
->latest('id')
->limit(5)
->get(),
'lines_needing_attention' => $this->linesNeedingAttention($businessId),
];
}
protected function linesNeedingAttention(int $businessId): array
{
$result = [];
$lines = LineTemplate::forBusiness($businessId)
->productionLines()
->with(['revisions.boms', 'productPlatform'])
->get();
foreach ($lines as $line) {
$assessment = $this->assessLine($line);
if ($assessment['status'] !== 'ok') {
$result[] = $assessment;
}
}
return array_slice($result, 0, 8);
}
public function assessLine(LineTemplate $line): array
{
$revision = $line->revisions->sortByDesc('id')->first();
$mbom = $revision?->boms->where('bom_type', 'mbom')->first();
if (! $mbom) {
return [
'line' => $line,
'status' => 'no_mbom',
'issues' => [__('industrialengineering::lang.issue_no_mbom')],
'can_run_mrp' => false,
'erp_unlinked' => 0,
];
}
$gate = $this->connectionGate->assessBom($mbom, 1);
$issues = array_merge($gate['blockers'], $gate['warnings']);
$cmmsMissing = CmmsAvailability::isReady() && ! $line->maintenance_equipment_id;
if ($cmmsMissing) {
$issues[] = __('industrialengineering::lang.issue_no_cmms');
}
$status = 'ok';
if (! empty($gate['blockers'])) {
$status = 'empty_bom';
} elseif ($gate['stats']['unlinked'] > 0) {
$status = 'erp_pending';
} elseif ($cmmsMissing) {
$status = 'cmms_pending';
}
return [
'line' => $line->fresh(),
'status' => $status,
'issues' => array_slice($issues, 0, 5),
'can_run_mrp' => (bool) ($gate['can_run_mrp'] ?? false),
'erp_unlinked' => (int) ($gate['stats']['unlinked'] ?? 0),
'cmms_linked' => (bool) $line->maintenance_equipment_id,
];
}
}