478 lines
18 KiB
PHP
478 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Dashboards;
|
|
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class TestPackageDashboardService
|
|
{
|
|
/**
|
|
* Build summary grouped by test type, area and subcontractor.
|
|
*/
|
|
public static function summaryByTestType(): array
|
|
{
|
|
$detailRows = self::fetchSummaryRows([
|
|
['column' => 'test_type', 'alias' => 'test_type', 'default' => 'Unknown'],
|
|
['column' => 'area', 'alias' => 'area', 'default' => 'Unknown'],
|
|
['column' => 'subcontractor', 'alias' => 'subcontractor', 'default' => 'Unassigned'],
|
|
]);
|
|
|
|
$groupTotals = self::buildGroupTotals(
|
|
$detailRows,
|
|
'test_type',
|
|
[
|
|
'area' => 'Grand Total',
|
|
'subcontractor' => 'All Subcontractors',
|
|
]
|
|
);
|
|
|
|
$overall = self::buildOverallRow(
|
|
$detailRows,
|
|
[
|
|
'test_type' => 'All Test Types',
|
|
'area' => 'Overall Grand Total',
|
|
'subcontractor' => 'All Subcontractors',
|
|
]
|
|
);
|
|
|
|
return self::finalizeRows(array_merge($detailRows, $groupTotals, [$overall]));
|
|
}
|
|
|
|
/**
|
|
* Build summary grouped by area, subcontractor and responsible person.
|
|
*/
|
|
public static function summaryByArea(): array
|
|
{
|
|
$detailRows = self::fetchSummaryRows([
|
|
['column' => 'area', 'alias' => 'area', 'default' => 'Unknown'],
|
|
['column' => 'subcontractor', 'alias' => 'subcontractor', 'default' => 'Unassigned'],
|
|
['column' => 'responsible_person', 'alias' => 'responsible_person', 'default' => 'Unassigned'],
|
|
]);
|
|
|
|
$groupTotals = self::buildGroupTotals(
|
|
$detailRows,
|
|
'area',
|
|
[
|
|
'subcontractor' => 'All Subcontractors',
|
|
'responsible_person' => 'All Responsible',
|
|
]
|
|
);
|
|
|
|
$overall = self::buildOverallRow(
|
|
$detailRows,
|
|
[
|
|
'area' => 'Overall Grand Total',
|
|
'subcontractor' => 'All Subcontractors',
|
|
'responsible_person' => 'All Responsible',
|
|
]
|
|
);
|
|
|
|
return self::finalizeRows(array_merge($detailRows, $groupTotals, [$overall]));
|
|
}
|
|
|
|
/**
|
|
* Normalized dataset that powers pivot views (test type + area perspectives).
|
|
*/
|
|
public static function statusPivotDataset(): array
|
|
{
|
|
$baseRows = self::finalizeRows(self::fetchSummaryRows([
|
|
['column' => 'area', 'alias' => 'area', 'default' => 'Unknown'],
|
|
['column' => 'test_type', 'alias' => 'test_type', 'default' => 'Unknown'],
|
|
['column' => 'subcontractor', 'alias' => 'subcontractor', 'default' => 'Unassigned'],
|
|
['column' => 'responsible_person', 'alias' => 'responsible_person', 'default' => 'Unassigned'],
|
|
]));
|
|
|
|
return [
|
|
'rows' => $baseRows,
|
|
'normalized' => self::expandRowsForPivot($baseRows),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Return monthly trend, pie and subcontractor/project trend datasets.
|
|
*/
|
|
public static function monthlyTrends(int $year): array
|
|
{
|
|
$periodExpr = "COALESCE(tpbs.target_test_date, tpbs.created_at)";
|
|
$monthExpr = "DATE_FORMAT($periodExpr, '%Y-%m-01')";
|
|
$labelExpr = "DATE_FORMAT($periodExpr, '%b %Y')";
|
|
$testedCondition = self::baseStatusTestedCondition('tpbs');
|
|
|
|
$projectData = DB::table('test_pack_base_statuses as tpbs')
|
|
->leftJoinSub(self::projectMapSubquery(), 'projects', function ($join) {
|
|
$join->on('projects.test_package_no', '=', 'tpbs.test_package_no');
|
|
})
|
|
->selectRaw("
|
|
COALESCE(projects.project, 'Unknown') as project,
|
|
$monthExpr as period_start,
|
|
$labelExpr as period_label,
|
|
COUNT(*) as total_packages,
|
|
SUM(CASE WHEN ($testedCondition) THEN 1 ELSE 0 END) as tested_packages
|
|
")
|
|
->whereYear(DB::raw($periodExpr), $year)
|
|
->groupBy('project', DB::raw($monthExpr), DB::raw($labelExpr))
|
|
->orderBy(DB::raw($monthExpr))
|
|
->orderBy('project')
|
|
->get()
|
|
->map(function ($row) {
|
|
$row = (array) $row;
|
|
$row['total_packages'] = (int) $row['total_packages'];
|
|
$row['tested_packages'] = (int) $row['tested_packages'];
|
|
$row['remaining_packages'] = max($row['total_packages'] - $row['tested_packages'], 0);
|
|
$row['period_label'] = $row['period_label'] ?? '';
|
|
$row['series_key'] = $row['project'];
|
|
$row['display_label'] = trim(($row['project'] ?? 'Unknown') . ' | ' . ($row['period_label'] ?? ''));
|
|
return $row;
|
|
})
|
|
->toArray();
|
|
|
|
$totals = self::calculateMonthlyTotals($projectData);
|
|
|
|
$subcontractorData = DB::table('test_pack_base_statuses as tpbs')
|
|
->leftJoinSub(self::projectMapSubquery(), 'projects', function ($join) {
|
|
$join->on('projects.test_package_no', '=', 'tpbs.test_package_no');
|
|
})
|
|
->selectRaw("
|
|
COALESCE(projects.project, 'Unknown') as project,
|
|
COALESCE(tpbs.subcontractor, 'Unassigned') as subcontractor,
|
|
$monthExpr as period_start,
|
|
$labelExpr as period_label,
|
|
SUM(CASE WHEN ($testedCondition) THEN 1 ELSE 0 END) as tested_packages
|
|
")
|
|
->whereYear(DB::raw($periodExpr), $year)
|
|
->groupBy('project', 'subcontractor', DB::raw($monthExpr), DB::raw($labelExpr))
|
|
->orderBy(DB::raw($monthExpr))
|
|
->orderBy('project')
|
|
->orderBy('subcontractor')
|
|
->get()
|
|
->map(function ($row) {
|
|
$row = (array) $row;
|
|
$row['tested_packages'] = (int) $row['tested_packages'];
|
|
$row['series_key'] = trim(($row['project'] ?? 'Unknown') . ' - ' . ($row['subcontractor'] ?? 'Unassigned'));
|
|
return $row;
|
|
})
|
|
->toArray();
|
|
|
|
return [
|
|
'monthly' => $projectData,
|
|
'pie' => [
|
|
['status' => 'Tested', 'value' => $totals['tested']],
|
|
['status' => 'Remaining', 'value' => $totals['remaining']],
|
|
],
|
|
'summary' => $totals,
|
|
'subcontractor' => $subcontractorData,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Map of test_package_no => project.
|
|
*/
|
|
public static function projectMap(): array
|
|
{
|
|
return self::projectMapSubquery()
|
|
->pluck('project', 'test_package_no')
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* Fetch aggregated rows based on provided grouping definition.
|
|
*/
|
|
protected static function fetchSummaryRows(array $grouping): array
|
|
{
|
|
$selectParts = [];
|
|
$groupParts = [];
|
|
|
|
foreach ($grouping as $definition) {
|
|
$expression = "COALESCE({$definition['column']}, '{$definition['default']}')";
|
|
$selectParts[] = "{$expression} as {$definition['alias']}";
|
|
$groupParts[] = DB::raw($expression);
|
|
}
|
|
|
|
$select = implode(', ', array_merge($selectParts, self::metricExpressions()));
|
|
|
|
$query = DB::table('test_pack_base_statuses')
|
|
->selectRaw($select)
|
|
->groupBy(...$groupParts);
|
|
|
|
foreach ($grouping as $definition) {
|
|
$query->orderBy($definition['alias']);
|
|
}
|
|
|
|
return $query->get()
|
|
->map(function ($row) {
|
|
$data = (array) $row;
|
|
foreach (self::numericColumns() as $column) {
|
|
if (isset($data[$column])) {
|
|
$data[$column] = (float) $data[$column];
|
|
}
|
|
}
|
|
$data['sort_index'] = 0;
|
|
$data['is_total_row'] = false;
|
|
return $data;
|
|
})
|
|
->toArray();
|
|
}
|
|
|
|
protected static function buildGroupTotals(array $rows, string $groupField, array $overrides): array
|
|
{
|
|
$numeric = self::numericColumns();
|
|
$groupTotals = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$groupValue = $row[$groupField] ?? 'Unknown';
|
|
if (!isset($groupTotals[$groupValue])) {
|
|
$groupTotals[$groupValue] = array_fill_keys($numeric, 0);
|
|
}
|
|
foreach ($numeric as $column) {
|
|
$groupTotals[$groupValue][$column] += (float) ($row[$column] ?? 0);
|
|
}
|
|
}
|
|
|
|
$result = [];
|
|
foreach ($groupTotals as $groupValue => $values) {
|
|
$result[] = array_merge(
|
|
[
|
|
$groupField => $groupValue,
|
|
'sort_index' => 1,
|
|
'is_total_row' => true,
|
|
],
|
|
$overrides,
|
|
$values
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
protected static function buildOverallRow(array $rows, array $overrides): array
|
|
{
|
|
$numeric = self::numericColumns();
|
|
$totals = array_fill_keys($numeric, 0);
|
|
|
|
foreach ($rows as $row) {
|
|
foreach ($numeric as $column) {
|
|
$totals[$column] += (float) ($row[$column] ?? 0);
|
|
}
|
|
}
|
|
|
|
return array_merge(
|
|
$overrides,
|
|
$totals,
|
|
[
|
|
'sort_index' => 2,
|
|
'is_total_row' => true,
|
|
]
|
|
);
|
|
}
|
|
|
|
protected static function finalizeRows(array $rows): array
|
|
{
|
|
$wdiColumns = self::wdiColumns();
|
|
|
|
return array_values(array_map(function ($row) use ($wdiColumns) {
|
|
foreach ($wdiColumns as $column) {
|
|
if (isset($row[$column])) {
|
|
$row[$column] = round((float) $row[$column], 2);
|
|
}
|
|
}
|
|
return $row;
|
|
}, $rows));
|
|
}
|
|
|
|
/**
|
|
* Convert aggregated summary rows into normalized pivot-friendly records.
|
|
*/
|
|
protected static function expandRowsForPivot(array $rows): array
|
|
{
|
|
$blueprint = self::pivotMetricBlueprint();
|
|
$normalized = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$base = [
|
|
'area' => $row['area'] ?? 'Unknown',
|
|
'test_type' => $row['test_type'] ?? 'Unknown',
|
|
'subcontractor' => $row['subcontractor'] ?? 'Unassigned',
|
|
'responsible_person' => $row['responsible_person'] ?? 'Unassigned',
|
|
];
|
|
|
|
foreach ($blueprint as $group => $definitions) {
|
|
foreach ($definitions as $definition) {
|
|
$column = $definition['column'];
|
|
$value = (float) ($row[$column] ?? 0);
|
|
|
|
$normalized[] = array_merge($base, [
|
|
'metric_group' => $group,
|
|
'metric_label' => $definition['label'],
|
|
'metric_key' => $column,
|
|
'metric_type' => $definition['type'],
|
|
'value' => round($value, 2),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
/**
|
|
* Blueprint describing how numeric columns should be exposed inside the pivot grids.
|
|
*/
|
|
protected static function pivotMetricBlueprint(): array
|
|
{
|
|
return [
|
|
'Total TP' => [
|
|
['label' => 'Qty', 'column' => 'total_tp_qty', 'type' => 'qty'],
|
|
['label' => 'WDI', 'column' => 'total_tp_wdi', 'type' => 'wdi'],
|
|
],
|
|
'Test Package Preparation' => [
|
|
['label' => 'Qty', 'column' => 'prep_qty', 'type' => 'qty'],
|
|
['label' => 'WDI', 'column' => 'prep_wdi', 'type' => 'wdi'],
|
|
],
|
|
'Approved TP' => [
|
|
['label' => 'Qty', 'column' => 'approved_qty', 'type' => 'qty'],
|
|
['label' => 'WDI', 'column' => 'approved_wdi', 'type' => 'wdi'],
|
|
],
|
|
'Line Check' => [
|
|
['label' => 'Qty', 'column' => 'linecheck_qty', 'type' => 'qty'],
|
|
['label' => 'WDI', 'column' => 'linecheck_wdi', 'type' => 'wdi'],
|
|
],
|
|
'Punch Points' => [
|
|
['label' => 'A Punch', 'column' => 'a_punch_qty', 'type' => 'count'],
|
|
['label' => 'B Punch', 'column' => 'b_punch_qty', 'type' => 'count'],
|
|
['label' => 'C Punch', 'column' => 'c_punch_qty', 'type' => 'count'],
|
|
],
|
|
'TP Released for NDT Clear' => [
|
|
['label' => 'Qty', 'column' => 'tp_released_qty', 'type' => 'qty'],
|
|
['label' => 'WDI', 'column' => 'tp_released_wdi', 'type' => 'wdi'],
|
|
],
|
|
'Status Summary' => [
|
|
['label' => 'B Waiting', 'column' => 'b_waiting_qty', 'type' => 'status'],
|
|
['label' => 'NDT', 'column' => 'ndt_status_qty', 'type' => 'status'],
|
|
['label' => 'Punch', 'column' => 'punch_status_qty', 'type' => 'status'],
|
|
['label' => 'Ready for Reinstatement', 'column' => 'ready_for_reinstatement_qty', 'type' => 'status'],
|
|
['label' => 'Ready for Test', 'column' => 'ready_for_test_qty', 'type' => 'status'],
|
|
['label' => 'Repair Waiting', 'column' => 'repair_waiting_qty', 'type' => 'status'],
|
|
['label' => 'Welding Ongoing', 'column' => 'welding_ongoing_qty', 'type' => 'status'],
|
|
['label' => 'Completed', 'column' => 'completed_qty', 'type' => 'status'],
|
|
],
|
|
];
|
|
}
|
|
|
|
protected static function metricExpressions(): array
|
|
{
|
|
$wdi = 'COALESCE(total_wdi, 0)';
|
|
$tpStatus = "LOWER(COALESCE(tp_status, ''))";
|
|
$weldingStatus = "LOWER(COALESCE(welding_status, ''))";
|
|
$generalStatus = "LOWER(COALESCE(tp_status, welding_status, ''))";
|
|
|
|
return [
|
|
'COUNT(*) as total_tp_qty',
|
|
"SUM($wdi) as total_tp_wdi",
|
|
"SUM(CASE WHEN LOWER(welding_status) LIKE '%prep%' THEN 1 ELSE 0 END) as prep_qty",
|
|
"SUM(CASE WHEN LOWER(welding_status) LIKE '%prep%' THEN $wdi ELSE 0 END) as prep_wdi",
|
|
"SUM(CASE WHEN ($tpStatus LIKE '%approved%' OR $weldingStatus LIKE '%approved%') THEN 1 ELSE 0 END) as approved_qty",
|
|
"SUM(CASE WHEN ($tpStatus LIKE '%approved%' OR $weldingStatus LIKE '%approved%') THEN $wdi ELSE 0 END) as approved_wdi",
|
|
"SUM(CASE WHEN punch_status IS NOT NULL AND punch_status != '' THEN 1 ELSE 0 END) as linecheck_qty",
|
|
"SUM(CASE WHEN punch_status IS NOT NULL AND punch_status != '' THEN $wdi ELSE 0 END) as linecheck_wdi",
|
|
'SUM(COALESCE(punch_a_quantity, 0)) as a_punch_qty',
|
|
'SUM(COALESCE(punch_b_quantity, 0)) as b_punch_qty',
|
|
'SUM(COALESCE(punch_c_quantity, 0)) as c_punch_qty',
|
|
"SUM(CASE WHEN $tpStatus LIKE '%released%' THEN 1 ELSE 0 END) as tp_released_qty",
|
|
"SUM(CASE WHEN $tpStatus LIKE '%released%' THEN $wdi ELSE 0 END) as tp_released_wdi",
|
|
"SUM(CASE WHEN $generalStatus = 'b waiting' THEN 1 ELSE 0 END) as b_waiting_qty",
|
|
"SUM(CASE WHEN $generalStatus = 'ndt' THEN 1 ELSE 0 END) as ndt_status_qty",
|
|
"SUM(CASE WHEN $generalStatus = 'punch' THEN 1 ELSE 0 END) as punch_status_qty",
|
|
"SUM(CASE WHEN $generalStatus = 'ready for reinstatement' THEN 1 ELSE 0 END) as ready_for_reinstatement_qty",
|
|
"SUM(CASE WHEN $generalStatus = 'ready for test' THEN 1 ELSE 0 END) as ready_for_test_qty",
|
|
"SUM(CASE WHEN $generalStatus = 'repair waiting' THEN 1 ELSE 0 END) as repair_waiting_qty",
|
|
"SUM(CASE WHEN $generalStatus = 'welding ongoing' THEN 1 ELSE 0 END) as welding_ongoing_qty",
|
|
"SUM(CASE WHEN $generalStatus = 'completed' THEN 1 ELSE 0 END) as completed_qty",
|
|
];
|
|
}
|
|
|
|
protected static function numericColumns(): array
|
|
{
|
|
return [
|
|
'total_tp_qty',
|
|
'total_tp_wdi',
|
|
'prep_qty',
|
|
'prep_wdi',
|
|
'approved_qty',
|
|
'approved_wdi',
|
|
'linecheck_qty',
|
|
'a_punch_qty',
|
|
'b_punch_qty',
|
|
'c_punch_qty',
|
|
'linecheck_wdi',
|
|
'tp_released_qty',
|
|
'tp_released_wdi',
|
|
'b_waiting_qty',
|
|
'ndt_status_qty',
|
|
'punch_status_qty',
|
|
'ready_for_reinstatement_qty',
|
|
'ready_for_test_qty',
|
|
'repair_waiting_qty',
|
|
'welding_ongoing_qty',
|
|
'completed_qty',
|
|
];
|
|
}
|
|
|
|
protected static function wdiColumns(): array
|
|
{
|
|
return [
|
|
'total_tp_wdi',
|
|
'prep_wdi',
|
|
'approved_wdi',
|
|
'linecheck_wdi',
|
|
'tp_released_wdi',
|
|
];
|
|
}
|
|
|
|
protected static function projectMapSubquery()
|
|
{
|
|
return DB::table('weld_logs')
|
|
->select('test_package_no', DB::raw('MAX(project) as project'))
|
|
->whereNotNull('test_package_no')
|
|
->where('test_package_no', '!=', '')
|
|
->groupBy('test_package_no');
|
|
}
|
|
|
|
protected static function testedCondition(string $alias = ''): string
|
|
{
|
|
$prefix = $alias ? $alias . '.' : '';
|
|
|
|
$tpStatus = "LOWER({$prefix}test_status)";
|
|
$generalStatus = "LOWER(COALESCE({$prefix}tp_status, {$prefix}welding_status, ''))";
|
|
|
|
return "$tpStatus = 'accepted' OR $generalStatus IN ('ready for test','ready for reinstatement','completed')";
|
|
}
|
|
|
|
protected static function baseStatusTestedCondition(string $alias = ''): string
|
|
{
|
|
$prefix = $alias ? $alias . '.' : '';
|
|
$statusExpr = "LOWER(COALESCE({$prefix}tp_status, {$prefix}welding_status, ''))";
|
|
return "$statusExpr IN ('ready for test','ready for reinstatement','completed','accepted')";
|
|
}
|
|
|
|
protected static function calculateMonthlyTotals(array $rows): array
|
|
{
|
|
$tested = 0;
|
|
$total = 0;
|
|
|
|
foreach ($rows as $row) {
|
|
$tested += (int) ($row['tested_packages'] ?? 0);
|
|
$total += (int) ($row['total_packages'] ?? 0);
|
|
}
|
|
|
|
$remaining = max($total - $tested, 0);
|
|
|
|
return [
|
|
'total' => $total,
|
|
'tested' => $tested,
|
|
'remaining' => $remaining,
|
|
];
|
|
}
|
|
}
|
|
|