130 lines
5.4 KiB
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,
|
|
];
|
|
}
|
|
}
|