İlk temizlik tamamlandı bir önceki projeden

This commit is contained in:
Ümit Tunç
2026-04-28 21:14:25 +03:00
commit f80443aec0
10000 changed files with 959965 additions and 0 deletions
@@ -0,0 +1,477 @@
<?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,
];
}
}
@@ -0,0 +1,188 @@
<?php
namespace App\Services\DocumentManager;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use FilesystemIterator;
class FolderCatalog
{
/**
* Cached flattened catalog.
*/
protected ?Collection $flattened = null;
/**
* Raw catalog definition pulled from configuration.
*/
protected array $catalog;
public function __construct(?array $catalog = null)
{
$this->catalog = $catalog ?? (array) config('document-folders.catalog', []);
}
/**
* Get the raw catalog structure as defined in the configuration file.
*/
public function all(): array
{
return $this->catalog;
}
/**
* Get a flattened collection containing each folder entry with metadata.
*/
public function flattened(): Collection
{
if ($this->flattened !== null) {
return $this->flattened;
}
$items = $this->flattenNodes($this->catalog);
return $this->flattened = collect($items)
->map(function (array $item) {
$path = trim($item['path'] ?? '', '/');
$number = Arr::get($item, 'number');
$name = Arr::get($item, 'name');
return [
'number' => $number,
'name' => $name,
'path' => $path,
'default_visible' => Arr::get($item, 'default_visible', true),
'is_system' => Arr::get($item, 'is_system', false),
'level' => Arr::get($item, 'level', 0),
'display' => trim(sprintf('%s %s', $number, $name)),
];
})
->filter(fn ($item) => $item['path'] !== '')
->values();
}
/**
* Return a collection of directory paths relative to storage/documents.
*/
public function paths(): Collection
{
return $this->flattened()
->pluck('path')
->map(fn ($path) => trim($path, '/'))
->unique()
->values();
}
/**
* Create missing directories under the provided documents root path.
*
* @return array{created: array, existing: array}
*/
public function sync(string $rootPath, bool $dryRun = false): array
{
$rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR);
$created = [];
$existing = [];
foreach ($this->paths() as $relativePath) {
$fullPath = $rootPath . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativePath);
if (File::isDirectory($fullPath)) {
$existing[] = $relativePath;
continue;
}
if (! $dryRun) {
File::ensureDirectoryExists($fullPath);
}
$created[] = $relativePath;
}
return compact('created', 'existing');
}
/**
* Detect directories that exist on disk but are not part of the catalog.
*/
public function detectExtra(string $rootPath): Collection
{
$rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR);
if (! File::isDirectory($rootPath)) {
return collect();
}
$defined = $this->paths();
$actual = collect($this->scanDirectories($rootPath));
return $actual->diff($defined)->values();
}
/**
* Recursively flatten catalog definition.
*/
protected function flattenNodes(array $nodes, int $level = 0): array
{
$items = [];
foreach ($nodes as $node) {
if (! is_array($node)) {
continue;
}
$item = $node;
$item['level'] = $level;
$items[] = $item;
if (! empty($node['children']) && is_array($node['children'])) {
$items = array_merge($items, $this->flattenNodes($node['children'], $level + 1));
}
}
return $items;
}
/**
* Collect relative directory paths recursively from the given root.
*/
protected function scanDirectories(string $rootPath): array
{
$rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR);
$directories = [];
if (! File::isDirectory($rootPath)) {
return $directories;
}
// Include direct children first (top-level ordering)
foreach (File::directories($rootPath) as $directory) {
$relative = trim(Str::after($directory, $rootPath . DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR);
if ($relative !== '') {
$directories[] = $relative;
}
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($rootPath, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $path => $info) {
if ($info->isDir()) {
$relative = trim(Str::after($path, $rootPath . DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR);
if ($relative !== '') {
$directories[] = $relative;
}
}
}
return array_values(array_unique($directories));
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
namespace App\Services;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class JointTypeService
{
private const CACHE_KEY = 'joint_types.normalized';
private const CACHE_TTL = 3600;
public function mechanicalTypes(): array
{
return $this->all()
->where('is_mechanical', true)
->pluck('short_name_en')
->values()
->all();
}
public function weldedTypes(): array
{
return $this->all()
->where('is_welded', true)
->pluck('short_name_en')
->values()
->all();
}
public function hasNdtTypes(): array
{
return $this->all()
->where('has_ndt', true)
->pluck('short_name_en')
->values()
->all();
}
public function requiresNdt(?string $code): bool
{
if (!$code) {
return false;
}
return in_array($code, $this->hasNdtTypes(), true);
}
public function isMechanical(?string $code): bool
{
if (!$code) {
return false;
}
return in_array($code, $this->mechanicalTypes(), true);
}
public function isWelded(?string $code): bool
{
if (!$code) {
return false;
}
return in_array($code, $this->weldedTypes(), true);
}
/**
* @param EloquentBuilder|QueryBuilder $query
* @return EloquentBuilder|QueryBuilder
*/
public function filterWelded($query, string $column = 'type_of_welds')
{
return $this->applyFilter($query, $this->weldedTypes(), $column);
}
/**
* @param EloquentBuilder|QueryBuilder $query
* @return EloquentBuilder|QueryBuilder
*/
public function filterMechanical($query, string $column = 'type_of_welds')
{
return $this->applyFilter($query, $this->mechanicalTypes(), $column);
}
public function clearCache(): void
{
Cache::forget(self::CACHE_KEY);
}
private function all(): Collection
{
return Cache::remember(self::CACHE_KEY, self::CACHE_TTL, function () {
return DB::table('joint_types')
->select([
'short_name_en',
DB::raw('COALESCE(is_mechanical, 0) as is_mechanical'),
DB::raw('COALESCE(is_welded, 0) as is_welded'),
DB::raw('COALESCE(has_ndt, 0) as has_ndt'),
])
->get()
->map(function ($row) {
$row->is_mechanical = (bool) $row->is_mechanical;
$row->is_welded = (bool) $row->is_welded;
$row->has_ndt = (bool) $row->has_ndt;
return $row;
});
});
}
/**
* @param EloquentBuilder|QueryBuilder $query
* @return EloquentBuilder|QueryBuilder
*/
private function applyFilter($query, array $values, string $column)
{
if ($query instanceof EloquentBuilder || $query instanceof QueryBuilder) {
return $query->whereIn($column, $values);
}
throw new \InvalidArgumentException('Unsupported query type for joint type filtering.');
}
}
@@ -0,0 +1,137 @@
<?php
namespace App\Services\LineListTriggers\Base;
use App\Services\LineListTriggers\Contracts\LineListTriggerInterface;
use Illuminate\Support\Facades\Log;
/**
* Base class for all LineList triggers
*
* Provides common functionality like timing, logging, and shouldRun logic
*/
abstract class BaseLineListTrigger implements LineListTriggerInterface
{
/**
* @var float Start time for performance tracking
*/
protected $startTime;
/**
* @var array Execution context
*/
protected $context = [];
/**
* Get trigger name - must be implemented by child classes
*/
abstract public function getName(): string;
/**
* Get dependent fields - must be implemented by child classes
*/
abstract public function getDependentFields(): array;
/**
* Process trigger logic - must be implemented by child classes
*
* @param object $lineListData Current line list data
* @param object|null $beforeData Previous line list data
* @param array $context Execution context
* @return array Result data
*/
abstract protected function process($lineListData, $beforeData, array $context): array;
/**
* Execute the trigger with logging and error handling
*
* @param object $lineListData Current line list data
* @param object|null $beforeData Previous line list data
* @param array $context Execution context
* @return array Result data
* @throws \Throwable
*/
public function execute($lineListData, $beforeData = null, array $context = []): array
{
$this->startTime = microtime(true);
$this->context = $context;
Log::info("LineList Trigger {$this->getName()} - STARTED", [
'line_list_id' => $lineListData->id,
'line_no' => $lineListData->line_no ?? 'NULL',
'process' => $this->getName()
]);
try {
$result = $this->process($lineListData, $beforeData, $context);
$this->logCompletion($lineListData->id, 'success', $result);
return $result;
} catch (\Throwable $th) {
$this->logCompletion($lineListData->id, 'error', [
'error' => $th->getMessage(),
'file' => $th->getFile(),
'line' => $th->getLine()
]);
throw $th;
}
}
/**
* Determine if trigger should run based on changed fields
*
* @param array $changedFields List of changed field names
* @param bool $isNewRecord Whether this is a new record
* @return bool
*/
public function shouldRun(array $changedFields, bool $isNewRecord): bool
{
if ($isNewRecord) {
return true;
}
$dependentFields = $this->getDependentFields();
// If no dependent fields defined, always run
if (empty($dependentFields)) {
return true;
}
// Check if any changed field is in dependent fields
return !empty(array_intersect($changedFields, $dependentFields));
}
/**
* Check if trigger is async (default: false)
* Override in child classes if needed
*
* @return bool
*/
public function isAsync(): bool
{
return false;
}
/**
* Log trigger completion with performance metrics
*
* @param int $lineListId Line list ID
* @param string $status Execution status (success/error)
* @param array $additionalData Additional log data
*/
protected function logCompletion($lineListId, string $status, array $additionalData = [])
{
$duration = round((microtime(true) - $this->startTime) * 1000, 2);
$logLevel = $status === 'error' ? 'error' : 'info';
Log::$logLevel("LineList Trigger {$this->getName()} - COMPLETED", array_merge([
'line_list_id' => $lineListId,
'process' => $this->getName(),
'status' => $status,
'duration_ms' => $duration,
'duration_sec' => round($duration / 1000, 3)
], $additionalData));
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Services\LineListTriggers\Contracts;
/**
* Interface for LineList triggers
*
* Each trigger must implement this interface to be registered
* in the LineListTriggerRegistry and managed by LineListTriggerManager
*/
interface LineListTriggerInterface
{
/**
* Get trigger name for logging purposes
*
* @return string The human-readable name of the trigger
*/
public function getName(): string;
/**
* Check if trigger should run based on changed fields
*
* @param array $changedFields List of field names that changed
* @param bool $isNewRecord Whether this is a new record
* @return bool True if trigger should execute
*/
public function shouldRun(array $changedFields, bool $isNewRecord): bool;
/**
* Get fields that this trigger depends on
* Used to determine if trigger should run when fields change
*
* @return array List of field names this trigger depends on
*/
public function getDependentFields(): array;
/**
* Execute the trigger logic
*
* @param object $lineListData Current line list data
* @param object|null $beforeData Previous line list data (null for new records)
* @param array $context Additional context (changed_fields, is_new_record, etc)
* @return array Result data from trigger execution
*/
public function execute($lineListData, $beforeData = null, array $context = []): array;
/**
* Check if trigger can be executed asynchronously (queued)
*
* @return bool True if can be queued, false for synchronous execution
*/
public function isAsync(): bool;
}
@@ -0,0 +1,117 @@
<?php
namespace App\Services\LineListTriggers;
use Illuminate\Support\Facades\Log;
use App\Helpers\TransactionHelper;
/**
* LineList Trigger Manager
*
* Orchestrates the execution of all registered LineList triggers
* Handles shouldRun checks, synchronous execution, and logging
*/
class LineListTriggerManager
{
protected $registry;
protected $overallStartTime;
public function __construct(LineListTriggerRegistry $registry)
{
$this->registry = $registry;
}
/**
* Execute all registered triggers for a LineList save operation
*
* @param mixed $lineListData The current line list data
* @param mixed $beforeData The line list data before changes (null for new records)
* @param array $changedFields Array of field names that changed
* @param bool $isNewRecord Whether this is a new record
* @return array Results from all executed triggers
*/
public function executeTriggers(
$lineListData,
$beforeData = null,
array $changedFields = [],
bool $isNewRecord = false
): array {
$this->overallStartTime = microtime(true);
Log::info("=== LINE LIST SAVE TRIGGER STARTED ===", [
'line_list_id' => $lineListData->id,
'line_no' => $lineListData->line_no ?? 'NULL',
'timestamp' => now()->toDateTimeString(),
'is_new_record' => $isNewRecord,
'changed_fields_count' => count($changedFields),
'changed_fields' => $changedFields
]);
// Set database timeouts for long-running operations
TransactionHelper::setDatabaseTimeouts(300, 600, 600);
$results = [];
$triggers = $this->registry->getTriggersInOrder();
foreach ($triggers as $index => $trigger) {
// Check if trigger should run
if (!$trigger->shouldRun($changedFields, $isNewRecord)) {
Log::info("Skipping trigger: {$trigger->getName()}", [
'reason' => 'shouldRun returned false',
'position' => $index + 1
]);
continue;
}
// Execute trigger synchronously (LineList triggers don't support async)
try {
$result = $trigger->execute($lineListData, $beforeData, [
'changed_fields' => $changedFields,
'is_new_record' => $isNewRecord,
'beforeData' => $beforeData,
'lineList' => $lineListData
]);
$results[$trigger->getName()] = $result;
} catch (\Throwable $th) {
Log::error("Trigger execution failed: {$trigger->getName()}", [
'error' => $th->getMessage(),
'file' => $th->getFile(),
'line' => $th->getLine(),
'trace' => $th->getTraceAsString()
]);
// Re-throw for critical triggers (first 5 in registry), log for non-critical ones
if ($index < 5) {
throw $th;
} else {
$results[$trigger->getName()] = [
'error' => $th->getMessage(),
'critical' => false
];
}
}
}
$this->logOverallCompletion($lineListData->id);
return $results;
}
/**
* Log overall completion summary
*/
protected function logOverallCompletion($lineListId)
{
$overallDuration = round((microtime(true) - $this->overallStartTime) * 1000, 2);
Log::info("=== LINE LIST SAVE TRIGGER COMPLETED ===", [
'line_list_id' => $lineListId,
'timestamp' => now()->toDateTimeString(),
'total_execution_time_ms' => $overallDuration,
'total_execution_time_sec' => round($overallDuration / 1000, 3),
'peak_memory_usage_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2)
]);
}
}
@@ -0,0 +1,121 @@
<?php
namespace App\Services\LineListTriggers;
use App\Services\LineListTriggers\Triggers\PaintCycleChangeTrigger;
use App\Services\LineListTriggers\Triggers\PaintingCycleDeletionTrigger;
use App\Services\LineListTriggers\Triggers\WeldLogFieldsSyncTrigger;
use App\Services\LineListTriggers\Triggers\PaintMatrixOperationsTrigger;
use App\Services\LineListTriggers\Triggers\NdeMatrixSyncTrigger;
use App\Services\LineListTriggers\Triggers\PaintSystemSyncTrigger;
use App\Services\LineListTriggers\Triggers\PaintSystemToMatrixSyncTrigger;
use App\Services\LineListTriggers\Triggers\ConstructionPaintLogsSyncTrigger;
use App\Services\LineListTriggers\Triggers\PaintFollowUpsSyncTrigger;
use App\Services\LineListTriggers\Triggers\SpoolStatusChangerFinalTrigger;
use App\Services\LineListTriggers\Triggers\CleanupOperationsTrigger;
use App\Services\LineListTriggers\Triggers\TestPackageTracingSyncTrigger;
use App\Services\LineListTriggers\Triggers\IsolationSyncTrigger;
/**
* LineList Trigger Registry
*
* Central registry for all LineList triggers
* Manages trigger registration and retrieval
*/
class LineListTriggerRegistry
{
protected $triggers = [];
public function __construct()
{
$this->registerTriggers();
}
/**
* Register all LineList triggers
*
* CRITICAL ORDER:
* 1. PaintCycleChangeTrigger must run first (handles painting_cycle changes)
* 2. WeldLogFieldsSyncTrigger syncs basic fields to weld_logs
* 3. PaintSystemSyncTrigger prepares paint_systems and color_systems data
* 4. PaintMatrixOperationsTrigger creates/updates base paint_matrices records
* 5. PaintSystemToMatrixSyncTrigger enriches paint_matrices with system data
* 6. PaintFollowUpsSyncTrigger creates/updates paint_follow_ups (uses matrices data)
* 7. ConstructionPaintLogsSyncTrigger creates construction paint logs
* 8. NdeMatrixSyncTrigger creates NDE data
* 9. Cleanup triggers run last
*/
protected function registerTriggers()
{
// 1. Handle painting_cycle changes first (updates weld_logs, paint_matrices, paint_follow_ups cycle)
$this->register(new PaintCycleChangeTrigger());
// 2. Sync basic fields to weld_logs
$this->register(new WeldLogFieldsSyncTrigger());
// 3. Prepare paint system data (paint_systems and color_systems tables)
$this->register(new PaintSystemSyncTrigger());
// 4. Ensure base Paint Matrix exists/updated before enrichment
$this->register(new PaintMatrixOperationsTrigger());
// 5. Sync paint systems to paint matrices (BEFORE paint follow ups creation)
// This ensures paint_matrices have complete paint coat data
$this->register(new PaintSystemToMatrixSyncTrigger());
// 6. Create/update paint follow ups (uses paint_matrices data)
$this->register(new PaintFollowUpsSyncTrigger());
// 7. Create construction paint logs
$this->register(new ConstructionPaintLogsSyncTrigger());
// 8. Create NDE Matrix records
$this->register(new NdeMatrixSyncTrigger());
// 9. Sync tracing_type to Test Packages
$this->register(new TestPackageTracingSyncTrigger());
// 10. Sync isolation to Test Packages
$this->register(new IsolationSyncTrigger());
// 11. Final cleanup operations
$this->register(new PaintingCycleDeletionTrigger());
$this->register(new SpoolStatusChangerFinalTrigger());
$this->register(new CleanupOperationsTrigger());
}
/**
* Register a single trigger
*/
public function register($trigger)
{
$this->triggers[$trigger->getName()] = $trigger;
}
/**
* Get all triggers in registration order
* Triggers execute in the order they are registered in registerTriggers()
*/
public function getTriggersInOrder(): array
{
return array_values($this->triggers);
}
/**
* Get a specific trigger by name
*/
public function getTrigger(string $name)
{
return $this->triggers[$name] ?? null;
}
/**
* Get all registered trigger names
*/
public function getAllTriggerNames(): array
{
return array_keys($this->triggers);
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use Illuminate\Support\Facades\Log;
/**
* Cleanup Operations Trigger
*
* Non-critical cleanup operations at the end
* Deletes orphaned paint follow up records
*/
class CleanupOperationsTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Cleanup Operations';
}
public function getDependentFields(): array
{
return ['line_no', 'painting_cycle'];
}
protected function process($lineListData, $beforeData, array $context): array
{
/*
try {
// Clean-up operation - delete no paint followup
view('cron.delete-no-paint-followup', [
'lineNumber' => $lineListData->line_no
])->render();
Log::debug("Cleanup operations completed", [
'line_no' => $lineListData->line_no
]);
Log::debug("Cleanup operations completed", [
'line_no' => $lineListData->line_no,
'before_painting_cycle' => $beforeData->painting_cycle
]);
return [
'success' => true,
'executed' => 'cleanup_view'
];
} catch (\Exception $cleanupException) {
Log::warning("Clean-up işlemi başarısız oldu: " . $cleanupException->getMessage(), [
'line_number' => $lineListData->line_no
]);
// Non-critical, don't throw
return [
'success' => false,
'error' => $cleanupException->getMessage(),
'note' => 'non_critical'
];
}
*/
}
}
@@ -0,0 +1,413 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use App\Models\PaintMatrix;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\Log;
/**
* Construction Paint Logs Sync Trigger
*
* Syncs data from Line Lists + Weld Logs + Paint Systems to Construction Paint Logs:
* - Creates/updates construction_paint_logs for each spool with shop joint
* - Handles painting cycle changes
* - Similar approach to WeldLog trigger for consistency
*/
class ConstructionPaintLogsSyncTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Construction Paint Logs Sync';
}
public function getDependentFields(): array
{
return [
];
}
protected function process($lineListData, $beforeData, array $context): array
{
$lineList = $lineListData;
// Skip processing if essential fields are missing
if (empty($lineList->line_no)) {
Log::debug("Skipping Construction Paint Logs sync - missing line number");
return ['skipped' => true, 'reason' => 'line_no_empty'];
}
// Skip processing if painting_cycle is empty
// Construction Paint Logs should only exist when there is a painting cycle
if (empty($lineList->painting_cycle)) {
Log::debug("Skipping Construction Paint Logs sync - painting_cycle is empty", [
'line_no' => $lineList->line_no
]);
return ['skipped' => true, 'reason' => 'painting_cycle_empty'];
}
// Check if matching weld_logs record exists with same unit + line + fluid_code
// Construction paint logs should only be created if there's a corresponding weld log
$matchingWeldLogExists = db("weld_logs")
->where("line_number", $lineList->line_no)
->where("design_area", $lineList->unit)
->where("fluid_code", $lineList->fluid_code)
->exists();
if (!$matchingWeldLogExists) {
Log::debug("Skipping Construction Paint Logs sync - no matching weld_logs record found", [
'line_no' => $lineList->line_no,
'unit' => $lineList->unit,
'fluid_code' => $lineList->fluid_code
]);
return ['skipped' => true, 'reason' => 'no_matching_weld_log'];
}
// Retrieve all weld logs with the same line number, unit and fluid_code
$allWeldLogs = db("weld_logs")
->where("line_number", $lineList->line_no)
->where("design_area", $lineList->unit)
->where("fluid_code", $lineList->fluid_code)
->orderBy('id', 'ASC') // Deadlock prevention
->get();
Log::debug("Syncing Construction Paint Logs for line: " . $lineList->line_no, [
'weld_log_count' => count($allWeldLogs),
'painting_cycle' => $lineList->painting_cycle
]);
$createdCount = 0;
$updatedCount = 0;
// Track processed spools to avoid duplicates
$processedSpools = [];
// Process weld logs in chunks with TransactionHelper
TransactionHelper::chunkTransaction(
$allWeldLogs,
function ($weldLogChunk) use ($lineList, &$updatedCount, &$createdCount, &$processedSpools) {
foreach ($weldLogChunk as $weldLogData) {
// Skip if spool is empty
if (empty($weldLogData->spool_number)) {
Log::debug("Skipping weld log - empty spool_number", ['weld_log_id' => $weldLogData->id]);
continue;
}
// Skip if already processed this spool
if (in_array($weldLogData->spool_number, $processedSpools)) {
Log::debug("Skipping weld log - spool already processed", [
'weld_log_id' => $weldLogData->id,
'spool' => $weldLogData->spool_number
]);
continue;
}
// Shop joint check - only process spools with shop joints
$hasShopJoint = db("weld_logs")
->where('line_number', $lineList->line_no)
->where('spool_number', $weldLogData->spool_number)
->where('type_of_joint', 'S')
->exists();
if (!$hasShopJoint) {
Log::debug("Skipping spool - no shop joint found", [
'line' => $lineList->line_no,
'spool' => $weldLogData->spool_number
]);
continue;
}
Log::debug("Processing spool for Construction Paint Log", [
'line' => $lineList->line_no,
'spool' => $weldLogData->spool_number,
'weld_log_id' => $weldLogData->id
]);
// Process this spool
$this->processWeldLogRecord($weldLogData, $lineList, $updatedCount, $createdCount);
// Mark this spool as processed
$processedSpools[] = $weldLogData->spool_number;
}
return $weldLogChunk->count();
},
1, // Chunk size
10000 // 10ms delay
);
Log::debug("Construction Paint Logs sync completed", [
'line_no' => $lineList->line_no,
'created' => $createdCount,
'updated' => $updatedCount
]);
return [
'success' => true,
'created' => $createdCount,
'updated' => $updatedCount
];
}
/**
* Process a single weld log record for construction paint logs
* Same structure as WeldLog trigger
*/
protected function processWeldLogRecord($currentWeldLog, $lineList, &$updatedCount, &$createdCount)
{
Log::debug("Starting processWeldLogRecord", [
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number,
'painting_cycle' => $lineList->painting_cycle,
'fluid_code' => $lineList->fluid_code
]);
// Get paint matrix data for paint system information
$paintMatrix = PaintMatrix::where('line', $lineList->line_no)
->where('fluid_code', $lineList->fluid_code)
->first();
Log::debug("Paint Matrix lookup result", [
'found' => $paintMatrix !== null,
'line' => $lineList->line_no,
'fluid_code' => $lineList->fluid_code
]);
// Base data for construction paint log
$baseConstructionPaintLogData = [
// Report Information
'construction_report_no' => $currentWeldLog->weld_map_no ?? '',
'test_package' => $currentWeldLog->test_package_no ?? '',
'rev' => $lineList->rev ?? '',
// Project Information
'engineering' => $lineList->engineering ?? '',
'area' => $currentWeldLog->project ?? '',
'unit' => $currentWeldLog->design_area ?? '',
// Line Details
'line' => $lineList->line_no,
'iso_drawings' => $currentWeldLog->iso_number ?? '',
'fluid_code' => $lineList->fluid_code ?? '',
'fluid_code_description' => $lineList->fluid_ru ?? '',
'isolation_info' => $lineList->external_finish_type ?? '',
];
// Add paint system data if available
if ($paintMatrix) {
$paintSystemData = [
'painting_system_type_1' => $paintMatrix->paint_cycle ?? '',
'painting_system_type_2' => $paintMatrix->paint_cycle ?? '',
// First coat (Primer)
'brend_name_1' => $paintMatrix->brend_name_1 ?? '',
'ral_1' => $paintMatrix->ral_code_1 ?? '',
'color_1' => $paintMatrix->colour_1 ?? '', // Russian color description
'thickness_1' => $paintMatrix->thickness_1 ?? '',
// Second coat (Intermediate)
'brend_name_2' => $paintMatrix->brend_name_2 ?? '',
'ral_2' => $paintMatrix->ral_code_2 ?? '',
'color_2' => $paintMatrix->colour_2 ?? '', // Russian color description
'thickness_2' => $paintMatrix->thickness_2 ?? '',
// Third coat (Final)
'brend_name_3' => $paintMatrix->brend_name_3 ?? '',
'ral_3' => $paintMatrix->ral_code_3 ?? '',
'color_3' => $paintMatrix->colour_3 ?? '', // Russian color description
'thickness_3' => $paintMatrix->thickness_3 ?? '',
];
$baseConstructionPaintLogData = array_merge($baseConstructionPaintLogData, $paintSystemData);
}
// Add timestamp
$baseConstructionPaintLogData['updated_at'] = now();
// Process only the specific spool
$constructionPaintLogData = $baseConstructionPaintLogData;
// Add spool-specific data
$constructionPaintLogData['test_package'] = $currentWeldLog->test_package_no ?? '';
$constructionPaintLogData['spool'] = $currentWeldLog->spool_number ?? '';
$constructionPaintLogData['spool_status'] = $currentWeldLog->spool_status ?? 'Waiting';
// Generate unique report number for the spool
$constructionPaintLogData['report_no'] = '';
// Add dimensions
$constructionPaintLogData['dn_1'] = $currentWeldLog->nps_1 ?? '';
$constructionPaintLogData['dn_2'] = $currentWeldLog->nps_2 ?? '';
$constructionPaintLogData['dn_3'] = '';
// UNIQUE CONSTRAINT: line + spool + painting_system_type_1 (paint cycle)
// This allows multiple records for same spool with different paint cycles
$uniqueConstraintCondition = [
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number,
'painting_system_type_1' => $paintMatrix->paint_cycle ?? ''
];
// WHERE condition without cycle (to find records with different cycles)
$whereCondition = [
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number
];
// Find existing record with SAME painting cycle
$existingRecordSameCycle = db("construction_paint_logs")
->where($uniqueConstraintCondition)
->first();
// Find records with DIFFERENT painting cycle
$recordsWithDifferentPaintCycle = db("construction_paint_logs")
->where($whereCondition)
->where('painting_system_type_1', '!=', $paintMatrix->paint_cycle ?? '')
->get();
Log::debug('LineList ConstructionPaintLogsTrigger: Record check', [
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number,
'currentPaintCycle' => $paintMatrix->paint_cycle ?? '',
'existingSameCycle' => (bool) $existingRecordSameCycle,
'differentCycleCount' => $recordsWithDifferentPaintCycle->count()
]);
try {
if ($existingRecordSameCycle) {
// Record with same paint cycle exists
$hasAllEmptyDates = $this->hasAllEmptyDates($existingRecordSameCycle);
if ($hasAllEmptyDates) {
// All dates are empty - safe to update completely
Log::debug('LineList ConstructionPaintLogsTrigger: Updating record (all dates empty)', [
'recordId' => $existingRecordSameCycle->id,
'paintCycle' => $paintMatrix->paint_cycle ?? ''
]);
db("construction_paint_logs")
->where('id', $existingRecordSameCycle->id)
->update($constructionPaintLogData);
$updatedCount++;
Log::info("✓ Updated Construction Paint Log (same cycle)", [
'record_id' => $existingRecordSameCycle->id,
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number,
'painting_cycle' => $lineList->painting_cycle
]);
} else {
// Some dates are filled - preserve completed work
Log::debug('LineList ConstructionPaintLogsTrigger: Skipping update (dates filled)', [
'recordId' => $existingRecordSameCycle->id,
'paintCycle' => $paintMatrix->paint_cycle ?? ''
]);
}
} else {
// No record with same paint cycle exists
// Handle records with different paint cycles
if ($recordsWithDifferentPaintCycle->count() > 0) {
foreach ($recordsWithDifferentPaintCycle as $oldRecord) {
$hasAnyDateFilled = !$this->hasAllEmptyDates($oldRecord);
if ($hasAnyDateFilled) {
// Dates are filled - keep old record, will create new one
Log::debug('LineList ConstructionPaintLogsTrigger: Old paint cycle record has dates, keeping it', [
'oldRecordId' => $oldRecord->id,
'oldPaintCycle' => $oldRecord->painting_system_type_1,
'newPaintCycle' => $paintMatrix->paint_cycle ?? ''
]);
} else {
// Dates are empty - update to new paint cycle instead of creating new
Log::debug('LineList ConstructionPaintLogsTrigger: Updating old record to new paint cycle', [
'oldRecordId' => $oldRecord->id,
'oldPaintCycle' => $oldRecord->painting_system_type_1,
'newPaintCycle' => $paintMatrix->paint_cycle ?? ''
]);
db("construction_paint_logs")
->where('id', $oldRecord->id)
->update($constructionPaintLogData);
$updatedCount++;
Log::info("✓ Updated Construction Paint Log (cycle changed, no dates)", [
'record_id' => $oldRecord->id,
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number,
'old_cycle' => $oldRecord->painting_system_type_1,
'new_cycle' => $lineList->painting_cycle
]);
return; // Don't create new record
}
}
}
// Create new record (either no record exists, or old one has dates)
Log::debug('LineList ConstructionPaintLogsTrigger: Creating new record', [
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number,
'paintCycle' => $paintMatrix->paint_cycle ?? '',
'reason' => $recordsWithDifferentPaintCycle->count() > 0
? 'different_cycle_with_dates'
: 'no_existing_record'
]);
$constructionPaintLogData['created_at'] = now();
$insertedId = db("construction_paint_logs")->insertGetId($constructionPaintLogData);
$createdCount++;
Log::info("✓ Created Construction Paint Log", [
'record_id' => $insertedId,
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number,
'painting_cycle' => $lineList->painting_cycle,
'reason' => $recordsWithDifferentPaintCycle->count() > 0
? 'different_cycle_with_dates'
: 'no_existing_record'
]);
}
} catch (\Exception $e) {
Log::error("✗ Failed to save Construction Paint Log", [
'error' => $e->getMessage(),
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number,
'file' => $e->getFile(),
'line_number' => $e->getLine()
]);
throw $e;
}
}
/**
* Check if all date fields are empty in a construction paint log record
*
* @param object $record Construction paint log record
* @return bool True if all date fields are empty
*/
protected function hasAllEmptyDates($record): bool
{
$dateFields = [
'blasting_date',
'blasting_finish_date',
'painting_date_1',
'painting_finish_date_1',
'rfi_date_1',
'painting_date_2',
'painting_finish_date_2',
'rfi_date_2',
'painting_date_3',
'painting_finish_date_3',
'rfi_date_3'
];
foreach ($dateFields as $field) {
if (!empty($record->$field)) {
return false; // Found a filled date
}
}
return true; // All dates are empty
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use Illuminate\Support\Facades\Log;
/**
* Isolation Sync Trigger
*
* Syncs external_finish_type from Line List to:
* - Test Packages (isolation field)
* Triggered when external_finish_type field changes in Line List
*/
class IsolationSyncTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Isolation Sync';
}
public function getDependentFields(): array
{
return ['external_finish_type'];
}
protected function process($lineListData, $beforeData, array $context): array
{
// Update 'isolation' field in test_packages table where p_id matches line_no
// 1. Get test_package_no's from test_pack_base_statuses where drawing_no matches line_no
$testPackageNos = db('test_pack_base_statuses')
->where('drawing_no', $lineListData->line_no)
->pluck('test_package_no')
->toArray();
// 2. Update test_packages where test_package_no matches the found ones
if (!empty($testPackageNos)) {
$result = db("test_packages")
->whereIn("test_package_number", $testPackageNos)
->update([
'isolation' => $lineListData->external_finish_type
]);
} else {
$result = 0;
}
Log::info("Line List Isolation Sync completed", [
'line_no' => $lineListData->line_no,
'external_finish_type' => $lineListData->external_finish_type,
'updated' => $result,
]);
return [
'success' => true,
'test_packages_updated' => $result,
];
}
}
@@ -0,0 +1,241 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use App\Models\NdeMatrix;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* NDE Matrix Sync Trigger
*
* Handles NDE Matrix synchronization from WeldLogs and LineList:
* - Collects spec joint types from weld_logs
* - Creates/updates nde_matrices for each fluid_code + line_no + type_of_welds combination
* - Handles PWHT, HT, NDT calculations
*/
class NdeMatrixSyncTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'NDE Matrix Sync';
}
public function getDependentFields(): array
{
return [
];
}
protected function process($lineListData, $beforeData, array $context): array
{
// Get fresh weld logs data
$weldlogs = db("weld_logs")
->where("line_number", $lineListData->line_no)
->get();
// Collect spec joint types from weld logs
$specJointTypes = $this->collectSpecJointTypes($weldlogs, $lineListData);
if(empty($specJointTypes)) {
return [
'skipped' => true,
'reason' => 'no_spec_joint_types_found'
];
}
Log::debug('specJointTypes içeriği:', ['specJointTypes' => $specJointTypes]);
$update = 0;
$create = 0;
// Check if specJointTypes exist for this line list
if(!isset($specJointTypes[$lineListData->fluid_code][$lineListData->line_no])) {
Log::warning("NDE Matrix işlemi atlandı - specJointTypes bulunamadı", [
'line_no' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code
]);
return [
'skipped' => true,
'reason' => 'spec_joint_types_not_found_for_line'
];
}
$typeOfJoint = $specJointTypes[$lineListData->fluid_code][$lineListData->line_no];
// Convert array to collection for chunk processing
$jointCollection = collect($typeOfJoint);
Log::debug("NDE Matrix sync starting with chunk transaction", [
'total_joint_types' => $jointCollection->count(),
'line_no' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code
]);
// Process joints in chunks with TransactionHelper
TransactionHelper::chunkTransaction(
$jointCollection,
function ($jointChunk) use ($lineListData, &$update, &$create) {
foreach($jointChunk as $jointItem) {
$ht = 0;
if($lineListData->pwht == "YES") $ht = 100;
$data = [
'design_area' => $lineListData->unit,
'fluid' => $lineListData->fluid_code,
'line' => $lineListData->line_no,
'line_spec' => $lineListData->line_specification,
'operation_temp' => $lineListData->working_temperature,
'operations_pressure_kg' => $lineListData->working_pressure_mpa,
'pipe_material_class' => $lineListData->pipe_material_class,
'type_of_joint' => $jointItem,
'pwht' => $lineListData->pwht,
'pwht_field' => $lineListData->pwht,
'ht' => $ht,
'ndt' => $lineListData->ndt,
'piping_class_according_to_gost' => $lineListData->category,
'piping_group' => $lineListData->fluid_group,
'rev' => $lineListData->rev,
'tracing' => $lineListData->tracing,
'tracing_type' => $lineListData->tracing_type,
'naks_technology' => $lineListData->naks_technology,
];
// UNIQUE CONSTRAINT: line + type_of_joint + fluid
$uniqueKey = [
'line' => $lineListData->line_no,
'type_of_joint' => $jointItem,
'fluid' => $lineListData->fluid_code,
];
// Check if record already exists with deadlock prevention
$already = NdeMatrix::where($uniqueKey)
->orderBy('id', 'ASC') // Deadlock prevention
->first();
if($already) {
unset($data['pwht_field']);
try {
$updateResult = NdeMatrix::where($uniqueKey)
->orderBy('id', 'ASC') // Deadlock prevention
->update($data);
Log::debug("NDE Matrix record updated successfully", [
'affected_rows' => $updateResult,
'line_no' => $lineListData->line_no,
'joint_type' => $jointItem,
'fluid_code' => $lineListData->fluid_code
]);
$update++;
} catch (\Exception $e) {
Log::error("NDE Matrix update error", [
'line_no' => $lineListData->line_no,
'joint_type' => $jointItem,
'fluid_code' => $lineListData->fluid_code,
'error_message' => $e->getMessage()
]);
throw $e;
}
} else {
try {
$ndeRecord = NdeMatrix::updateOrCreate($uniqueKey, $data);
$action = $ndeRecord->wasRecentlyCreated ? 'created' : 'updated';
Log::debug("NDE Matrix record processed successfully", [
'record_id' => $ndeRecord->id,
'action' => $action,
'line_no' => $lineListData->line_no,
'joint_type' => $jointItem,
'fluid_code' => $lineListData->fluid_code
]);
if($action === 'created') {
$create++;
} else {
$update++;
}
} catch (\Exception $e) {
Log::error("NDE Matrix create/update error", [
'line_no' => $lineListData->line_no,
'joint_type' => $jointItem,
'fluid_code' => $lineListData->fluid_code,
'error_message' => $e->getMessage()
]);
throw $e;
}
}
}
return $jointChunk->count();
},
1, // Chunk size
10000 // 10ms delay - sufficient for deadlock prevention
);
Log::info("NDE Matrix sync completed", [
'updated' => $update,
'created' => $create,
'total_affected' => $update + $create
]);
return [
'success' => true,
'created' => $create,
'updated' => $update
];
}
/**
* Collect spec joint types from weld logs
*/
protected function collectSpecJointTypes($weldlogs, $lineListData): array
{
$specJointTypes = [];
foreach($weldlogs AS $weldlog) {
// Update fluid_code if empty
if(empty($weldlog->fluid_code) && !empty($lineListData->fluid_code)) {
db("weld_logs")
->where("id", $weldlog->id)
->update([
'fluid_code' => $lineListData->fluid_code,
'service_category' => $lineListData->category,
'fluid_group' => $lineListData->fluid_group,
]);
$weldlog->fluid_code = $lineListData->fluid_code;
$weldlog->service_category = $lineListData->category;
$weldlog->fluid_group = $lineListData->fluid_group;
}
if(empty($weldlog->fluid_code)) {
continue;
}
if(empty($weldlog->line_number)) {
continue;
}
if(empty($weldlog->type_of_welds)) {
continue;
}
if(!isset($specJointTypes[$weldlog->fluid_code][$weldlog->line_number])) {
$specJointTypes[$weldlog->fluid_code][$weldlog->line_number] = [];
}
if(!in_array($weldlog->type_of_welds, $specJointTypes[$weldlog->fluid_code][$weldlog->line_number])) {
$specJointTypes[$weldlog->fluid_code][$weldlog->line_number][] = $weldlog->type_of_welds;
}
}
return $specJointTypes;
}
}
@@ -0,0 +1,390 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use App\Models\PaintMatrix;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Paint Cycle Change Trigger
*
* Handles comprehensive synchronizations when painting_cycle field changes:
* - WeldLogs painting_cycle field update
* - Paint Matrices paint_cycle field update
* - Paint Follow Ups cycle field update for ALL records
* - Construction Paint Logs synchronization/deletion
*
* CRITICAL: This trigger updates cycle fields BEFORE other triggers run,
* ensuring PaintSystemToMatrixSyncTrigger and PaintFollowUpsSyncTrigger
* can find and update ALL records with the new painting_cycle value.
*/
class PaintCycleChangeTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Paint Cycle Change';
}
public function getDependentFields(): array
{
return [
'painting_cycle',
'line_no',
'fluid_code',
'unit'
];
}
protected function process($lineListData, $beforeData, array $context): array
{
// Only run if painting_cycle actually changed
if (!isset($beforeData) || $beforeData->painting_cycle == $lineListData->painting_cycle) {
return ['skipped' => true, 'reason' => 'painting_cycle_not_changed'];
}
//değişiklik yapıldığında draft olan cycle'ın tüm recordlarını sil
$paintFollowUps = db('paint_follow_ups')
->where('line', $lineListData->line_no)
->where('cycle', $beforeData->painting_cycle)
->whereNull('primer_coating_start_date')
->whereNull('primer_coating_finish_date')
->whereNull('start_intermediate_date2')
->whereNull('finish_intermediate_date2')
->whereNull('final_coat_start_date3')
->whereNull('final_coat_finish_date3')
->delete();
Log::debug("Paint follow ups deleted data", [
'paint_follow_ups' => $paintFollowUps
]);
$constructionPaintLogs = db('construction_paint_logs')
->where('line', $lineListData->line_no)
->where("painting_system_type_1", $beforeData->painting_cycle)
->whereNull('blasting_date')
->whereNull('blasting_finish_date')
->whereNull('painting_date_1')
->whereNull('painting_finish_date_1')
->whereNull('painting_date_2')
->whereNull('painting_finish_date_2')
->whereNull('painting_date_3')
->whereNull('painting_finish_date_3')
->delete();
Log::debug("Construction paint logs deleted data", [
'construction_paint_logs' => $constructionPaintLogs
]);
Log::debug("Paint cycle changed - Starting processing", [
'before_painting_cycle' => $beforeData->painting_cycle ?? 'NULL',
'current_painting_cycle' => $lineListData->painting_cycle ?? 'NULL',
'line_no' => $lineListData->line_no
]);
$results = [
'weld_logs_updated' => 0,
'paint_matrices_updated' => 0,
'paint_follow_ups_updated' => 0,
'paint_follow_ups_held' => 0,
'construction_paint_logs_deleted' => 0
];
// Check if painting_cycle became empty
if (empty($lineListData->painting_cycle)) {
// Painting cycle was removed - delete Construction Paint Logs
Log::debug("Painting cycle became empty - Deleting Construction Paint Logs", [
'line_no' => $lineListData->line_no
]);
$results['construction_paint_logs_deleted'] = $this->deleteConstructionPaintLogs($lineListData);
} else {
// Painting cycle has value - update WeldLogs, Paint Matrices AND Paint Follow Ups
// This is critical: these updates must happen BEFORE PaintSystemToMatrixSyncTrigger runs
// so that subsequent triggers can find records with the new painting_cycle value
$results['weld_logs_updated'] = $this->updateWeldLogsPaintingCycle($lineListData);
$results['paint_matrices_updated'] = $this->updatePaintMatricesPaintingCycle($lineListData, $beforeData);
// Update ALL paint_follow_ups records for this line to new painting_cycle
// This ensures PaintFollowUpsSyncTrigger will update ALL records, not just ones from weld_logs
$pfuResults = $this->updatePaintFollowUpsCycle($lineListData, $beforeData);
$results['paint_follow_ups_updated'] = $pfuResults['updated'];
$results['paint_follow_ups_held'] = $pfuResults['held'];
}
Log::info("Paint cycle change processing completed", [
'line_no' => $lineListData->line_no,
'painting_cycle' => $lineListData->painting_cycle,
'results' => $results
]);
return $results;
}
/**
* Update WeldLogs painting_cycle field
*/
protected function updateWeldLogsPaintingCycle($lineListData): int
{
try {
$weldLogsUpdateCount = db("weld_logs")
->where("line_number", $lineListData->line_no)
->update(['painting_cycle' => $lineListData->painting_cycle]);
Log::debug("WeldLogs painting_cycle updated", [
'line_no' => $lineListData->line_no,
'updated_count' => $weldLogsUpdateCount,
'new_painting_cycle' => $lineListData->painting_cycle
]);
return $weldLogsUpdateCount;
} catch (\Throwable $th) {
Log::error("WeldLogs painting_cycle update error: " . $th->getMessage(), [
'line_no' => $lineListData->line_no
]);
return 0;
}
}
/**
* Delete Construction Paint Logs when painting_cycle becomes empty
*/
protected function deleteConstructionPaintLogs($lineListData): int
{
try {
// Get all spools for this line from weld_logs
$spools = db("weld_logs")
->where("line_number", $lineListData->line_no)
->whereNotNull('spool_number')
->where('spool_number', '!=', '')
->distinct()
->pluck('spool_number');
if ($spools->isEmpty()) {
Log::debug("No spools found for line", ['line_no' => $lineListData->line_no]);
return 0;
}
// Delete Construction Paint Logs for these spools (only if painting not started)
$deletedCount = db('construction_paint_logs')
->where('line', $lineListData->line_no)
->whereIn('spool', $spools)
->whereNull('blasting_date')
->whereNull('blasting_finish_date')
->whereNull('painting_date_1')
->whereNull('painting_finish_date_1')
->whereNull('painting_date_2')
->whereNull('painting_finish_date_2')
->whereNull('painting_date_3')
->whereNull('painting_finish_date_3')
->delete();
Log::info("Construction Paint Logs deleted due to empty painting_cycle", [
'line_no' => $lineListData->line_no,
'deleted_count' => $deletedCount,
'spool_count' => $spools->count()
]);
return $deletedCount;
} catch (\Throwable $th) {
Log::error("Construction Paint Logs deletion error: " . $th->getMessage(), [
'line_no' => $lineListData->line_no
]);
return 0;
}
}
/**
* Update Paint Matrices painting_cycle field
*
* CRITICAL: This must run BEFORE PaintSystemToMatrixSyncTrigger and PaintFollowUpsSyncTrigger
* so they can find the correct paint_matrices records with the new painting_cycle value
*/
protected function updatePaintMatricesPaintingCycle($lineListData, $beforeData): int
{
try {
// Build query to find paint_matrices records to update
$query = db("paint_matrices")
->where("line", $lineListData->line_no)
->where("fluid_code", $lineListData->fluid_code)
->where("area", $lineListData->unit);
// If we know the old painting_cycle, use it for more precise targeting
if (isset($beforeData) && !empty($beforeData->painting_cycle)) {
$query->where("paint_cycle", $beforeData->painting_cycle);
Log::debug("Updating paint_matrices from old to new painting_cycle", [
'line_no' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code,
'area' => $lineListData->unit,
'old_painting_cycle' => $beforeData->painting_cycle,
'new_painting_cycle' => $lineListData->painting_cycle
]);
} else {
Log::debug("Updating paint_matrices painting_cycle (no old value)", [
'line_no' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code,
'area' => $lineListData->unit,
'new_painting_cycle' => $lineListData->painting_cycle
]);
}
$paintMatricesUpdateCount = $query->update([
'paint_cycle' => $lineListData->painting_cycle,
'updated_at' => now()
]);
Log::info("Paint Matrices painting_cycle updated", [
'line_no' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code,
'area' => $lineListData->unit,
'updated_count' => $paintMatricesUpdateCount,
'new_painting_cycle' => $lineListData->painting_cycle
]);
return $paintMatricesUpdateCount;
} catch (\Throwable $th) {
Log::error("Paint Matrices painting_cycle update error: " . $th->getMessage(), [
'line_no' => $lineListData->line_no,
'file' => $th->getFile(),
'line' => $th->getLine(),
'trace' => $th->getTraceAsString()
]);
return 0;
}
}
/**
* Update Paint Follow Ups cycle field for ALL records
*
* CRITICAL: This ensures ALL paint_follow_ups records (not just ones from weld_logs)
* get their cycle updated when painting_cycle changes
*
* Strategy:
* - Records with empty date fields: Update cycle to new value (will be updated by PaintFollowUpsSyncTrigger)
* - Records with filled date fields: Mark as HOLD (painting already started, should not change)
*/
protected function updatePaintFollowUpsCycle($lineListData, $beforeData): array
{
$updatedCount = 0;
$heldCount = 0;
try {
// Build query to find ALL paint_follow_ups records for this line
$query = db("paint_follow_ups")
->where("line", $lineListData->line_no)
->where("fluid_code", $lineListData->fluid_code)
->where("area", $lineListData->unit);
// If we know the old painting_cycle, target only those records
if (isset($beforeData) && !empty($beforeData->painting_cycle)) {
$query->where("cycle", $beforeData->painting_cycle);
Log::debug("Updating paint_follow_ups from old to new cycle", [
'line_no' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code,
'area' => $lineListData->unit,
'old_cycle' => $beforeData->painting_cycle,
'new_cycle' => $lineListData->painting_cycle
]);
} else {
// If no old cycle, update records that don't match new cycle
$query->where("cycle", "!=", $lineListData->painting_cycle);
Log::debug("Updating paint_follow_ups to new cycle (no old value)", [
'line_no' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code,
'area' => $lineListData->unit,
'new_cycle' => $lineListData->painting_cycle
]);
}
$paintFollowUps = $query->get();
Log::info("Found {$paintFollowUps->count()} paint_follow_ups records to process", [
'line_no' => $lineListData->line_no
]);
foreach ($paintFollowUps as $pfu) {
$hasAllEmptyDates = $this->hasAllEmptyDates($pfu);
if ($hasAllEmptyDates) {
// No painting started - safe to update cycle
db("paint_follow_ups")
->where('id', $pfu->id)
->update([
'cycle' => $lineListData->painting_cycle,
'updated_at' => now()
]);
$updatedCount++;
Log::debug("Updated paint_follow_ups cycle (dates empty)", [
'id' => $pfu->id,
'spool_no_joint_no' => $pfu->spool_no_joint_no,
'location' => $pfu->location
]);
} else {
// Painting already started - mark as HOLD
db("paint_follow_ups")
->where('id', $pfu->id)
->update([
'status' => 'HOLD',
'updated_at' => now()
]);
$heldCount++;
Log::debug("Marked paint_follow_ups as HOLD (dates filled)", [
'id' => $pfu->id,
'spool_no_joint_no' => $pfu->spool_no_joint_no,
'location' => $pfu->location,
'old_cycle' => $pfu->cycle
]);
}
}
Log::info("Paint Follow Ups cycle update completed", [
'line_no' => $lineListData->line_no,
'total_records' => $paintFollowUps->count(),
'updated_count' => $updatedCount,
'held_count' => $heldCount,
'new_cycle' => $lineListData->painting_cycle
]);
return [
'updated' => $updatedCount,
'held' => $heldCount
];
} catch (\Throwable $th) {
Log::error("Paint Follow Ups cycle update error: " . $th->getMessage(), [
'line_no' => $lineListData->line_no,
'file' => $th->getFile(),
'line' => $th->getLine(),
'trace' => $th->getTraceAsString()
]);
return [
'updated' => $updatedCount,
'held' => $heldCount
];
}
}
/**
* Check if all date fields are empty in a paint_follow_ups record
*/
protected function hasAllEmptyDates($record): bool
{
return empty($record->primer_coating_start_date) &&
empty($record->primer_coating_finish_date) &&
empty($record->start_intermediate_date2) &&
empty($record->finish_intermediate_date2) &&
empty($record->final_coat_start_date3) &&
empty($record->final_coat_finish_date3);
}
}
@@ -0,0 +1,618 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use App\Models\PaintMatrix;
use App\Helpers\TransactionHelper;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Paint Follow Ups Sync Trigger
*
* Most complex trigger - handles comprehensive Paint Follow Ups synchronization:
* - Creates SHOP and FIELD paint follow up records from weld logs
* - Handles painting cycle changes with date field protection
* - Calculates volumes and temperatures
* - Protects records with filled date fields from updates
* - Manages HOLD status for old painting cycles
*/
class PaintFollowUpsSyncTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Paint Follow Ups Sync';
}
public function getDependentFields(): array
{
return [
];
}
protected function process($lineListData, $beforeData, array $context): array
{
$lineList = $lineListData;
// Skip processing if essential fields are missing
if (empty($lineList->line_no)) {
Log::debug("Paint Follow Ups sync skipped - line_number missing");
return ['skipped' => true, 'reason' => 'line_no_empty'];
}
// Skip processing if painting_cycle is empty
// Paint Follow Ups should only exist when there is a painting cycle
if (empty($lineList->painting_cycle)) {
Log::debug("Paint Follow Ups sync skipped - painting_cycle is empty", [
'line_no' => $lineList->line_no
]);
return ['skipped' => true, 'reason' => 'painting_cycle_empty'];
}
// Check if matching weld_logs record exists with same unit + line + fluid_code
// Paint records should only be created if there's a corresponding weld log
$matchingWeldLogExists = db("weld_logs")
->where("line_number", $lineList->line_no)
->where("design_area", $lineList->unit)
->where("fluid_code", $lineList->fluid_code)
->exists();
if (!$matchingWeldLogExists) {
Log::debug("Paint Follow Ups sync skipped - no matching weld_logs record found", [
'line_no' => $lineList->line_no,
'unit' => $lineList->unit,
'fluid_code' => $lineList->fluid_code
]);
return ['skipped' => true, 'reason' => 'no_matching_weld_log'];
}
// Get all weld logs with this line number, unit and fluid_code
$resultFields = [
'vt_result',
'rt_result',
'ut_result',
'pt_result',
'mt_result',
'pmi_result',
'ht_result',
'ferrite_result'
];
$allWeldLogs = db("weld_logs")
->where("line_number", $lineList->line_no)
->where("design_area", $lineList->unit)
->where("fluid_code", $lineList->fluid_code)
->where(function ($query) {
$query->where("no_of_the_joint_as_per_as_built_survey", "not like", "%clone%");
})
->where(function ($query) use ($resultFields) {
foreach ($resultFields as $field) {
$query->where(function ($subQuery) use ($field) {
$subQuery->whereNull($field)
->orWhere($field, '')
->orWhere($field, 'Accept / Годен');
});
}
})
->orderBy('id', 'ASC') // Deadlock prevention
->get();
Log::debug("Syncing " . count($allWeldLogs) . " weld logs for line: " . $lineList->line_no);
// Get or create paint matrix for this line
// CRITICAL: Must include paint_cycle to get the correct matrix after painting_cycle changes
$paintMatrix = PaintMatrix::where('line', $lineList->line_no)
->where('fluid_code', $lineList->fluid_code)
->where('area', $lineList->unit)
->where('paint_cycle', $lineList->painting_cycle)
->first();
Log::debug("Paint Matrix data: " . json_encode($paintMatrix));
if (!$paintMatrix) {
$paintMatrixData = [
'project' => $allWeldLogs->first()->project ?? '',
'area' => $lineList->unit,
'description' => "PIPE",
'line' => $lineList->line_no,
'fluid_code_description' => $lineList->fluid_ru,
'fluid_code' => $lineList->fluid_code,
'design_temperature' => $lineList->design_temperature,
'operation_temperature' => $lineList->working_temperature,
'paint_cycle' => $lineList->painting_cycle,
'created_at' => now(),
'updated_at' => now()
];
$paintMatrix = PaintMatrix::create($paintMatrixData);
Log::debug("Created new Paint Matrix for line: {$lineList->line_no}");
}
// Load temperature settings
$temperatures = j(setting("temperatures"));
$todayTempData = null;
// Get blasting_date from construction_paint_logs
$blastingDateRecord = db('construction_paint_logs')
->where('line', $lineList->line_no)
->whereNotNull('blasting_date')
->first();
if (!is_null($temperatures) && $blastingDateRecord && !empty($blastingDateRecord->blasting_date)) {
$blastingDate = Carbon::parse($blastingDateRecord->blasting_date);
$dayOfYear = $blastingDate->format('z');
$todayTempData = $temperatures[$dayOfYear] ?? null;
Log::debug('Temperature data loaded', [
'blasting_date' => $blastingDateRecord->blasting_date,
'day_of_year' => $dayOfYear
]);
}
// Calculate volumes
$fAvgNps = $allWeldLogs->where("type_of_joint", "F")->avg("nps_1");
$sAvgNps = $allWeldLogs->where("type_of_joint", "S")->avg("nps_1");
$mtoTotal = db("m_t_o_s")
->where("description_en", "like", "%pipe%")
->where("line", $lineList->line_no)
->selectRaw("SUM(quantity * POWER(odmm_1/2000, 2) * PI()) AS total")
->first()->total ?? 0;
$fVolume = round(pi() * pow($fAvgNps, 2) * 200, 2);
$sVolume = round($mtoTotal, 2);
Log::debug('Volume calculations:', [
'fVolume' => $fVolume,
'sVolume' => $sVolume
]);
$paintFollowUpCreated = 0;
$paintFollowUpUpdated = 0;
$processedPaintFollowUpIds = [];
// Process weld logs in chunks with TransactionHelper
TransactionHelper::chunkTransaction(
$allWeldLogs,
function ($weldLogChunk) use (
$lineList,
$paintMatrix,
$todayTempData,
$sVolume,
$fVolume,
&$paintFollowUpCreated,
&$paintFollowUpUpdated,
&$processedPaintFollowUpIds
) {
foreach ($weldLogChunk as $weldLog) {
// Skip if fluid code is empty
if (empty($weldLog->fluid_code)) {
continue;
}
// Base data for paint follow up
$basePaintFollowUpData = [
'project' => $weldLog->project ?? '',
'description' => "PIPE",
'area' => $lineList->unit ?? '',
'line' => $lineList->line_no,
'iso_number' => $weldLog->iso_number ?? '',
'fluid_code' => $lineList->fluid_code,
'fluid_code_description' => $lineList->fluid_ru ?? '',
'cycle' => $lineList->painting_cycle ?? '',
'primer_coat_name_1' => $paintMatrix->primer_coat ?? '',
'brend_name_1' => $paintMatrix->brend_name_1 ?? '',
'colour_1' => $paintMatrix->colour_1 ?? '',
'ral_code_1' => $paintMatrix->ral_code_1 ?? '',
'thickness_1' => $paintMatrix->thickness_1 ?? '',
'intermediate_coat_name_2' => $paintMatrix->intermediate_coat ?? '',
'brend_name_2' => $paintMatrix->brend_name_2 ?? '',
'colour_2' => $paintMatrix->colour_2 ?? '',
'ral_code_2' => $paintMatrix->ral_code_2 ?? '',
'thickness_2' => $paintMatrix->thickness_2 ?? '',
'final_coat_name_3' => $paintMatrix->final_coat ?? '',
'brend_name_3' => $paintMatrix->brend_name_3 ?? '',
'colour_3' => $paintMatrix->colour_3 ?? '',
'ral_code_3' => $paintMatrix->ral_code_3 ?? '',
'thickness_3' => $paintMatrix->thickness_3 ?? '',
'surface_roughness' => $paintMatrix->surface_preparation ?? '',
'status' => 'In Progress',
'updated_at' => now()
];
// Process SHOP entry (using spool_number)
$result = $this->processShopRecord(
$lineList,
$weldLog,
$basePaintFollowUpData,
$paintMatrix,
$todayTempData,
$sVolume,
$paintFollowUpCreated,
$paintFollowUpUpdated
);
$paintFollowUpCreated = $result['created'];
$paintFollowUpUpdated = $result['updated'];
// Process FIELD entry (using joint number)
$result = $this->processFieldRecord(
$lineList,
$weldLog,
$basePaintFollowUpData,
$paintMatrix,
$todayTempData,
$fVolume,
$paintFollowUpCreated,
$paintFollowUpUpdated,
$processedPaintFollowUpIds
);
$paintFollowUpCreated = $result['created'];
$paintFollowUpUpdated = $result['updated'];
$processedPaintFollowUpIds = $result['processed_ids'];
}
return $weldLogChunk->count();
},
1, // Chunk size
10000 // 10ms delay
);
Log::debug("Paint Follow Ups sync completed: $paintFollowUpCreated created, $paintFollowUpUpdated updated");
return [
'success' => true,
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated
];
}
/**
* Process SHOP paint follow up record
*/
protected function processShopRecord(
$lineList,
$weldLog,
$basePaintFollowUpData,
$paintMatrix,
$todayTempData,
$sVolume,
$paintFollowUpCreated,
$paintFollowUpUpdated
): array {
// Check if line has shop joints
$hasShopJoint = db("weld_logs")
->where('line_number', $lineList->line_no)
->where('spool_number', $weldLog->spool_number)
->where('type_of_joint', 'S')
->exists();
if (!$hasShopJoint || empty($weldLog->spool_number)) {
return ['created' => $paintFollowUpCreated, 'updated' => $paintFollowUpUpdated];
}
// Prepare shop data
$shopData = $basePaintFollowUpData;
$shopData['location'] = 'SHOP';
$shopData['spool_no_joint_no'] = $weldLog->spool_number;
$shopData['surface_roughness'] = $paintMatrix->surface_preparation ?? '';
// Add temperature and volume data
if ($todayTempData) {
$shopData['substrate_temprature'] = $todayTempData['temp_material_shop'];
$shopData['ambient_temprature'] = $todayTempData['shop_ambient'];
}
$shopData['volume_1'] = $sVolume;
$shopData['volume_2'] = $sVolume;
$shopData['volume_3'] = $sVolume;
$shopData['total_volume'] = $sVolume * 3;
// Unique constraint for SHOP records
$shopUniqueConstraintCondition = [
'line' => $lineList->line_no,
'spool_no_joint_no' => $weldLog->spool_number,
'cycle' => $lineList->painting_cycle,
'location' => 'SHOP'
];
$shopWhereCondition = [
'line' => $lineList->line_no,
'spool_no_joint_no' => $weldLog->spool_number,
'location' => 'SHOP'
];
// Find existing record
$existingShopRecord = db("paint_follow_ups")
->where($shopUniqueConstraintCondition)
->first();
// Check for records with different painting cycle
$recordsWithDifferentPaintCycle = db("paint_follow_ups")
->where($shopWhereCondition)
->where('cycle', '!=', $lineList->painting_cycle)
->get();
if ($existingShopRecord) {
// Update existing record with same painting cycle
$hasAllEmptyDates = $this->hasAllEmptyDates($existingShopRecord);
if ($hasAllEmptyDates) {
db("paint_follow_ups")
->where('id', $existingShopRecord->id)
->update($shopData);
$paintFollowUpUpdated++;
Log::debug("Updated SHOP paint follow up {$existingShopRecord->id} - all dates were empty");
} else {
// Update only temperature/volume if empty
$tempVolumeData = $this->getTempVolumeUpdateData(
$existingShopRecord,
$todayTempData,
$sVolume,
'SHOP'
);
if (!empty($tempVolumeData)) {
db("paint_follow_ups")
->where('id', $existingShopRecord->id)
->update($tempVolumeData);
Log::debug("Updated temp/volume for SHOP record {$existingShopRecord->id}");
}
}
} else {
// Handle records with different painting cycles
if ($recordsWithDifferentPaintCycle->count() > 0) {
foreach ($recordsWithDifferentPaintCycle as $oldRecord) {
$hasAnyDateFilled = !$this->hasAllEmptyDates($oldRecord);
if ($hasAnyDateFilled) {
// Mark as HOLD
db("paint_follow_ups")
->where('id', $oldRecord->id)
->update([
'status' => 'HOLD',
'updated_at' => now()
]);
Log::debug("Marked old SHOP record as HOLD");
} else {
// Update to new painting cycle
db("paint_follow_ups")
->where('id', $oldRecord->id)
->update([
'cycle' => $lineList->painting_cycle,
'updated_at' => now()
]);
$paintFollowUpUpdated++;
Log::debug("Updated old SHOP record to new painting cycle");
return ['created' => $paintFollowUpCreated, 'updated' => $paintFollowUpUpdated];
}
}
}
// Create new record
$newShopData = $shopData;
$newShopData['primer_coating_start_date'] = null;
$newShopData['primer_coating_finish_date'] = null;
$newShopData['start_intermediate_date2'] = null;
$newShopData['finish_intermediate_date2'] = null;
$newShopData['final_coat_start_date3'] = null;
$newShopData['final_coat_finish_date3'] = null;
$newShopData['created_at'] = now();
$newShopData['updated_at'] = now();
db("paint_follow_ups")->insert($newShopData);
$paintFollowUpCreated++;
Log::debug("Created new SHOP Paint Follow Up");
}
return ['created' => $paintFollowUpCreated, 'updated' => $paintFollowUpUpdated];
}
/**
* Process FIELD paint follow up record
*/
protected function processFieldRecord(
$lineList,
$weldLog,
$basePaintFollowUpData,
$paintMatrix,
$todayTempData,
$fVolume,
$paintFollowUpCreated,
$paintFollowUpUpdated,
$processedPaintFollowUpIds
): array {
if (empty($weldLog->no_of_the_joint_as_per_as_built_survey)) {
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_ids' => $processedPaintFollowUpIds
];
}
$jointNo = $weldLog->no_of_the_joint_as_per_as_built_survey;
$fieldData = $basePaintFollowUpData;
$fieldData['location'] = 'FIELD';
$fieldData['spool_no_joint_no'] = $jointNo;
$fieldData['surface_roughness'] = $paintMatrix->touch_up_of_damaged_parts ?? '';
// Add temperature and volume data
if ($todayTempData) {
$fieldData['substrate_temprature'] = $todayTempData['temp_material_field'];
$fieldData['ambient_temprature'] = $todayTempData['field_ambient'];
}
$fieldData['volume_1'] = $fVolume;
$fieldData['volume_2'] = $fVolume;
$fieldData['volume_3'] = $fVolume;
$fieldData['total_volume'] = $fVolume * 3;
// Unique constraint for FIELD records
$fieldUniqueConstraintCondition = [
'line' => $lineList->line_no,
'spool_no_joint_no' => $jointNo,
'cycle' => $lineList->painting_cycle,
'location' => 'FIELD'
];
$fieldWhereCondition = [
'line' => $lineList->line_no,
'spool_no_joint_no' => $jointNo,
'location' => 'FIELD'
];
// Find existing record
$existingFieldRecord = db("paint_follow_ups")
->where($fieldUniqueConstraintCondition)
->first();
// Check for records with different painting cycle
$recordsWithDifferentPaintCycle = db("paint_follow_ups")
->where($fieldWhereCondition)
->where('cycle', '!=', $lineList->painting_cycle)
->get();
if ($existingFieldRecord) {
// Skip if already processed
if (in_array($existingFieldRecord->id, $processedPaintFollowUpIds)) {
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_ids' => $processedPaintFollowUpIds
];
}
$processedPaintFollowUpIds[] = $existingFieldRecord->id;
$hasAllEmptyDates = $this->hasAllEmptyDates($existingFieldRecord);
if ($hasAllEmptyDates) {
db("paint_follow_ups")
->where('id', $existingFieldRecord->id)
->update($fieldData);
$paintFollowUpUpdated++;
Log::debug("Updated FIELD paint follow up {$existingFieldRecord->id} - all dates were empty");
} else {
// Update only temperature/volume if empty
$tempVolumeData = $this->getTempVolumeUpdateData(
$existingFieldRecord,
$todayTempData,
$fVolume,
'FIELD'
);
if (!empty($tempVolumeData)) {
db("paint_follow_ups")
->where('id', $existingFieldRecord->id)
->update($tempVolumeData);
Log::debug("Updated temp/volume for FIELD record {$existingFieldRecord->id}");
}
}
} else {
// Handle records with different painting cycles
if ($recordsWithDifferentPaintCycle->count() > 0) {
foreach ($recordsWithDifferentPaintCycle as $oldRecord) {
$hasAnyDateFilled = !$this->hasAllEmptyDates($oldRecord);
if ($hasAnyDateFilled) {
// Mark as HOLD
db("paint_follow_ups")
->where('id', $oldRecord->id)
->update([
'status' => 'HOLD',
'updated_at' => now()
]);
Log::debug("Marked old FIELD record as HOLD");
} else {
// Update to new painting cycle
db("paint_follow_ups")
->where('id', $oldRecord->id)
->update([
'cycle' => $lineList->painting_cycle,
'updated_at' => now()
]);
$paintFollowUpUpdated++;
Log::debug("Updated old FIELD record to new painting cycle");
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_ids' => $processedPaintFollowUpIds
];
}
}
}
// Create new record
$newFieldData = $fieldData;
$newFieldData['primer_coating_start_date'] = null;
$newFieldData['primer_coating_finish_date'] = null;
$newFieldData['start_intermediate_date2'] = null;
$newFieldData['finish_intermediate_date2'] = null;
$newFieldData['final_coat_start_date3'] = null;
$newFieldData['final_coat_finish_date3'] = null;
$newFieldData['created_at'] = now();
$newFieldData['updated_at'] = now();
db("paint_follow_ups")->insert($newFieldData);
$paintFollowUpCreated++;
Log::debug("Created new FIELD Paint Follow Up");
}
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_ids' => $processedPaintFollowUpIds
];
}
/**
* Check if all date fields are empty
*/
protected function hasAllEmptyDates($record): bool
{
return empty($record->primer_coating_start_date) &&
empty($record->primer_coating_finish_date) &&
empty($record->start_intermediate_date2) &&
empty($record->finish_intermediate_date2) &&
empty($record->final_coat_start_date3) &&
empty($record->final_coat_finish_date3);
}
/**
* Get temperature and volume update data for records with filled dates
*/
protected function getTempVolumeUpdateData($record, $todayTempData, $volume, $location): array
{
$updateData = [];
// Add temperature data if available and field is empty
if ($todayTempData) {
$tempField = $location === 'SHOP' ? 'temp_material_shop' : 'temp_material_field';
$ambientField = $location === 'SHOP' ? 'shop_ambient' : 'field_ambient';
if (empty($record->substrate_temprature)) {
$updateData['substrate_temprature'] = $todayTempData[$tempField];
}
if (empty($record->ambient_temprature)) {
$updateData['ambient_temprature'] = $todayTempData[$ambientField];
}
}
// Add volume data if fields are empty
if (empty($record->volume_1)) {
$updateData['volume_1'] = $volume;
}
if (empty($record->volume_2)) {
$updateData['volume_2'] = $volume;
}
if (empty($record->volume_3)) {
$updateData['volume_3'] = $volume;
}
if (empty($record->total_volume)) {
$updateData['total_volume'] = $volume * 3;
}
return $updateData;
}
}
@@ -0,0 +1,257 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use App\Models\PaintMatrix;
use App\Models\PaintFollowUp;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\Log;
/**
* Paint Matrix Operations Trigger
*
* Handles Paint Matrix create/update operations and syncs to Paint Follow Ups:
* - Creates or updates paint_matrices records
* - Syncs surface roughness to paint_follow_ups
* - Syncs paint coat details (3 coats) to paint_follow_ups
*/
class PaintMatrixOperationsTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Paint Matrix Operations';
}
public function getDependentFields(): array
{
return [
];
}
protected function process($lineListData, $beforeData, array $context): array
{
// Skip processing if painting_cycle is empty
// Paint Matrix operations should only run when there is a painting cycle
if (empty($lineListData->painting_cycle)) {
Log::debug("Paint Matrix Operations skipped - painting_cycle is empty", [
'line_no' => $lineListData->line_no
]);
return ['skipped' => true, 'reason' => 'painting_cycle_empty'];
}
$paintFollowUpCount = 0;
$paintMatrix2PaintFollowUpCount = 0;
// Get zone (project) information from weld logs
$weldlogs = db("weld_logs")
->where("line_number", $lineListData->line_no)
->where('design_area', $lineListData->unit)
->get();
$zones = [];
foreach($weldlogs AS $weldLog) {
$zones[$weldLog->line_number] = $weldLog->project;
}
// Fetch paint system data for enrichment
$paintSystem = db('paint_systems')
->where('paint_cycle', $lineListData->painting_cycle)
->first();
$colorSystem = db('color_systems')
->where('fluid_code', $lineListData->fluid_code)
->first();
$ralCodes = j(setting("ral-codes"));
$ral1 = $colorSystem->ral_1 ?? null;
$ral2 = $colorSystem->ral_2 ?? null;
$ral3 = $colorSystem->ral_3 ?? null;
$colorRu1 = $this->findRussianColorDescription($ralCodes, $ral1);
$colorRu2 = $this->findRussianColorDescription($ralCodes, $ral2);
$colorRu3 = $this->findRussianColorDescription($ralCodes, $ral3);
// Create or update Paint Matrix
$updateData = [
'project' => @$zones[$lineListData->line_no],
'area' => $lineListData->unit,
'description' => "PIPE",
'line' => $lineListData->line_no,
'fluid_code_description' => $lineListData->fluid_ru,
'fluid_code' => $lineListData->fluid_code,
'design_temperature' => $lineListData->design_temperature,
'operation_temperature' => $lineListData->working_temperature,
'paint_cycle' => $lineListData->painting_cycle,
];
if ($paintSystem) {
$updateData = array_merge($updateData, [
'surface_preparation' => $paintSystem->surface_preparation,
'touch_up_of_damaged_parts' => $paintSystem->surface_roughness,
'primer_coat' => $paintSystem->primer_coat_name_1,
'brend_name_1' => $paintSystem->brand_name_1,
'thickness_1' => $paintSystem->thickness_1,
'intermediate_coat' => $paintSystem->primer_coat_name_2,
'brend_name_2' => $paintSystem->brand_name_2,
'thickness_2' => $paintSystem->thickness_2,
'final_coat' => $paintSystem->primer_coat_name_3,
'brend_name_3' => $paintSystem->brand_name_3,
'thickness_3' => $paintSystem->thickness_3,
'total_thickness' => ($paintSystem->thickness_1 ?? 0) + ($paintSystem->thickness_2 ?? 0) + ($paintSystem->thickness_3 ?? 0),
]);
}
if ($colorSystem) {
$updateData = array_merge($updateData, [
'fluid_code_description' => $colorSystem->fluid_code_description ?? $lineListData->fluid_ru,
'design_temperature' => $colorSystem->design_temperature ?? $lineListData->design_temperature,
'operation_temperature' => $colorSystem->working_temperature ?? $lineListData->working_temperature,
'ral_code_1' => $ral1,
'ral_code_2' => $ral2,
'ral_code_3' => $ral3,
'colour_1' => $colorRu1,
'colour_2' => $colorRu2,
'colour_3' => $colorRu3,
]);
}
$whereData = [
'line' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code,
'area' => $lineListData->unit,
];
$existingPaintMatrix = PaintMatrix::where($whereData)->first();
if ($existingPaintMatrix) {
$existingPaintMatrix->update($updateData);
Log::debug("Updated existing Paint Matrix for line: {$lineListData->line_no}");
} else {
PaintMatrix::create($updateData);
Log::debug("Created new Paint Matrix for line: {$lineListData->line_no}");
}
$paintFollowUpCount++;
// Surface preparation sync: Paint Matrix -> Paint Follow Ups
$paintFollowUps = PaintFollowUp::where("line", $lineListData->line_no)
->where('area', $lineListData->unit)
->where('fluid_code', $lineListData->fluid_code)
->orderBy('id', 'ASC') // Deadlock prevention
->get();
$paintMatrix = PaintMatrix::where("line", $lineListData->line_no)->get();
$surfaces = [];
foreach($paintMatrix AS $pm) {
$surfaces[$pm->line] = [
'surface_preparation' => $pm->surface_preparation,
'touch_up_of_damaged_parts' => $pm->touch_up_of_damaged_parts,
];
}
// Process paint follow ups in chunks with TransactionHelper
TransactionHelper::chunkTransaction(
$paintFollowUps,
function ($pfuChunk) use ($surfaces, &$paintMatrix2PaintFollowUpCount) {
foreach($pfuChunk AS $pfu) {
if(isset($surfaces[$pfu->line])) {
if(strpos($pfu->spool_no_joint_no, "SPL") !== false) {
$thisSurface = $surfaces[$pfu->line]['surface_preparation'];
} else {
$thisSurface = $surfaces[$pfu->line]['touch_up_of_damaged_parts'];
}
db("paint_follow_ups")->where([
'id' => $pfu->id
])->update([
'surface_roughness' => $thisSurface
]);
$paintMatrix2PaintFollowUpCount++;
}
}
return $pfuChunk->count();
},
1, // Chunk size
10000 // 10ms delay
);
// Sync paint coat details to paint_follow_ups
/*
$paintMatrix = db('paint_matrices')
->where('line', $lineListData->line_no)
->where('area', $lineListData->unit)
->where('fluid_code', $lineListData->fluid_code)
->where('paint_cycle', $lineListData->painting_cycle)
->first();
if(!is_null($paintMatrix)) {
$updateArray = [
'cycle' => $paintMatrix->paint_cycle,
'primer_coat_name_1' => $paintMatrix->primer_coat,
'brend_name_1' => $paintMatrix->brend_name_1,
'colour_1' => $paintMatrix->colour_1,
'ral_code_1' => $paintMatrix->ral_code_1,
'thickness_1' => $paintMatrix->thickness_1,
'intermediate_coat_name_2' => $paintMatrix->intermediate_coat,
'brend_name_2' => $paintMatrix->brend_name_2,
'colour_2' => $paintMatrix->colour_2,
'ral_code_2' => $paintMatrix->ral_code_2,
'thickness_2' => $paintMatrix->thickness_2,
'final_coat_name_3' => $paintMatrix->final_coat,
'brend_name_3' => $paintMatrix->brend_name_3,
'colour_3' => $paintMatrix->colour_3,
'ral_code_3' => $paintMatrix->ral_code_3,
'thickness_3' => $paintMatrix->thickness_3,
];
$result = db('paint_follow_ups')->where([
'line' => $lineListData->line_no,
'cycle' => $lineListData->painting_cycle,
'area' => $lineListData->unit,
'fluid_code' => $lineListData->fluid_code,
])
->whereNull('primer_coating_start_date')
->whereNull('primer_coating_finish_date')
->whereNull('start_intermediate_date2')
->whereNull('finish_intermediate_date2')
->whereNull('final_coat_start_date3')
->whereNull('final_coat_finish_date3')
->update($updateArray);
Log::debug("$result data sync Paint Matrix ==> Paint Follow Ups");
}
Log::debug("$paintFollowUpCount Data has been create or update from lineList to Paint Matrix And update Paint Follow Up");
Log::debug("$paintMatrix2PaintFollowUpCount Data has been sync from Paint Matrix to Paint Follow Up");
*/
return [
'success' => true,
'paint_matrices_processed' => $paintFollowUpCount,
'surface_synced' => $paintMatrix2PaintFollowUpCount
];
}
/**
* Find Russian color description for a RAL code
*/
protected function findRussianColorDescription($ralCodes, $ralCode): string
{
if (empty($ralCode) || empty($ralCodes)) {
return '';
}
foreach ($ralCodes as $entry) {
if (isset($entry['ral_code']) && $entry['ral_code'] == $ralCode && isset($entry['ru'])) {
return $entry['ru'];
}
}
return '';
}
}
@@ -0,0 +1,135 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Jobs\ExecuteSaveTriggerJob;
/**
* Paint System Sync Trigger
*
* Syncs data to paint_systems and color_systems tables
*/
class PaintSystemSyncTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Paint System Sync';
}
public function getDependentFields(): array
{
return [
'painting_cycle',
'rev',
'fluid_code',
'fluid_ru',
'design_temperature',
'working_temperature',
'unit'
];
}
protected function process($lineListData, $beforeData, array $context): array
{
$count = 0;
if (!empty($lineListData->painting_cycle)) {
// Update or insert into paint_systems
$existingRecord = DB::table('paint_systems')
->where('paint_cycle', $lineListData->painting_cycle)
->first();
if ($existingRecord) {
DB::table('paint_systems')
->where('paint_cycle', $lineListData->painting_cycle)
->update([
'paint_cycle' => $lineListData->painting_cycle,
'revision' => $lineListData->rev ?? '',
]);
ExecuteSaveTriggerJob::dispatch(
'paint_systems',
['key' => ['id' => $existingRecord->id]],
['id' => $existingRecord->id],
['paint_cycle' => $lineListData->painting_cycle, 'revision' => $lineListData->rev ?? ''],
$existingRecord,
'update'
);
} else {
$insertedId = DB::table('paint_systems')->insertGetId([
'paint_cycle' => $lineListData->painting_cycle,
'revision' => $lineListData->rev ?? '',
'created_at' => now(),
'updated_at' => now(),
]);
$newRecord = DB::table('paint_systems')->find($insertedId);
ExecuteSaveTriggerJob::dispatch(
'paint_systems',
['key' => ['id' => $insertedId]],
['id' => $insertedId],
['paint_cycle' => $lineListData->painting_cycle, 'revision' => $lineListData->rev ?? ''],
$newRecord,
'insert'
);
}
// Update or insert into color_systems
$colorSystemRecord = DB::table('color_systems')
->where('fluid_code', $lineListData->fluid_code)
->first();
$colorSystemData = [
'fluid_code_description' => $lineListData->fluid_ru ?? "",
'fluid_code' => $lineListData->fluid_code,
'design_temperature' => $lineListData->design_temperature ?? "",
'working_temperature' => $lineListData->working_temperature ?? "",
'unit' => $lineListData->unit ?? "",
'updated_at' => now(),
];
if ($colorSystemRecord) {
DB::table('color_systems')
->where('id', $colorSystemRecord->id)
->update($colorSystemData);
ExecuteSaveTriggerJob::dispatch(
'color_systems',
['key' => ['id' => $colorSystemRecord->id]],
['id' => $colorSystemRecord->id],
$colorSystemData,
$colorSystemRecord,
'update'
);
} else {
$colorSystemData['created_at'] = now();
$insertedId = DB::table('color_systems')->insertGetId($colorSystemData);
$newColorRecord = DB::table('color_systems')->find($insertedId);
ExecuteSaveTriggerJob::dispatch(
'color_systems',
['key' => ['id' => $insertedId]],
['id' => $insertedId],
$colorSystemData,
$newColorRecord,
'insert'
);
}
$count++;
}
Log::debug("Sync Paint System $count row update or create");
return [
'success' => true,
'processed_records' => $count
];
}
}
@@ -0,0 +1,159 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Paint System To Matrix Sync Trigger
*
* Syncs paint_systems + color_systems data to paint_matrices
* Includes RAL code to Russian color description mapping
*/
class PaintSystemToMatrixSyncTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Paint System To Matrix Sync';
}
public function getDependentFields(): array
{
return [
];
}
protected function process($lineListData, $beforeData, array $context): array
{
Log::info("PaintSystemToMatrixSyncTrigger started", ['painting_cycle' => $lineListData->painting_cycle]);
// Skip if painting_cycle is empty
if (empty($lineListData->painting_cycle)) {
Log::debug("PaintSystemToMatrixSyncTrigger skipped - painting_cycle is empty");
return ['skipped' => true, 'reason' => 'painting_cycle_empty'];
}
// Get paint system
$paintSystem = DB::table('paint_systems')
->where('paint_cycle', $lineListData->painting_cycle)
->first();
if (!$paintSystem) {
Log::debug("No paint system found for paint cycle: {$lineListData->painting_cycle}");
return ['skipped' => true, 'reason' => 'paint_system_not_found'];
}
// Get color system
$colorSystem = DB::table('color_systems')
->where('fluid_code', $lineListData->fluid_code)
->first();
if (!$colorSystem) {
Log::debug("No color system found for fluid code: {$lineListData->fluid_code}");
return ['skipped' => true, 'reason' => 'color_system_not_found'];
}
// Get RAL values
$ral1 = $colorSystem->ral_1;
$ral2 = $colorSystem->ral_2;
$ral3 = $colorSystem->ral_3;
// Get Russian color descriptions
$ralCodes = j(setting("ral-codes"));
$colorRu1 = $this->findRussianColorDescription($ralCodes, $ral1);
$colorRu2 = $this->findRussianColorDescription($ralCodes, $ral2);
$colorRu3 = $this->findRussianColorDescription($ralCodes, $ral3);
// Find paint matrices with matching paint_cycle
$paintMatrices = DB::table('paint_matrices')
->where('area', $lineListData->unit)
->where('fluid_code', $lineListData->fluid_code)
->where('paint_cycle', $lineListData->painting_cycle)
->where('line', $lineListData->line_no)
->orderBy('id', 'ASC') // Deadlock prevention
->get();
Log::info("Paint Matrices found", ['count' => $paintMatrices->count()]);
$updatedCount = 0;
// Process in chunks with TransactionHelper
TransactionHelper::chunkTransaction(
$paintMatrices,
function ($pmChunk) use ($paintSystem, $colorSystem, $ral1, $ral2, $ral3, $colorRu1, $colorRu2, $colorRu3, &$updatedCount) {
foreach ($pmChunk as $paintMatrix) {
$updateData = [
// Paint System data
'surface_preparation' => $paintSystem->surface_preparation,
'touch_up_of_damaged_parts' => $paintSystem->surface_roughness,
'primer_coat' => $paintSystem->primer_coat_name_1,
'brend_name_1' => $paintSystem->brand_name_1,
'thickness_1' => $paintSystem->thickness_1,
'intermediate_coat' => $paintSystem->primer_coat_name_2,
'brend_name_2' => $paintSystem->brand_name_2,
'thickness_2' => $paintSystem->thickness_2,
'final_coat' => $paintSystem->primer_coat_name_3,
'brend_name_3' => $paintSystem->brand_name_3,
'thickness_3' => $paintSystem->thickness_3,
'total_thickness' => ($paintSystem->thickness_1 ?? 0) + ($paintSystem->thickness_2 ?? 0) + ($paintSystem->thickness_3 ?? 0),
// Color System data
'fluid_code_description' => $colorSystem->fluid_code_description,
'design_temperature' => $colorSystem->design_temperature,
'operation_temperature' => $colorSystem->working_temperature,
'ral_code_1' => $ral1,
'ral_code_2' => $ral2,
'ral_code_3' => $ral3,
'colour_1' => $colorRu1,
'colour_2' => $colorRu2,
'colour_3' => $colorRu3,
'updated_at' => now()
];
$result = DB::table('paint_matrices')
->where('id', $paintMatrix->id)
->update($updateData);
if ($result) {
$updatedCount++;
}
}
return $pmChunk->count();
},
1, // Chunk size
10000 // 10ms delay
);
Log::debug("Paint Systems to Paint Matrices synchronization completed. Updated $updatedCount records.");
return [
'success' => true,
'updated_records' => $updatedCount
];
}
/**
* Find Russian color description for a RAL code
*/
protected function findRussianColorDescription($ralCodes, $ralCode): string
{
if (empty($ralCode) || empty($ralCodes)) {
return '';
}
foreach ($ralCodes as $entry) {
if (isset($entry['ral_code']) && $entry['ral_code'] == $ralCode && isset($entry['ru'])) {
return $entry['ru'];
}
}
return '';
}
}
@@ -0,0 +1,136 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use Illuminate\Support\Facades\Log;
/**
* Painting Cycle Deletion Trigger
*
* Handles cleanup when painting_cycle is deleted (changed from value to empty):
* - Deletes paint_matrices records
* - Deletes paint_follow_ups records (where date fields are null)
* - Deletes construction_paint_logs records (where date fields are null)
* - Deletes nde_matrices records
*/
class PaintingCycleDeletionTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Painting Cycle Deletion';
}
public function getDependentFields(): array
{
return ['painting_cycle'];
}
protected function process($lineListData, $beforeData, array $context): array
{
// Only run if painting_cycle was deleted (had value, now empty)
if (!isset($beforeData) || $beforeData->painting_cycle == "" || $lineListData->painting_cycle != "") {
return ['skipped' => true, 'reason' => 'painting_cycle_not_deleted'];
}
Log::debug("Painting cycle siliniyor - ilgili verileri temizleme başlatılıyor", [
'before_painting_cycle' => $beforeData->painting_cycle,
'current_painting_cycle' => $lineListData->painting_cycle,
'line_no' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code
]);
$results = [];
// Delete paint matrices
$results['deleted_paint_matrices'] = db("paint_matrices")
->where('line', $beforeData->line_no)
->where('fluid_code', $beforeData->fluid_code)
->where('area', $beforeData->unit)
->delete();
// Delete paint follow ups (only if no dates filled)
$results['deleted_paint_follow_ups'] = db('paint_follow_ups')
->where([
'line' => $lineListData->line_no,
'area' => $lineListData->unit,
'fluid_code' => $lineListData->fluid_code,
])
->whereNull('primer_coating_start_date')
->whereNull('primer_coating_finish_date')
->whereNull('start_intermediate_date2')
->whereNull('finish_intermediate_date2')
->whereNull('final_coat_start_date3')
->whereNull('final_coat_finish_date3')
->delete();
// Delete from construction_paint_logs (spool-based, only if no dates filled)
// Get all spools for this line from weld_logs
$spools = db("weld_logs")
->where("line_number", $lineListData->line_no)
->whereNotNull('spool_number')
->where('spool_number', '!=', '')
->distinct()
->pluck('spool_number');
if ($spools->isNotEmpty()) {
$results['deleted_construction_paint_logs'] = db('construction_paint_logs')
->where('line', $lineListData->line_no)
->whereIn('spool', $spools)
->whereNull('blasting_date')
->whereNull('blasting_finish_date')
->whereNull('painting_date_1')
->whereNull('painting_finish_date_1')
->whereNull('painting_date_2')
->whereNull('painting_finish_date_2')
->whereNull('painting_date_3')
->whereNull('painting_finish_date_3')
->delete();
Log::debug("Construction Paint Logs deleted (spool-based)", [
'line_no' => $lineListData->line_no,
'spool_count' => $spools->count(),
'deleted_count' => $results['deleted_construction_paint_logs']
]);
} else {
// Fallback to unit-based deletion if no spools found
$results['deleted_construction_paint_logs'] = db('construction_paint_logs')
->where([
'line' => $lineListData->line_no,
'unit' => $lineListData->unit,
'fluid_code' => $lineListData->fluid_code,
])
->whereNull('blasting_date')
->whereNull('blasting_finish_date')
->whereNull('painting_date_1')
->whereNull('painting_finish_date_1')
->whereNull('painting_date_2')
->whereNull('painting_finish_date_2')
->whereNull('painting_date_3')
->whereNull('painting_finish_date_3')
->delete();
Log::debug("Construction Paint Logs deleted (unit-based fallback)", [
'line_no' => $lineListData->line_no,
'unit' => $lineListData->unit,
'deleted_count' => $results['deleted_construction_paint_logs']
]);
}
// Delete NDE Matrix
$results['deleted_nde_matrices'] = db('nde_matrices')
->where('line', $lineListData->line_no)
->where('fluid', $lineListData->fluid_code)
->delete();
Log::info("Painting cycle deletion cleanup completed", [
'deleted_records' => $results,
'line_no' => $lineListData->line_no,
'fluid_code' => $lineListData->fluid_code
]);
return $results;
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use Illuminate\Support\Facades\Log;
/**
* Spool Status Changer Final Trigger
*
* Runs final spool status changer view at the end of all syncs
*/
class SpoolStatusChangerFinalTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Spool Status Changer Final';
}
public function getDependentFields(): array
{
return ['line_no', 'painting_cycle'];
}
protected function process($lineListData, $beforeData, array $context): array
{
$paintChanged = false;
// Check if painting_cycle changed (especially if old was filled and new is empty)
if((!empty($beforeData->painting_cycle) && empty($lineListData->painting_cycle)) ||
(isset($lineListData->painting_cycle) && $lineListData->painting_cycle != $beforeData->painting_cycle)) {
$paintChanged = true;
Log::info("Painting cycle changed", [
'before_painting_cycle' => $beforeData->painting_cycle,
'current_painting_cycle' => $lineListData->painting_cycle
]);
}
spoolStatusChanger($lineListData->line_no, null, $paintChanged);
Log::info("Spool Status Changer triggered", [
'line_no' => $lineListData->line_no
]);
return [
'success' => true,
'executed' => 'spool_status_changer_view'
];
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use Illuminate\Support\Facades\Log;
/**
* Test Package Tracing Sync Trigger
*
* Syncs tracing_type from Line List to:
* - Test Packages (tracing field)
* - Test Pack Base Statuses (tracing field)
* - NDE Matrices (tracing and tracing_type fields)
* Triggered when tracing_type field changes in Line List
*/
class TestPackageTracingSyncTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'Tracing Sync';
}
public function getDependentFields(): array
{
return ['tracing', 'tracing_type'];
}
protected function process($lineListData, $beforeData, array $context): array
{
// Update 'tracing' field in test_packages table where p_id matches line_no
// user requirement: p id no = line no match
// 1. Get test_package_no's from test_pack_base_statuses where drawing_no matches line_no
$testPackageNos = db('test_pack_base_statuses')
->where('drawing_no', $lineListData->line_no)
->pluck('test_package_no')
->toArray();
Log::info("Test Package Tracing Sync Trigger", [
'line_no' => $lineListData->line_no,
'test_package_nos' => $testPackageNos,
]);
// 2. Update test_packages where test_package_no matches the found ones
if (!empty($testPackageNos)) {
$result = db("test_packages")
->whereIn("test_package_number", $testPackageNos)
->update([
'tracing' => $lineListData->tracing
]);
} else {
$result = 0;
}
// No need to update test_pack_base_statuses per user request
// Sync to nde_matrices (using line_no)
Log::info("Line List Tracing Sync completed", [
'line_no' => $lineListData->line_no,
'tracing' => $lineListData->tracing,
'test_packages_updated' => $result,
]);
return [
'success' => true,
'test_packages_updated' => $result,
];
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Services\LineListTriggers\Triggers;
use App\Services\LineListTriggers\Base\BaseLineListTrigger;
use Illuminate\Support\Facades\Log;
/**
* WeldLog Fields Sync Trigger
*
* Synchronizes 15+ fields from Line Lists to WeldLogs
* Updates all weld logs with matching line_number
*/
class WeldLogFieldsSyncTrigger extends BaseLineListTrigger
{
public function getName(): string
{
return 'WeldLog Fields Sync';
}
public function getDependentFields(): array
{
return []; // Always run - syncs all line list fields
}
protected function process($lineListData, $beforeData, array $context): array
{
$data = [
'painting_cycle' => $lineListData->painting_cycle,
'main_nps' => $lineListData->dn,
'fluid_code' => $lineListData->fluid_code,
'service_category' => $lineListData->category,
'fluid_group' => $lineListData->fluid_group,
'piping_class' => $lineListData->pipe_material_class,
'external_finish_type' => $lineListData->external_finish_type,
'circuit_number' => $lineListData->circuit_number,
'p_id' => $lineListData->p_id,
'type_of_test' => $lineListData->test_media,
'test_pressure' => $lineListData->test_pressure_mpa,
'line_specification' => $lineListData->line_specification,
'design_pressure_mpa' => $lineListData->design_pressure_mpa,
'design_temperature_s' => $lineListData->design_temperature,
'operating_pressure_mpa' => $lineListData->working_pressure_mpa,
'operating_temperature_s' => $lineListData->working_temperature,
'ndt_percent' => $lineListData->ndt,
];
$result = db("weld_logs")
->where("line_number", $lineListData->line_no)
->update($data);
Log::debug("WeldLog güncellendi", [
'line_number' => $lineListData->line_no,
'affected_rows' => $result
]);
return [
'success' => true,
'updated_records' => $result
];
}
}
+699
View File
@@ -0,0 +1,699 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\File;
use Carbon\Carbon;
/**
* NAKS Multi-Module Synchronization Service
*
* Synchronizes NAKS data across multiple project sites for various modules:
* - NAKS Technology (naks_certificates)
* - NAKS Welder (naks_welders)
* - NAKS Consumables (naks_consumables)
* - NAKS Expert (register_of_experts)
* - NAKS Equipment (welding_equipment)
*
* Each site runs this service to pull data from other sites.
*/
class NaksSyncService
{
protected string $basePdfPath = 'documents/003_Welding_Database';
protected int $timeout = 30;
protected int $batchSize = 100;
protected ?string $adminEmail = null;
protected ?string $adminPassword = null;
protected ?string $currentSiteUrl = null;
/**
* Module configurations
* Each module defines its table, unique keys, and PDF folder
*/
protected array $modules = [
'technology' => [
'name' => 'NAKS Technology',
'table' => 'naks_certificates',
'unique_keys' => ['short_number', 'certificate_no'],
'pdf_folder' => '0000_Naks Technology',
'download_field' => 'download',
],
'welder' => [
'name' => 'NAKS Welder',
'table' => 'naks_welders',
'unique_keys' => ['naks_certificate_no', 'welder_id'],
'pdf_folder' => '0003_Naks Welder',
'download_field' => 'download',
],
'consumables' => [
'name' => 'NAKS Consumables',
'table' => 'naks_consumables',
'unique_keys' => ['naks_certificate_no', 'batch_number'],
'pdf_folder' => '0002_Naks Consumables',
'download_field' => 'download',
],
'expert' => [
'name' => 'NAKS Expert',
'table' => 'register_of_experts',
'unique_keys' => ['certificate_no'],
'pdf_folder' => '0004_Naks Expert',
'download_field' => 'download',
],
'equipment' => [
'name' => 'NAKS Equipment',
'table' => 'welding_equipment',
'unique_keys' => ['attestation'],
'pdf_folder' => '0001_Naks Equipment',
'download_field' => 'download',
],
];
protected array $stats = [
'total_projects' => 0,
'successful_projects' => 0,
'failed_projects' => [],
'modules' => [],
'errors' => [],
];
public function __construct()
{
$this->adminEmail = env('SYNC_ADMIN_EMAIL');
$this->adminPassword = env('SYNC_ADMIN_PASSWORD');
$this->currentSiteUrl = rtrim(config('app.url'), '/');
}
/**
* Get available modules
*/
public function getModules(): array
{
return $this->modules;
}
/**
* Get module configuration by key
*/
public function getModuleConfig(string $moduleKey): ?array
{
return $this->modules[$moduleKey] ?? null;
}
/**
* Run the full synchronization process
*
* @param string $moduleFilter Module to sync ('all' or specific module key)
* @param string|null $specificProject Filter to sync only a specific project
* @param bool $dryRun If true, don't make any changes
* @param bool $force If true, sync all records (ignore last_synced_id)
* @return array Statistics about the sync operation
*/
public function sync(
string $moduleFilter = 'all',
?string $specificProject = null,
bool $dryRun = false,
bool $force = false
): array {
Log::info("🔄 NAKS Multi-Module Sync Started", [
'module_filter' => $moduleFilter,
'specific_project' => $specificProject,
'dry_run' => $dryRun,
'force' => $force,
]);
if (!$this->adminEmail || !$this->adminPassword) {
$error = 'SYNC_ADMIN_EMAIL or SYNC_ADMIN_PASSWORD not configured in .env';
Log::error($error);
$this->stats['errors'][] = $error;
return $this->stats;
}
// Determine which modules to sync
$modulesToSync = $this->getModulesToSync($moduleFilter);
if (empty($modulesToSync)) {
$error = "Invalid module: {$moduleFilter}. Available: " . implode(', ', array_keys($this->modules));
Log::error($error);
$this->stats['errors'][] = $error;
return $this->stats;
}
// Initialize module stats
foreach ($modulesToSync as $moduleKey => $moduleConfig) {
$this->stats['modules'][$moduleKey] = [
'name' => $moduleConfig['name'],
'total_synced' => 0,
'total_inserted' => 0,
'total_updated' => 0,
'total_skipped' => 0,
'total_pdfs_downloaded' => 0,
];
}
try {
$projects = $this->getProjectUrls();
$this->stats['total_projects'] = count($projects);
foreach ($projects as $project) {
$projectUrl = rtrim($project['url'], '/');
$projectName = $project['project_name'] ?? $projectUrl;
// Skip if filtering by specific project
if ($specificProject && !$this->matchesProject($projectUrl, $projectName, $specificProject)) {
continue;
}
// Skip current site (don't sync from ourselves)
if ($this->isSameSite($projectUrl)) {
Log::debug("Skipping current site: {$projectUrl}");
continue;
}
try {
$this->syncFromProject($projectUrl, $projectName, $modulesToSync, $dryRun, $force);
$this->stats['successful_projects']++;
} catch (\Exception $e) {
Log::error("Failed to sync from project: {$projectName}", [
'url' => $projectUrl,
'error' => $e->getMessage(),
]);
$this->stats['failed_projects'][] = [
'name' => $projectName,
'url' => $projectUrl,
'error' => $e->getMessage(),
];
}
}
} catch (\Exception $e) {
Log::error("NAKS Sync failed: " . $e->getMessage());
$this->stats['errors'][] = $e->getMessage();
}
Log::info("🔄 NAKS Multi-Module Sync Completed", $this->stats);
return $this->stats;
}
/**
* Get modules to sync based on filter
*/
protected function getModulesToSync(string $filter): array
{
if ($filter === 'all') {
return $this->modules;
}
if (isset($this->modules[$filter])) {
return [$filter => $this->modules[$filter]];
}
return [];
}
/**
* Get list of project URLs from the API
*/
public function getProjectUrls(): array
{
$response = Http::timeout($this->timeout)
->get($this->currentSiteUrl . '/api/project-app-urls');
if (!$response->successful()) {
throw new \Exception("Failed to fetch project URLs: " . $response->status());
}
$data = $response->json();
if (!isset($data['data']) || !is_array($data['data'])) {
throw new \Exception("Invalid project URLs response format");
}
return $data['data'];
}
/**
* Authenticate with a remote site and get bearer token
*/
public function authenticate(string $baseUrl): ?string
{
try {
$response = Http::timeout($this->timeout)
->post(rtrim($baseUrl, '/') . '/api/login', [
'email' => $this->adminEmail,
'password' => $this->adminPassword,
]);
if (!$response->successful()) {
Log::warning("Authentication failed for {$baseUrl}", [
'status' => $response->status(),
]);
return null;
}
$data = $response->json();
return $data['data']['access_token'] ?? null;
} catch (\Exception $e) {
Log::error("Authentication error for {$baseUrl}: " . $e->getMessage());
return null;
}
}
/**
* Fetch records from a remote site for a specific module
*/
public function fetchRecords(string $baseUrl, string $token, string $tableName, int $afterId = 0, int $take = 100): array
{
$url = rtrim($baseUrl, '/') . '/api/' . $tableName . '/read';
$params = [
'take' => $take,
'sort' => json_encode([['selector' => 'id', 'desc' => false]]),
];
if ($afterId > 0) {
$params['filter'] = json_encode(['id', '>', $afterId]);
}
$response = Http::timeout($this->timeout)
->withToken($token)
->get($url, $params);
if (!$response->successful()) {
throw new \Exception("Failed to fetch records from {$tableName}: " . $response->status());
}
$data = $response->json();
return $data['data'] ?? [];
}
/**
* Sync all modules from a specific project
*/
protected function syncFromProject(
string $projectUrl,
string $projectName,
array $modulesToSync,
bool $dryRun,
bool $force
): void {
Log::info("Syncing from project: {$projectName}", ['url' => $projectUrl]);
// Authenticate once for all modules
$token = $this->authenticate($projectUrl);
if (!$token) {
throw new \Exception("Authentication failed");
}
// Sync each module
foreach ($modulesToSync as $moduleKey => $moduleConfig) {
try {
$this->syncModuleFromProject(
$projectUrl,
$projectName,
$token,
$moduleKey,
$moduleConfig,
$dryRun,
$force
);
} catch (\Exception $e) {
Log::error("Failed to sync module {$moduleKey} from {$projectName}: " . $e->getMessage());
$this->stats['errors'][] = "Module {$moduleKey} from {$projectName}: " . $e->getMessage();
}
}
}
/**
* Sync a specific module from a project
*/
protected function syncModuleFromProject(
string $projectUrl,
string $projectName,
string $token,
string $moduleKey,
array $moduleConfig,
bool $dryRun,
bool $force
): void {
$tableName = $moduleConfig['table'];
Log::info("Syncing module: {$moduleConfig['name']} from {$projectName}");
// Get last synced ID
$lastSyncedId = $force ? 0 : $this->getLastSyncedId($tableName, $projectUrl);
Log::debug("Last synced ID for {$tableName} from {$projectName}: {$lastSyncedId}");
$moduleStats = [
'synced' => 0,
'inserted' => 0,
'updated' => 0,
'skipped' => 0,
'pdfs' => 0,
];
$maxId = $lastSyncedId;
$hasMore = true;
while ($hasMore) {
// Fetch batch of records
$records = $this->fetchRecords($projectUrl, $token, $tableName, $maxId, $this->batchSize);
if (empty($records)) {
$hasMore = false;
break;
}
foreach ($records as $record) {
$recordId = $record['id'] ?? 0;
$maxId = max($maxId, $recordId);
try {
$result = $this->syncRecord($record, $moduleConfig, $projectUrl, $projectName, $dryRun);
$moduleStats['synced']++;
switch ($result['action']) {
case 'inserted':
$moduleStats['inserted']++;
break;
case 'updated':
$moduleStats['updated']++;
break;
case 'skipped':
$moduleStats['skipped']++;
break;
}
// Download PDF if needed
$downloadField = $moduleConfig['download_field'] ?? 'download';
if ($result['action'] !== 'skipped' && !$dryRun && !empty($record[$downloadField])) {
if ($this->downloadPdf($projectUrl, $record[$downloadField], $moduleConfig['pdf_folder'])) {
$moduleStats['pdfs']++;
}
}
} catch (\Exception $e) {
Log::warning("Failed to sync record", [
'module' => $moduleKey,
'id' => $recordId,
'error' => $e->getMessage(),
]);
}
}
// If we got less than batch size, we're done
if (count($records) < $this->batchSize) {
$hasMore = false;
}
}
// Update sync state
if (!$dryRun && $maxId > $lastSyncedId) {
$this->updateSyncState($tableName, $projectUrl, $maxId, $moduleStats['synced']);
}
// Update global stats
$this->stats['modules'][$moduleKey]['total_synced'] += $moduleStats['synced'];
$this->stats['modules'][$moduleKey]['total_inserted'] += $moduleStats['inserted'];
$this->stats['modules'][$moduleKey]['total_updated'] += $moduleStats['updated'];
$this->stats['modules'][$moduleKey]['total_skipped'] += $moduleStats['skipped'];
$this->stats['modules'][$moduleKey]['total_pdfs_downloaded'] += $moduleStats['pdfs'];
Log::info("Completed sync of {$moduleConfig['name']} from {$projectName}", $moduleStats);
}
/**
* Sync a single record
*
* @param string $projectName The project name where the record is synced from (stored in source_project column)
* @return array ['action' => 'inserted'|'updated'|'skipped', 'local_id' => int|null]
*/
public function syncRecord(array $record, array $moduleConfig, string $sourceUrl, string $projectName, bool $dryRun = false): array
{
$tableName = $moduleConfig['table'];
$uniqueKeys = $moduleConfig['unique_keys'];
// Build unique key conditions
$conditions = [];
foreach ($uniqueKeys as $key) {
$value = $record[$key] ?? null;
if ($value === null || $value === '') {
return ['action' => 'skipped', 'local_id' => null, 'reason' => "missing_unique_key: {$key}"];
}
$conditions[$key] = $value;
}
// Check if record exists locally
$query = DB::table($tableName);
foreach ($conditions as $field => $value) {
$query->where($field, $value);
}
$existing = $query->first();
// Prepare data for insert/update
$data = $this->prepareRecordData($record, $tableName);
if ($existing) {
// Compare updated_at to decide if we should update
$remoteUpdatedAt = isset($record['updated_at']) ? Carbon::parse($record['updated_at']) : null;
$localUpdatedAt = isset($existing->updated_at) ? Carbon::parse($existing->updated_at) : null;
// Only update if remote is newer
if ($remoteUpdatedAt && $localUpdatedAt && $remoteUpdatedAt <= $localUpdatedAt) {
return ['action' => 'skipped', 'local_id' => $existing->id, 'reason' => 'local_is_newer'];
}
if (!$dryRun) {
// Add source_project to track where the record came from
$data['source_project'] = $projectName;
DB::table($tableName)
->where('id', $existing->id)
->update($data);
}
return ['action' => 'updated', 'local_id' => $existing->id];
} else {
if (!$dryRun) {
// Add source_project to track where the record came from
$data['source_project'] = $projectName;
$newId = DB::table($tableName)->insertGetId($data);
return ['action' => 'inserted', 'local_id' => $newId];
}
return ['action' => 'inserted', 'local_id' => null];
}
}
/**
* Prepare record data for database insert/update
*/
protected function prepareRecordData(array $record, string $tableName): array
{
// Remove fields that should not be copied
$excludeFields = ['id', 'uid'];
$data = [];
$columns = \Schema::getColumnListing($tableName);
foreach ($record as $key => $value) {
if (in_array($key, $excludeFields)) {
continue;
}
if (in_array($key, $columns)) {
$data[$key] = $value;
}
}
// Update timestamps
$data['updated_at'] = now();
// If inserting, set created_at
if (!isset($data['created_at'])) {
$data['created_at'] = now();
}
return $data;
}
/**
* Download PDF file from remote site
*/
public function downloadPdf(string $baseUrl, string $remotePath, string $pdfFolder): bool
{
if (empty($remotePath)) {
return false;
}
// Extract just the filename
$filename = basename($remotePath);
// Local path
$localPath = storage_path($this->basePdfPath . '/' . $pdfFolder . '/' . $filename);
// Skip if file already exists locally
if (File::exists($localPath)) {
return false;
}
try {
// Construct download URL
$downloadUrl = rtrim($baseUrl, '/') . '/' . ltrim($remotePath, '/');
$response = Http::timeout(60)->get($downloadUrl);
if (!$response->successful()) {
Log::warning("Failed to download PDF: {$downloadUrl}", [
'status' => $response->status(),
]);
return false;
}
// Ensure directory exists
$directory = dirname($localPath);
if (!File::isDirectory($directory)) {
File::makeDirectory($directory, 0755, true);
}
// Save file
File::put($localPath, $response->body());
Log::debug("Downloaded PDF: {$filename} to {$pdfFolder}");
return true;
} catch (\Exception $e) {
Log::warning("Error downloading PDF: " . $e->getMessage(), [
'url' => $baseUrl,
'path' => $remotePath,
]);
return false;
}
}
/**
* Get the last synced ID for a table and source URL
*/
public function getLastSyncedId(string $tableName, string $sourceUrl): int
{
$state = DB::table('sync_states')
->where('table_name', $tableName)
->where('source_url', $sourceUrl)
->first();
return $state ? (int) $state->last_synced_id : 0;
}
/**
* Update the sync state for a table and source URL
*/
public function updateSyncState(string $tableName, string $sourceUrl, int $lastId, int $syncedCount = 0): void
{
$existing = DB::table('sync_states')
->where('table_name', $tableName)
->where('source_url', $sourceUrl)
->first();
if ($existing) {
DB::table('sync_states')
->where('id', $existing->id)
->update([
'last_synced_id' => $lastId,
'last_synced_at' => now(),
'synced_count' => $existing->synced_count + $syncedCount,
'updated_at' => now(),
]);
} else {
DB::table('sync_states')->insert([
'table_name' => $tableName,
'source_url' => $sourceUrl,
'last_synced_id' => $lastId,
'last_synced_at' => now(),
'synced_count' => $syncedCount,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
/**
* Check if a URL matches the current site
*/
protected function isSameSite(string $url): bool
{
$normalizedUrl = $this->normalizeUrl($url);
$normalizedCurrent = $this->normalizeUrl($this->currentSiteUrl);
return $normalizedUrl === $normalizedCurrent;
}
/**
* Normalize URL for comparison
*/
protected function normalizeUrl(string $url): string
{
$parsed = parse_url($url);
$host = $parsed['host'] ?? '';
// Remove common prefixes/suffixes
$host = str_replace(['www.', '/admin'], '', $host);
return strtolower($host);
}
/**
* Check if a project matches the filter
*/
protected function matchesProject(string $url, string $name, string $filter): bool
{
$filter = strtolower($filter);
return str_contains(strtolower($url), $filter) ||
str_contains(strtolower($name), $filter);
}
/**
* Get sync statistics
*/
public function getStats(): array
{
return $this->stats;
}
/**
* Reset sync state for a specific table/source or all
*/
public function resetSyncState(?string $tableName = null, ?string $sourceUrl = null): int
{
$query = DB::table('sync_states');
if ($tableName) {
$query->where('table_name', $tableName);
}
if ($sourceUrl) {
$query->where('source_url', $sourceUrl);
}
return $query->delete();
}
/**
* Get all sync states, optionally filtered by table
*/
public function getSyncStates(?string $tableName = null): array
{
$query = DB::table('sync_states');
if ($tableName) {
$query->where('table_name', $tableName);
}
return $query->orderBy('table_name')->orderBy('source_url')->get()->toArray();
}
}
+145
View File
@@ -0,0 +1,145 @@
<?php
namespace App\Services;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use App\DevExtreme\FilterHelper; // Mevcut FilterHelper'ı kullanacağız
class QueryBuilderService
{
/**
* DevExtreme LoadOptions formatındaki parametreleri sorguya uygular.
*
* @param Builder|\Illuminate\Database\Query\Builder $query
* @param array|string|null $filter JSON string veya array
* @param array|string|null $sort JSON string veya array
* @param int|null $skip
* @param int|null $take
* @return Builder|\Illuminate\Database\Query\Builder
*/
public function apply($query, $filter = null, $sort = null, $skip = null, $take = null)
{
// JSON string gelirse array'e çevir
if (is_string($filter)) {
$filter = json_decode($filter, true);
}
if (is_string($sort)) {
$sort = json_decode($sort, true);
}
// Filtreleme (Mevcut DevExtreme/FilterHelper veya özel mantık)
if (!empty($filter)) {
// Eğer projenizdeki FilterHelper statik ise doğrudan çağırabiliriz.
// Değilse burada custom bir recursive parser yazıyorum.
$this->applyFilterRecursive($query, $filter);
}
// Sıralama
if (!empty($sort)) {
foreach ($sort as $s) {
$selector = $s['selector'];
$desc = (isset($s['desc']) && $s['desc'] === true) ? 'desc' : 'asc';
$query->orderBy($selector, $desc);
}
}
// Sayfalama
if (!is_null($skip)) {
$query->skip($skip);
}
if (!is_null($take)) {
$query->take($take);
}
return $query;
}
/**
* Recursive Filter Parser
* DevExtreme formatı: ["field", "=", "value"] veya ["and", ["f1", "=", "v1"], ["f2", "=", "v2"]]
*/
protected function applyFilterRecursive($query, $filter)
{
// Grup koşulu mu? İlk eleman "and" veya "or" ise grup olarak işle (2+ filtre birleşimi)
$firstIsLogic = isset($filter[0]) && is_string($filter[0])
&& in_array(strtolower($filter[0]), ['and', 'or'], true);
if ($firstIsLogic && count($filter) >= 2) {
$this->applyFilterGroup($query, $filter);
return;
}
// Tekil koşul: ["field", "op", "value"] -> tam 3 eleman, ikinci operatör
if (isset($filter[0], $filter[1], $filter[2]) && count($filter) === 3
&& is_string($filter[0]) && is_string($filter[1])) {
$this->addCondition($query, $filter[0], $filter[1], $filter[2]);
return;
}
// Eski format: [ [..], "and", [..] ] (operatör ortada)
$this->applyFilterGroup($query, $filter);
}
/**
* Grup filtre: ["and", cond1, cond2] veya [cond1, "and", cond2]
*/
protected function applyFilterGroup($query, $filter)
{
$query->where(function ($subQuery) use ($filter) {
$logic = 'and'; // Varsayılan bağlaç
foreach ($filter as $item) {
if (is_string($item)) {
// "and" veya "or" bağlacı
$logic = strtolower($item);
continue;
}
if (is_array($item)) {
if ($logic === 'or') {
$subQuery->orWhere(function ($q) use ($item) {
$this->applyFilterRecursive($q, $item);
});
} else {
$subQuery->where(function ($q) use ($item) {
$this->applyFilterRecursive($q, $item);
});
}
}
}
});
}
protected function addCondition($query, $field, $operator, $value)
{
switch ($operator) {
case 'contains':
$query->where($field, 'like', '%' . $value . '%');
break;
case 'notcontains':
$query->where($field, 'not like', '%' . $value . '%');
break;
case 'startswith':
$query->where($field, 'like', $value . '%');
break;
case 'endswith':
$query->where($field, 'like', '%' . $value);
break;
case '=':
case '<>':
case '>':
case '>=':
case '<':
case '<=':
$query->where($field, $operator, $value);
break;
default:
// Desteklenmeyen operatörler için varsayılan davranış
$query->where($field, '=', $value);
break;
}
}
}
@@ -0,0 +1,207 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use App\Services\RegisterCreator\ExcelRowHandler;
abstract class AbstractDocumentProcessor
{
protected array $weldLogData;
protected array $document;
protected Worksheet $sheet;
protected int $currentRow;
protected string $registerColumnBased;
protected array $settings;
protected string $logFilePath;
protected ExcelRowHandler $excelRowHandler;
protected int $documentsAdded = 0; // Counter for documents added to Excel
/**
* Process the document
*/
abstract public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int;
/**
* Initialize processor
*/
protected function initialize(
array $weldLogData,
array $document,
Worksheet $sheet,
int $currentRow,
array $settings
): void {
$this->weldLogData = $weldLogData;
$this->document = $document;
$this->sheet = $sheet;
$this->currentRow = $currentRow;
$this->settings = $settings;
$this->registerColumnBased = $settings['register_column_based'] ?? 'line_number';
$this->excelRowHandler = new ExcelRowHandler();
$this->documentsAdded = 0; // Reset counter for each document type
// Set documents array for dynamic type order mapping
if (isset($settings['all_documents']) && is_array($settings['all_documents'])) {
$this->excelRowHandler->setDocuments($settings['all_documents']);
Log::debug("ExcelRowHandler initialized with documents", [
'processor' => class_basename($this),
'documents_count' => count($settings['all_documents'])
]);
}
// Setup log file path
$lineIdentifier = $weldLogData[$this->registerColumnBased] ?? 'unknown';
$basePath = $settings['path'] ?? '';
$this->logFilePath = "{$basePath}/{$lineIdentifier}/log.txt";
}
/**
* Normalize search term for file matching
*/
protected function normalizeSearchTerm(string $term): string
{
$term = str_replace("/", "*", $term);
$term = str_replace(" ", "*", $term);
$term = str_replace("\\", "*", $term);
return $term;
}
/**
* Search for files using glob pattern
*/
protected function searchFiles(string $pattern): array
{
Log::debug('Searching for files', ['pattern' => $pattern]);
$files = glob($pattern);
if ($files === false) {
$files = [];
}
Log::debug('Files found', ['count' => count($files)]);
return $files;
}
/**
* Get full folder path for current line
*/
protected function getFullFolder(): string
{
$path = $this->settings['path'] ?? '';
$lineIdentifier = $this->weldLogData[$this->registerColumnBased] ?? 'unknown';
$basePath = "storage/documents/{$path}";
return "{$basePath}/{$lineIdentifier}/";
}
/**
* Write to log file
* Note: This method is now silent to keep log files clean
* Only errors and NOT FOUND messages are logged
*/
protected function log(string $message, string $level = 'info'): void
{
// Don't write info messages to log file anymore - keep it clean
// Only errors will be logged via ExcelRowHandler
// Only log to Laravel debug for development purposes
Log::debug($message, [
'processor' => class_basename($this),
'line' => $this->weldLogData[$this->registerColumnBased] ?? 'unknown'
]);
}
/**
* Get contractor name
*/
protected function getContractor(): string
{
$subcontractors = \Cache::get("subcontractors", []);
$contractorKey = $this->weldLogData['contractor'] ?? '';
if (isset($subcontractors[$contractorKey])) {
return $subcontractors[$contractorKey]->company_name_ru ?? $contractorKey;
}
return $contractorKey;
}
/**
* Add row to Excel using ExcelRowHandler service
*
* @param array $search File paths to search for
* @param string $lineNumber Line number or identifier
* @param string $documentDate Document date
*
* @return int Next row position
*/
protected function addRowToExcel(
array $search,
string $lineNumber,
string $documentDate
): int {
$fullFolder = $this->getFullFolder();
$override = $this->settings['override'] ?? false;
// Calculate current row number based on starting row + documents added so far
$startRowNo = $this->settings['row_no'] ?? 1;
$currentRowNo = $startRowNo + $this->documentsAdded;
Log::info("🟢 AbstractDocumentProcessor->addRowToExcel() CALLED", [
'processor' => class_basename($this),
'row_no' => $currentRowNo,
'start_row_no' => $startRowNo,
'documents_added' => $this->documentsAdded,
'doc_type' => $this->document['type'] ?? 'N/A',
'doc_title2' => $this->document['title2'] ?? 'N/A',
'line' => $lineNumber,
'search_files' => count($search)
]);
$oldCurrentRow = $this->currentRow;
// Ensure the actual line identifier is available in the document array
// This allows processors (like TemplateProcessor) to override $lineNumber for Excel
// while still preserving the real line number for filename generation.
$this->document['real_line_identifier'] = $this->weldLogData[$this->registerColumnBased] ?? $lineNumber;
$newRow = $this->excelRowHandler->addRow(
$search,
$this->document,
$fullFolder,
$lineNumber,
$documentDate,
$currentRowNo,
$this->sheet,
$this->currentRow,
$override
);
// If a row was added, increment the documents counter and update current row
if ($newRow > $oldCurrentRow) {
$this->documentsAdded++;
$this->currentRow = $newRow; // ← UPDATE CURRENT ROW!
}
return $newRow;
}
/**
* Get the number of documents added during processing
*/
public function getDocumentsAdded(): int
{
return $this->documentsAdded;
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DocumentProcedureProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$procedures = [];
if (!empty($document['title2'])) {
$procedures = array_map('trim', explode(",", $document['title2']));
}
// Prepare procedures with dates for sorting
$proceduresWithDates = [];
foreach ($procedures as $procedureNo) {
$procedure = db("document_procedures")->where("document_no", $procedureNo)->first();
if (!$procedure) continue;
$proceduresWithDates[] = [
'procedure' => $procedure,
'date' => $procedure->publish_date ?? ''
];
}
// Sort by publish_date (oldest first) - reverse insertion order
usort($proceduresWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
foreach ($proceduresWithDates as $procData) {
$procedure = $procData['procedure'];
$normalized = $this->normalizeSearchTerm($procedure->document_no);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $procedure->document_no;
$this->document = $document;
$documentDate = $procData['date'];
$newRow = $this->addRowToExcel($search, $procedure->document_no, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use Exception;
class DocumentProcessorFactory
{
/**
* Create document processor instance based on type
*/
public static function make(string $type, ?array $document = null): AbstractDocumentProcessor
{
// Check if this is a dynamic document
if ($type === 'dynamic' || ($document && isset($document['is_dynamic']) && $document['is_dynamic'])) {
return new DynamicMappingProcessor();
}
return match($type) {
'qa' => new QaDocumentProcessor(),
'wdb' => new WdbDocumentProcessor(),
'wps_naks_technology' => new WpsNaksTechnologyProcessor(),
'naks_consumables_certificate' => new NaksConsumablesCertificateProcessor(),
'naks_consumables_inspection_test_report' => new NaksConsumablesInspectionProcessor(),
'drawings' => new DrawingsProcessor(),
'materials' => new MaterialsProcessor(),
'incoming_control_materials' => new IncomingControlProcessor(),
'template' => new TemplateProcessor(),
'prikaz' => new PrikazProcessor(),
'document-procedure' => new DocumentProcedureProcessor(),
'dynamic' => new DynamicMappingProcessor(),
default => new GenericDocumentProcessor(),
};
}
/**
* Check if processor exists for given type
*/
public static function exists(string $type): bool
{
try {
self::make($type);
return true;
} catch (\Throwable $th) {
return false;
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DrawingsProcessor extends AbstractDocumentProcessor
{
/**
* Process drawings documents
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing Drawings");
$lineIdentifier = $weldLogData[$this->registerColumnBased];
$normalized = $this->normalizeSearchTerm($lineIdentifier);
$search = $this->searchFiles("{$document['path']}/*{$normalized}*.pdf");
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
$documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
// Use the new ExcelRowHandler service method
$newRow = $this->addRowToExcel(
$search,
$lineIdentifier,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
return $currentRow;
}
}
@@ -0,0 +1,233 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class DynamicMappingProcessor extends AbstractDocumentProcessor
{
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing dynamic mapped document: {$document['title2']}");
// Check if this document has SQL query configuration
if (empty($document['sql_query'])) {
$this->log("No SQL query found for dynamic document", 'warning');
return $currentRow;
}
return $this->processSqlBasedMapping($currentRow);
}
private function processSqlBasedMapping(int &$currentRow): int
{
try {
// Execute SQL query with placeholder replacement
$results = $this->executeSqlQuery();
if (empty($results)) {
$this->log("SQL query returned no results", 'warning');
return $currentRow;
}
$this->log("Found " . count($results) . " records from SQL query");
// Process each result
foreach ($results as $result) {
try {
$identifier = $result['identifier'];
$recordData = $result['data'];
$documentDate = $result['document_date'];
// Generate row title using pattern
$rowTitle = $this->generateRowTitle($recordData);
// Generate file search pattern
$searchPattern = $this->generateFileSearchPattern($recordData);
$fullPath = "storage/documents/{$this->document['path']}/{$searchPattern}";
// Search for files
$files = $this->searchFiles($fullPath);
if (empty($files)) {
$this->log("No files found for: {$identifier} (pattern: {$searchPattern})", 'warning');
continue;
}
$this->log("✓ Found " . count($files) . " files for: {$identifier}");
// Update document titles
$this->document['title2'] = $identifier;
$this->document['title4'] = $rowTitle;
// Add row to Excel
$newRow = $this->addRowToExcel(
$files,
$identifier,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
$this->log("✓ Processed: {$identifier} (from SQL query)");
} catch (\Throwable $th) {
$this->log("Error processing SQL result: {$th->getMessage()}", 'error');
Log::error("Dynamic mapping result processing error", [
'identifier' => $result['identifier'] ?? 'unknown',
'error' => $th->getMessage(),
'trace' => $th->getTraceAsString()
]);
continue;
}
}
return $currentRow;
} catch (\Throwable $th) {
$this->log("SQL query execution error: {$th->getMessage()}", 'error');
Log::error("Dynamic mapping SQL execution error", [
'sql_query' => $this->document['sql_query'] ?? 'N/A',
'error' => $th->getMessage(),
'trace' => $th->getTraceAsString()
]);
throw $th;
}
}
/**
* Execute SQL query with placeholder replacement
*/
private function executeSqlQuery(): array
{
$sqlQuery = $this->document['sql_query'] ?? '';
if (empty($sqlQuery)) {
throw new \Exception("No SQL query defined for dynamic document");
}
// Replace placeholders
$executedQuery = $this->replacePlaceholders($sqlQuery);
Log::info("Executing dynamic SQL query", [
'original_query' => $sqlQuery,
'executed_query' => $executedQuery
]);
try {
$startTime = microtime(true);
$results = DB::select($executedQuery);
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
Log::info("Dynamic SQL query executed successfully", [
'record_count' => count($results),
'execution_time' => $executionTime . 'ms'
]);
return $this->formatResults($results);
} catch (\Throwable $th) {
Log::error("Dynamic SQL query execution failed", [
'query' => $executedQuery,
'error' => $th->getMessage()
]);
throw $th;
}
}
/**
* Replace :placeholder with actual values from register data
*/
private function replacePlaceholders(string $query): string
{
$result = $query;
// Find all :placeholder patterns
preg_match_all('/:(\w+)/', $query, $matches);
foreach ($matches[1] as $placeholder) {
$value = $this->weldLogData[$placeholder] ?? null;
if ($value !== null) {
// Escape value for SQL
$escapedValue = DB::getPdo()->quote($value);
$result = str_replace(":{$placeholder}", $escapedValue, $result);
} else {
Log::warning("Placeholder value not found", [
'placeholder' => $placeholder,
'available_fields' => array_keys($this->weldLogData)
]);
}
}
return $result;
}
/**
* Format SQL results to standard structure
*/
private function formatResults(array $results): array
{
$identifierField = $this->document['identifier_field'] ?? 'identifier';
$dateField = $this->document['date_field'] ?? 'document_date';
$formatted = [];
foreach ($results as $result) {
$recordArray = (array) $result;
$identifier = $recordArray[$identifierField] ?? $recordArray['id'] ?? 'unknown';
$date = $recordArray[$dateField] ?? '';
$formatted[] = [
'identifier' => $identifier,
'document_date' => $date,
'data' => $recordArray
];
}
return $formatted;
}
/**
* Generate file search pattern with field replacements
*/
private function generateFileSearchPattern(array $recordData): string
{
$pattern = $this->document['file_search_pattern'] ?? '*{identifier}*.pdf';
// Replace {field_name} with actual values
foreach ($recordData as $key => $value) {
$pattern = str_replace("{{$key}}", $value, $pattern);
}
return $pattern;
}
/**
* Generate row title with pattern
*/
private function generateRowTitle(array $recordData): string
{
$pattern = $this->document['title4_pattern'] ?? '{identifier}';
// Replace {field_name} with actual values
foreach ($recordData as $key => $value) {
$pattern = str_replace("{{$key}}", $value, $pattern);
}
return $pattern;
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/**
* Generic processor for document types that don't have specific processors
*/
class GenericDocumentProcessor extends AbstractDocumentProcessor
{
/**
* Process generic document
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing generic document: {$document['type']}");
$lineIdentifier = $weldLogData[$this->registerColumnBased];
$normalized = $this->normalizeSearchTerm($lineIdentifier);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
$documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel(
$search,
$lineIdentifier,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
return $currentRow;
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
class IncomingControlProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$project = $weldLogData['line_number'] ?? '';
if (empty($project)) return $currentRow;
$incomingControls = db("incoming_controls")->where("project", $project)->groupBy("certificate_no", "description_ru")->get()->toArray();
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
// Sort by certificate_date (oldest first) - reverse insertion order
usort($incomingControls, function($a, $b) use ($placeholderReplacer, $weldLogData) {
$a = (object) $a;
$b = (object) $b;
$dateA = $a->certificate_date ?? $a->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$dateB = $b->certificate_date ?? $b->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$timestampA = !empty($dateA) ? strtotime($dateA) : 0;
$timestampB = !empty($dateB) ? strtotime($dateB) : 0;
return $timestampA <=> $timestampB;
});
foreach ($incomingControls as $control) {
$control = (object) $control;
if (empty($control->certificate_no)) continue;
$normalized = $this->normalizeSearchTerm($control->certificate_no);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $control->description_ru;
$document['incoming_control_description'] = $control->description_ru;
$document['title3'] = $control->certificate_no;
$this->document = $document;
$documentDate = $control->certificate_date ?? $control->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel($search, $control->certificate_no, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,113 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class MaterialsProcessor extends AbstractDocumentProcessor
{
/**
* Process materials documents
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing Materials");
// Get all certificates from same line
$allRecords = db("weld_logs")
->where($this->registerColumnBased, $weldLogData['line_number'])
->get();
$uniqueCertificates = [];
foreach ($allRecords as $record) {
if (!empty($record->certificate_number_of_1) && !in_array($record->certificate_number_of_1, $uniqueCertificates)) {
$uniqueCertificates[] = $record->certificate_number_of_1;
}
if (!empty($record->certificate_number_of_2) && !in_array($record->certificate_number_of_2, $uniqueCertificates)) {
$uniqueCertificates[] = $record->certificate_number_of_2;
}
}
$this->log("Found " . count($uniqueCertificates) . " unique certificates");
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
// Prepare certificates with dates for sorting
$certificatesWithDates = [];
foreach ($uniqueCertificates as $certificateNumber) {
$incomingControl = db("incoming_controls")
->where("certificate_no", $certificateNumber)
->first();
$documentDate = $incomingControl->certificate_date ?? $incomingControl->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$certificatesWithDates[] = [
'certificate_no' => $certificateNumber,
'incoming_control' => $incomingControl,
'date' => $documentDate
];
}
// Sort by date (oldest first) - reverse insertion order
usort($certificatesWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Certificates sorted by date (oldest first for reverse insertion)");
foreach ($certificatesWithDates as $certData) {
$certificateNumber = $certData['certificate_no'];
$incomingControl = $certData['incoming_control'];
$documentDate = $certData['date'];
try {
if ($incomingControl) {
$document['title2'] = $incomingControl->description_ru;
$document['incoming_control_description'] = $incomingControl->description_ru;
$document['file_name'] = $certificateNumber;
} else {
$document['title2'] = "-";
$document['incoming_control_description'] = "-";
$document['file_name'] = $certificateNumber;
}
$normalized = $this->normalizeSearchTerm($certificateNumber);
$search = $this->searchFiles("{$document['path']}/*{$normalized}*.pdf");
// Update document reference for current iteration
$this->document = $document;
$newRow = $this->addRowToExcel(
$search,
$certificateNumber,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing certificate: {$certificateNumber} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class NaksConsumablesCertificateProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$certificates = array_filter([
$weldLogData['welding_materials_1_certificate_no'] ?? '',
$weldLogData['welding_materials_2_certificate_no'] ?? '',
$weldLogData['welding_materials_3_certificate_no'] ?? ''
]);
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
foreach ($certificates as $certNo) {
$normalized = $this->normalizeSearchTerm($certNo);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $certNo;
$this->document = $document;
$documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel($search, $certNo, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class NaksConsumablesInspectionProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$lotNumbers = array_filter([
$weldLogData['welding_materials_1_lot_no'] ?? '',
$weldLogData['welding_materials_2_lot_no'] ?? '',
$weldLogData['welding_materials_3_lot_no'] ?? ''
]);
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
foreach ($lotNumbers as $lotNo) {
$normalized = $this->normalizeSearchTerm($lotNo);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $lotNo;
$this->document = $document;
$documentDate = $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel($search, $lotNo, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class PrikazProcessor extends AbstractDocumentProcessor
{
public function process(array $weldLogData, array $document, Worksheet $sheet, int &$currentRow, array $settings): int
{
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$project = $weldLogData['project'] ?? '';
if (empty($project)) return $currentRow;
$workPermitDocs = db("work_permit_documents")->where("zone", "like", "%" . $project . "%")->get()->toArray();
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
// Sort by issue_date (oldest first) - reverse insertion order
usort($workPermitDocs, function($a, $b) use ($placeholderReplacer, $weldLogData) {
$a = (object) $a;
$b = (object) $b;
$dateA = $a->issue_date ?? $a->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$dateB = $b->issue_date ?? $b->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$timestampA = !empty($dateA) ? strtotime($dateA) : 0;
$timestampB = !empty($dateB) ? strtotime($dateB) : 0;
return $timestampA <=> $timestampB;
});
foreach ($workPermitDocs as $doc) {
$doc = (object) $doc;
$normalized = $this->normalizeSearchTerm($doc->document_number);
$search = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized}*.pdf");
$document['title2'] = $doc->document_number;
$document['title3'] = $doc->title ?? $doc->document_number;
$this->document = $document;
$documentDate = $doc->issue_date ?? $doc->created_at ?? $placeholderReplacer->getLatestDate($weldLogData, $this->registerColumnBased);
$newRow = $this->addRowToExcel($search, $doc->document_number, $documentDate);
if ($newRow > $currentRow) { $currentRow = $newRow; }
}
return $currentRow;
}
}
@@ -0,0 +1,209 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
class QaDocumentProcessor extends AbstractDocumentProcessor
{
/**
* Process QA type documents (NDT reports, procedures, etc.)
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing QA document: {$document['path']}");
// Get all joints with same line number
$allJoints = apply_welded_filter(
db("weld_logs")->where(
$this->registerColumnBased,
$this->weldLogData[$this->registerColumnBased]
)
)->get();
$this->log("Found {$allJoints->count()} joints for processing");
// Check if this is a procedure document
if (strpos($document['path'], "Procedure") !== false) {
return $this->processProcedureDocuments($currentRow);
}
// Process NDT reports
return $this->processNdtReports($allJoints, $currentRow);
}
/**
* Process NDT (Non-Destructive Testing) reports
*/
private function processNdtReports($allJoints, int &$currentRow): int
{
$allReports = [];
$uniqueReports = [];
$duplicateCount = 0;
// LAYER 1: Get reports from weld_logs (synced data)
$this->log("Layer 1: Extracting reports from weld_logs");
foreach ($allJoints as $joint) {
$jointArray = (array) $joint;
$logTypes = array_keys(log_test_types());
foreach ($logTypes as $logType) {
try {
if (strpos(strtolower($this->document['path']), $logType) !== false) {
$reportNoPrefix = $logType . "_report";
if ($logType == "pmi") {
$reportNoPrefix = "no_of_testing_report";
}
$reportNo = $jointArray[$reportNoPrefix] ?? '';
if (!empty($reportNo) && !in_array($reportNo, $uniqueReports)) {
$uniqueReports[] = $reportNo;
$allReports[] = [
'report_no' => $reportNo,
'document_date' => $jointArray[$logType . '_test_date'] ?? '',
'log_type' => $logType,
'weld_log_array' => $jointArray
];
$this->log("Layer 1 - Report added: {$reportNo} ({$logType})");
} else if (!empty($reportNo)) {
$duplicateCount++;
}
}
} catch (\Throwable $th) {
Log::error("Error processing test type: {$logType}", [
'error' => $th->getMessage()
]);
continue;
}
}
}
$this->log("Layer 1 complete: " . count($allReports) . " unique reports from weld_logs");
// Sort reports by joint number (A-Z)
// Note: Excel rows are inserted in reverse (insertNewRowBefore),
// so A-Z order becomes Z-A in Excel (correct order)
usort($allReports, function($a, $b) {
$jointNoA = $a['weld_log_array']['no_of_the_joint_as_per_as_built_survey'] ?? '';
$jointNoB = $b['weld_log_array']['no_of_the_joint_as_per_as_built_survey'] ?? '';
// Check if joint numbers are empty - throw error
if (empty($jointNoA)) {
throw new \Exception("Joint number is empty for report: {$a['report_no']}");
}
if (empty($jointNoB)) {
throw new \Exception("Joint number is empty for report: {$b['report_no']}");
}
// Sort by joint number (natural/numeric order: 1, 2, 3, 10, 11)
return strnatcmp($jointNoA, $jointNoB);
});
$this->log("Total " . count($allReports) . " unique reports sorted by joint number (A-Z)");
$this->log("Duplicate reports skipped: {$duplicateCount}");
// Process sorted reports
foreach ($allReports as $reportData) {
try {
$normalizedReportNo = $this->normalizeSearchTerm($reportData['report_no']);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalizedReportNo}*.pdf");
$this->document['title2'] = $reportData['report_no'];
// Get translation safely - handle array return
$translationKey = $reportData['log_type'] . "_register_title";
$translatedValue = e2($translationKey);
// Ensure we have a string, not an array
if (is_array($translatedValue)) {
$this->document['title4'] = $translationKey; // Use key as fallback
Log::warning("Translation returned array for key: {$translationKey}, using key as fallback");
} else {
$this->document['title4'] = (string) $translatedValue;
}
$lineNumber = $reportData['report_no'];
$newRow = $this->addRowToExcel(
$search,
$lineNumber,
$reportData['document_date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing report: {$reportData['report_no']} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process procedure documents
*/
private function processProcedureDocuments(int &$currentRow): int
{
$this->log("Processing Document Procedure");
$documentProcedures = [];
if (!empty($this->document['title2'])) {
$customCertificates = array_map('trim', explode(",", $this->document['title2']));
$documentProcedures = $customCertificates;
}
foreach ($documentProcedures as $procedureNo) {
try {
$procedure = db("document_procedures")
->where("document_no", $procedureNo)
->first();
if ($procedure) {
$this->log("Procedure found: {$procedure->document_no}");
$normalizedDocNo = $this->normalizeSearchTerm($procedure->document_no);
$searchPath = "storage/documents/{$this->document['path']}/*{$normalizedDocNo}*.pdf";
$search = $this->searchFiles($searchPath);
$documentDate = $procedure->publish_date ?? '';
$lineNumber = $procedure->document_no;
$this->document['title2'] = $procedure->document_no;
$newRow = $this->addRowToExcel(
$search,
$lineNumber,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} else {
$this->log("Procedure not found: {$procedureNo}", 'warning');
}
} catch (\Throwable $th) {
$this->log("Error processing procedure: {$procedureNo} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
}
@@ -0,0 +1,152 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class TemplateProcessor extends AbstractDocumentProcessor
{
/**
* Process template type documents
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing Template document: {$document['path']}");
$lineIdentifier = $weldLogData[$this->registerColumnBased];
// Search by line number
$normalized1 = $this->normalizeSearchTerm($lineIdentifier);
$search1 = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized1}*.pdf");
// Search by test package numbers
$testPackageNumbers = db("weld_logs")
->where($this->registerColumnBased, $lineIdentifier)
->whereNotNull("test_package_no")
->where("test_package_no", "!=", "")
->distinct()
->pluck("test_package_no")
->toArray();
$this->log("Found " . count($testPackageNumbers) . " test package numbers for search");
$search2 = [];
foreach ($testPackageNumbers as $testPackageNo) {
$normalized2 = $this->normalizeSearchTerm($testPackageNo);
$result = $this->searchFiles("storage/documents/{$document['path']}/*{$normalized2}*.pdf");
if (!empty($result)) {
$search2 = array_merge($search2, $result);
}
}
// Merge and remove duplicates
$allFiles = array_unique(array_merge($search1, $search2));
$this->log("Total files found: " . count($allFiles));
if (empty($allFiles)) {
$this->log("No template files found", 'warning');
return $currentRow;
}
// Sort files by date extracted from filename (oldest first)
// Note: Excel rows are inserted in reverse (insertNewRowBefore),
// so oldest first becomes newest last in Excel (correct order)
$self = $this;
usort($allFiles, function($a, $b) use ($self) {
$dateA = strtotime($self->extractDateFromFilename(basename($a))) ?: 0;
$dateB = strtotime($self->extractDateFromFilename(basename($b))) ?: 0;
// Sort ascending (oldest first)
return $dateA <=> $dateB;
});
$this->log("Files sorted by date (oldest first for reverse insertion)");
foreach ($allFiles as $filePath) {
try {
$fileName = basename($filePath);
$documentDate = $this->extractDateFromFilename($fileName);
// Process single file
$templateFileName = str_replace(".pdf", "", $fileName);
$lineNumber = $templateFileName;
$document['title2'] = $templateFileName;
// Remove date from filename
$templateFileName = preg_replace('/_\d{2}\.\d{2}\.\d{4}/', '', $templateFileName);
$templateFileName = preg_replace('/_\d{4}-\d{2}-\d{2}/', '', $templateFileName);
$document['templateFileName'] = $templateFileName;
// Special handling for Incoming_Control
if ($document['id'] == "Incoming_Control") {
$incomingControl = db("incoming_controls")
->where("certificate_no", $lineNumber)
->first();
if ($incomingControl) {
$document['title2'] = $incomingControl->description_ru;
$this->log("Incoming Control found: {$document['title2']}");
}
}
// Update document reference for current iteration
$this->document = $document;
$newRow = $this->addRowToExcel(
[$filePath],
$lineNumber,
$documentDate
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing template file: " . basename($filePath) . " - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Extract date from filename
*/
private function extractDateFromFilename(string $fileName): string
{
// Try YYYY-MM-DD format
if (preg_match('/(\d{4}-\d{2}-\d{2})/', $fileName, $matches)) {
return $matches[1];
}
// Try DD.MM.YYYY format
if (preg_match('/(\d{2}\.\d{2}\.\d{4})/', $fileName, $matches)) {
try {
return Carbon::createFromFormat('d.m.Y', $matches[1])->format('Y-m-d');
} catch (\Throwable $th) {
Log::debug("Could not parse date: {$matches[1]}");
}
}
// Default to latest date
$placeholderReplacer = new \App\Services\RegisterCreator\PlaceholderReplacer();
return $placeholderReplacer->getLatestDate($this->weldLogData, $this->registerColumnBased);
}
}
@@ -0,0 +1,459 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
class WdbDocumentProcessor extends AbstractDocumentProcessor
{
/**
* Process WDB (Welding Database) type documents
*/
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$this->log("Processing WDB document: {$document['path']}");
$path = $document['path'];
$wpsData = $settings['wps_data'] ?? null;
$startRowNo = $settings['row_no'] ?? 1;
// Process based on sub-type
if (strpos($path, "Naks Technology") !== false) {
return $this->processNaksTechnology($currentRow, $wpsData, $startRowNo);
}
if (strpos($path, "Naks_Welder") !== false) {
return $this->processNaksWelder($currentRow, $startRowNo);
}
if (strpos($path, "Naks_Equipments") !== false) {
return $this->processNaksEquipments($currentRow, $startRowNo);
}
if (strpos($path, "Naks_Consumables") !== false) {
return $this->processNaksConsumables($currentRow, $startRowNo);
}
if (strpos($path, "Welding Experts") !== false) {
return $this->processWeldingExperts($currentRow, $startRowNo);
}
if (strpos($path, "WPQ") !== false) {
return $this->processWpq($currentRow, $startRowNo);
}
if (strpos($path, "WPS") !== false) {
return $this->processWps($currentRow, $wpsData, $startRowNo);
}
if (strpos($path, "PQR") !== false) {
return $this->processPqr($currentRow, $wpsData, $startRowNo);
}
return $currentRow;
}
/**
* Process Naks Technology certificates
*/
private function processNaksTechnology(int &$currentRow, $wpsData, int $startRowNo = 1): int
{
if (!$wpsData) {
$this->log("WPS data not found, skipping Naks Technology", 'warning');
return $currentRow;
}
$naksCertificates = explode(" + ", $wpsData->naks_certificate_no);
$this->log("Processing " . count($naksCertificates) . " Naks Technology certificates");
foreach ($naksCertificates as $naksCertificate) {
try {
$normalized = $this->normalizeSearchTerm($naksCertificate);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title3'] = $naksCertificate;
$newRow = $this->addRowToExcel(
$search,
$naksCertificate,
$wpsData->date ?? ''
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing Naks certificate: {$naksCertificate} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process Naks Welder certificates
*/
private function processNaksWelder(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing Naks Welders");
$weldersData = apply_welded_filter(
db("weld_logs")
->select("certificate_no_1", "certificate_no_2")
->where($this->registerColumnBased, $this->weldLogData[$this->registerColumnBased])
)->get();
$naksWelders = [];
foreach ($weldersData as $welderData) {
if (!empty($welderData->certificate_no_1) && !in_array($welderData->certificate_no_1, $naksWelders)) {
$naksWelders[] = $welderData->certificate_no_1;
}
if (!empty($welderData->certificate_no_2) && !in_array($welderData->certificate_no_2, $naksWelders)) {
$naksWelders[] = $welderData->certificate_no_2;
}
}
$this->log("Found " . count($naksWelders) . " welder certificates");
// Prepare welders with their dates for sorting
$weldersWithDates = [];
foreach ($naksWelders as $naksWelder) {
$welderInfo = db("naks_welders")->where("naks_certificate_no", $naksWelder)->first();
$weldersWithDates[] = [
'certificate_no' => $naksWelder,
'date' => $welderInfo->period_of_validity ?? ''
];
}
// Sort by date (oldest first) - reverse insertion order
usort($weldersWithDates, function($a, $b) {
$dateA = !empty($a['date']) ? strtotime($a['date']) : 0;
$dateB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $dateA <=> $dateB;
});
$this->log("Welders sorted by date (oldest first for reverse insertion)");
foreach ($weldersWithDates as $welderData) {
try {
$naksWelder = $welderData['certificate_no'];
$normalized = $this->normalizeSearchTerm($naksWelder);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $naksWelder;
$newRow = $this->addRowToExcel(
$search,
$naksWelder,
$welderData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing welder: {$naksWelder} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process Naks Equipment certificates
*/
private function processNaksEquipments(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing Naks Equipments");
$certificates = [];
if (!empty($this->document['title2'])) {
$certificates = array_map('trim', explode(",", $this->document['title2']));
}
// Prepare equipments with dates for sorting
$equipmentsWithDates = [];
foreach ($certificates as $certificate) {
$equipmentInfo = db("welding_equipment")->where("attestation", $certificate)->first();
$equipmentsWithDates[] = [
'certificate' => $certificate,
'date' => $equipmentInfo->valid_until ?? ''
];
}
// Sort by valid_until date (oldest first) - reverse insertion order
usort($equipmentsWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Equipments sorted by date (oldest first for reverse insertion)");
foreach ($equipmentsWithDates as $equipData) {
try {
$certificate = $equipData['certificate'];
$normalized = $this->normalizeSearchTerm($certificate);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $certificate;
$this->document['title3'] = $certificate;
$newRow = $this->addRowToExcel(
$search,
$certificate,
$equipData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing equipment: {$certificate} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process Naks Consumables
*/
private function processNaksConsumables(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing Naks Consumables");
$consumables = [];
if (!empty($this->document['title2'])) {
$consumables = array_map('trim', explode(",", $this->document['title2']));
}
// Prepare consumables with dates for sorting
$consumablesWithDates = [];
foreach ($consumables as $consumable) {
$consumableInfo = db("naks_consumables")->where("naks_certificate_no", $consumable)->first();
$consumablesWithDates[] = [
'consumable' => $consumable,
'date' => $consumableInfo->certificate_date ?? ''
];
}
// Sort by certificate_date (oldest first) - reverse insertion order
usort($consumablesWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Consumables sorted by date (oldest first for reverse insertion)");
foreach ($consumablesWithDates as $consData) {
try {
$consumable = $consData['consumable'];
$normalized = $this->normalizeSearchTerm($consumable);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $consumable;
$newRow = $this->addRowToExcel(
$search,
$consumable,
$consData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing consumable: {$consumable} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process Welding Experts
*/
private function processWeldingExperts(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing Welding Experts");
$certificates = array_map('trim', explode(',', $this->document['title2'] ?? ''));
// Prepare experts with dates for sorting
$expertsWithDates = [];
foreach ($certificates as $certificate) {
$expertInfo = db("register_of_experts")->where("certificate_no", $certificate)->first();
$expertsWithDates[] = [
'certificate' => $certificate,
'date' => $expertInfo->expration_of_the_certificate ?? ''
];
}
// Sort by expiration date (oldest first) - reverse insertion order
usort($expertsWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Experts sorted by date (oldest first for reverse insertion)");
foreach ($expertsWithDates as $expertData) {
try {
$certificate = $expertData['certificate'];
$normalized = $this->normalizeSearchTerm($certificate);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $certificate;
$newRow = $this->addRowToExcel(
$search,
$certificate,
$expertData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing expert: {$certificate} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process WPQ documents
*/
private function processWpq(int &$currentRow, int $startRowNo = 1): int
{
$this->log("Processing WPQ");
$wpqs = db("welder_tests")->whereIn("wpq_document_no", [
$this->weldLogData['wpq_report_1'] ?? '',
$this->weldLogData['wpq_report_2'] ?? '',
])->get()->toArray();
$this->log("Found " . count($wpqs) . " WPQ documents");
// Sort by naks_validity date (oldest first) - reverse insertion order
usort($wpqs, function($a, $b) {
$a = (object) $a;
$b = (object) $b;
$timestampA = !empty($a->naks_validity) ? strtotime($a->naks_validity) : 0;
$timestampB = !empty($b->naks_validity) ? strtotime($b->naks_validity) : 0;
return $timestampA <=> $timestampB;
});
$this->log("WPQ documents sorted by date (oldest first for reverse insertion)");
foreach ($wpqs as $wpq) {
$wpq = (object) $wpq;
try {
$normalized = $this->normalizeSearchTerm($wpq->wpq_document_no);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title3'] = $wpq->wpq_document_no;
$newRow = $this->addRowToExcel(
$search,
$wpq->wpq_document_no,
$wpq->naks_validity ?? ''
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error processing WPQ: {$wpq->wpq_document_no} - {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
/**
* Process WPS documents
*/
private function processWps(int &$currentRow, $wpsData, int $startRowNo = 1): int
{
if (!$wpsData) {
$this->log("WPS data not found", 'warning');
return $currentRow;
}
$this->log("Processing WPS: {$wpsData->details}");
$normalized = $this->normalizeSearchTerm($wpsData->details);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title3'] = $wpsData->details;
$newRow = $this->addRowToExcel(
$search,
$wpsData->details,
$wpsData->date ?? ''
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
return $currentRow;
}
/**
* Process PQR documents
*/
private function processPqr(int &$currentRow, $wpsData, int $startRowNo = 1): int
{
if (!$wpsData) {
$this->log("WPS data not found, cannot process PQR", 'warning');
return $currentRow;
}
$this->log("Processing PQR");
$pqr = db("prosedure_qualification_records")->where("pqr_no", $wpsData->pqr_no)->first();
if ($pqr) {
$this->log("PQR found: {$pqr->pqr_no}");
$normalized = $this->normalizeSearchTerm($pqr->pqr_no);
$search = $this->searchFiles("storage/documents/{$this->document['path']}/*{$normalized}*.pdf");
$this->document['title2'] = $pqr->pqr_no;
$newRow = $this->addRowToExcel(
$search,
$pqr->pqr_no,
$pqr->approved_date ?? ''
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
}
return $currentRow;
}
}
@@ -0,0 +1,229 @@
<?php
namespace App\Services\RegisterCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
class WpsNaksTechnologyProcessor extends AbstractDocumentProcessor
{
/**
* Search files using the same algorithm as pdf-db-naks-technology-sync
* This algorithm handles zero-prefix patterns and multiple search strategies
*/
private function searchFilesWithFallback(string $basePath, string $certificateNo): array
{
$files = [];
// Parse certificate number to extract short_number and cert_no parts
// Format examples: АЦСТ-20-01934, АЦСТ-161-00050
$certParts = explode('-', $certificateNo);
if (count($certParts) >= 2) {
$shortNumber = $certParts[0]; // e.g., АЦСТ
$certNumber = isset($certParts[1]) ? $certParts[1] : '';
// If there's a third part, combine with second
if (count($certParts) >= 3) {
$certNumber = $certParts[1]; // e.g., 20 or 161
$thirdPart = $certParts[2]; // e.g., 01934 or 00050
// Generate certificate patterns with different zero prefixes
$certPatterns = [$thirdPart];
// Clean leading zeros and generate additional search patterns
$trimmedCertNo = ltrim($thirdPart, '0');
if ($trimmedCertNo != $thirdPart && $trimmedCertNo != '') {
$certPatterns[] = $trimmedCertNo;
// Add versions with different numbers of leading zeros
for ($i = 1; $i <= 5; $i++) {
$certPatterns[] = str_pad($trimmedCertNo, $i, '0', STR_PAD_LEFT);
}
}
// Try each pattern until we find a match
foreach ($certPatterns as $certPattern) {
// Format: short_number-certNumber-certPattern
$searchData = "*{$shortNumber}-{$certNumber}-{$certPattern}*";
$searchData = str_replace("/", "*", $searchData);
$searchData = str_replace(" ", "*", $searchData);
$searchData = str_replace("\\", "*", $searchData);
$path = "{$basePath}/{$searchData}";
$searchPDF = glob($path);
if (!empty($searchPDF)) {
Log::debug("Found with pattern", [
'pattern' => $searchData,
'files' => count($searchPDF)
]);
return $searchPDF;
}
}
} else {
// Format with only 2 parts: АЦСТ-161
$certPatterns = [$certNumber];
$trimmedCertNo = ltrim($certNumber, '0');
if ($trimmedCertNo != $certNumber && $trimmedCertNo != '') {
$certPatterns[] = $trimmedCertNo;
for ($i = 1; $i <= 5; $i++) {
$certPatterns[] = str_pad($trimmedCertNo, $i, '0', STR_PAD_LEFT);
}
}
foreach ($certPatterns as $certPattern) {
$searchData = "*{$shortNumber}-{$certPattern}*";
$searchData = str_replace("/", "*", $searchData);
$searchData = str_replace(" ", "*", $searchData);
$searchData = str_replace("\\", "*", $searchData);
$path = "{$basePath}/{$searchData}";
$searchPDF = glob($path);
if (!empty($searchPDF)) {
Log::debug("Found with 2-part pattern", [
'pattern' => $searchData,
'files' => count($searchPDF)
]);
return $searchPDF;
}
}
}
// Fallback: Try just the short_number
$searchData = "*{$shortNumber}*";
$searchData = str_replace("/", "*", $searchData);
$searchData = str_replace(" ", "*", $searchData);
$searchData = str_replace("\\", "*", $searchData);
$path = "{$basePath}/{$searchData}";
$searchPDF = glob($path);
if (!empty($searchPDF)) {
Log::debug("Found with short_number fallback", [
'pattern' => $searchData,
'files' => count($searchPDF)
]);
return $searchPDF;
}
}
// Final fallback: Try exact certificate number
$searchData = "*{$certificateNo}*";
$searchData = str_replace("/", "*", $searchData);
$searchData = str_replace(" ", "*", $searchData);
$searchData = str_replace("\\", "*", $searchData);
$path = "{$basePath}/{$searchData}";
$searchPDF = glob($path);
if (!empty($searchPDF)) {
Log::debug("Found with exact match", [
'pattern' => $searchData,
'files' => count($searchPDF)
]);
return $searchPDF;
}
Log::warning("No files found for certificate", [
'certificate' => $certificateNo,
'base_path' => $basePath
]);
return $files;
}
public function process(
array $weldLogData,
array $document,
Worksheet $sheet,
int &$currentRow,
array $settings
): int {
$this->initialize($weldLogData, $document, $sheet, $currentRow, $settings);
$wpsData = $settings['wps_data'] ?? null;
if (!$wpsData || empty($wpsData->naks_certificate_no)) {
$this->log("WPS Naks certificate not found", 'warning');
return $currentRow;
}
$naksCertificates = explode(" + ", $wpsData->naks_certificate_no);
// Prepare certificates with dates for sorting
$certificatesWithDates = [];
foreach ($naksCertificates as $naksCertificate) {
$certInfo = db("naks_certificates")->where("certificate_no", $naksCertificate)->first();
$certificatesWithDates[] = [
'certificate_no' => $naksCertificate,
'date' => $certInfo->valid_from ?? ''
];
}
// Sort by valid_from date (oldest first) - reverse insertion order
usort($certificatesWithDates, function($a, $b) {
$timestampA = !empty($a['date']) ? strtotime($a['date']) : 0;
$timestampB = !empty($b['date']) ? strtotime($b['date']) : 0;
return $timestampA <=> $timestampB;
});
$this->log("Certificates sorted by date (oldest first for reverse insertion)");
foreach ($certificatesWithDates as $certData) {
$naksCertificate = $certData['certificate_no'];
try {
Log::debug("Processing Naks certificate: " . $naksCertificate);
// Try multiple search strategies for Cyrillic characters and URL encoding
$search = [];
$basePath = "storage/documents/{$document['path']}";
// Strategy 1: Try with URL decoded path
$decodedPath = urldecode($basePath);
$files = $this->searchFilesWithFallback($decodedPath, $naksCertificate);
if (!empty($files)) {
$search = $files;
Log::debug("Found files with decoded path", ['count' => count($files), 'path' => $decodedPath]);
}
// Strategy 2: Try with original path if first strategy failed
if (empty($search) && $decodedPath !== $basePath) {
$files = $this->searchFilesWithFallback($basePath, $naksCertificate);
if (!empty($files)) {
$search = $files;
Log::debug("Found files with original path", ['count' => count($files), 'path' => $basePath]);
}
}
$document['title2'] = $naksCertificate;
$this->document = $document;
$newRow = $this->addRowToExcel(
$search,
$naksCertificate,
$certData['date']
);
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
} catch (\Throwable $th) {
$this->log("Error: {$th->getMessage()}", 'error');
continue;
}
}
return $currentRow;
}
}
@@ -0,0 +1,200 @@
<?php
namespace App\Services\RegisterCreator;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Log;
use Exception;
class ExcelHandler
{
private Spreadsheet $spreadsheet;
private Worksheet $sheet;
private string $templatePath;
/**
* Load Excel template
*/
public function loadTemplate(string $templatePath): self
{
$this->templatePath = $templatePath;
$fullPath = storage_path('documents/' . $templatePath);
if (!file_exists($fullPath)) {
throw new Exception("Excel template not found: {$fullPath}");
}
Log::debug('Loading Excel template', ['path' => $fullPath]);
$this->spreadsheet = IOFactory::load($fullPath);
$this->sheet = $this->spreadsheet->getActiveSheet();
Log::debug('Excel template loaded', [
'type' => get_class($this->spreadsheet),
'sheet_name' => $this->sheet->getTitle()
]);
return $this;
}
/**
* Get spreadsheet instance
*/
public function getSpreadsheet(): Spreadsheet
{
return $this->spreadsheet;
}
/**
* Get active sheet
*/
public function getSheet(): Worksheet
{
return $this->sheet;
}
/**
* Replace placeholders in Excel sheet
*/
public function replacePlaceholders(array $replacements): self
{
Log::debug('Replacing placeholders in Excel', [
'count' => count($replacements)
]);
foreach ($this->sheet->getRowIterator() as $row) {
foreach ($row->getCellIterator() as $cell) {
$cellValue = $cell->getValue();
// Handle RichText objects
if ($cellValue instanceof \PhpOffice\PhpSpreadsheet\RichText\RichText) {
$cellValue = $cellValue->getPlainText();
}
// Convert to string for processing
$cellValueStr = (string)$cellValue;
if (!empty($cellValueStr)) {
$originalValue = $cellValueStr;
foreach ($replacements as $placeholder => $replacement) {
if (strpos($cellValueStr, $placeholder) !== false) {
$cellValueStr = str_replace($placeholder, $replacement, $cellValueStr);
}
}
// Only update if value changed
if ($cellValueStr !== $originalValue) {
$cell->setValue($cellValueStr);
Log::debug('Placeholder replaced', [
'cell' => $cell->getCoordinate(),
'original' => $originalValue,
'new' => $cellValueStr
]);
}
}
}
}
return $this;
}
/**
* Save Excel file
*/
public function save(string $outputPath, bool $override = true): string
{
$fullPath = storage_path('documents/' . $outputPath);
// Create directory if not exists
$directory = dirname($fullPath);
if (!file_exists($directory)) {
mkdir($directory, 0777, true);
Log::debug('Created directory', ['path' => $directory]);
}
// Check override setting
if (!$override && file_exists($fullPath)) {
$fullPath = $this->generateUniqueFilename($fullPath);
$outputPath = str_replace(storage_path('documents/'), '', $fullPath);
Log::debug('File exists and override is false, using unique name', [
'path' => $fullPath
]);
}
// Check write permissions
if (!is_writable($directory)) {
throw new Exception("Directory is not writable: {$directory}");
}
Log::debug('Saving Excel file', [
'path' => $fullPath,
'memory_usage' => round(memory_get_usage(true) / 1024 / 1024, 2) . ' MB'
]);
$writer = IOFactory::createWriter($this->spreadsheet, 'Xlsx');
$writer->save($fullPath);
Log::debug('Excel file saved successfully', [
'size' => filesize($fullPath) . ' bytes'
]);
return $outputPath;
}
/**
* Remove template row from sheet
*/
public function removeTemplateRow(int $templateRow): self
{
$this->sheet->removeRow($templateRow);
Log::debug('Template row removed', ['row' => $templateRow]);
return $this;
}
/**
* Generate unique filename if file exists
*/
private function generateUniqueFilename(string $filePath): string
{
$counter = 1;
$pathInfo = pathinfo($filePath);
do {
$newFileName = $pathInfo['dirname'] . '/' .
$pathInfo['filename'] . '_' . $counter . '.' .
$pathInfo['extension'];
$counter++;
} while (file_exists($newFileName));
return $newFileName;
}
/**
* Cleanup resources
*/
public function cleanup(): void
{
if (isset($this->spreadsheet)) {
$this->spreadsheet->disconnectWorksheets();
unset($this->spreadsheet);
gc_collect_cycles();
Log::debug('Excel resources cleaned up');
}
}
/**
* Destructor
*/
public function __destruct()
{
$this->cleanup();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
<?php
namespace App\Services\RegisterCreator;
use Illuminate\Support\Facades\Log;
use Exception;
class PdfConverter
{
/**
* Convert Excel to PDF using xlsx_to_pdf_legacy helper
*/
public function convert(string $excelPath, string $pdfPath, bool $override = true): string
{
$fullExcelPath = storage_path('documents/' . $excelPath);
$fullPdfPath = storage_path('documents/' . $pdfPath);
if (!file_exists($fullExcelPath)) {
throw new Exception("Excel file not found: {$fullExcelPath}");
}
// Prepare PDF filename
$pdfFileName = rtrim($fullPdfPath, '/') . 'Register.pdf';
// Check override setting
if (!$override && file_exists($pdfFileName)) {
$pdfFileName = $this->generateUniquePdfFilename($pdfFileName);
$pdfPath = str_replace(storage_path('documents/'), '', dirname($pdfFileName)) . '/';
Log::debug('PDF exists and override is false, using unique name', [
'path' => $pdfFileName
]);
}
Log::debug('Converting Excel to PDF', [
'excel' => $fullExcelPath,
'pdf_dir' => $fullPdfPath
]);
try {
// Use existing helper function
$result = xlsx_to_pdf_legacy($fullExcelPath, $fullPdfPath);
if (!$result) {
throw new Exception("PDF conversion failed");
}
// Fix file permissions if PDF was created
if (file_exists($pdfFileName)) {
try {
// Change owner to www-data for web access
chown($pdfFileName, 'www-data');
chgrp($pdfFileName, 'www-data');
chmod($pdfFileName, 0644);
} catch (\Throwable $th) {
Log::warning('Could not change PDF file permissions', [
'file' => $pdfFileName,
'error' => $th->getMessage()
]);
}
}
Log::debug('PDF conversion successful', [
'pdf_file' => $pdfFileName,
'exists' => file_exists($pdfFileName)
]);
return $pdfPath;
} catch (\Throwable $th) {
Log::error('PDF conversion error', [
'error' => $th->getMessage(),
'excel' => $fullExcelPath,
'pdf_dir' => $fullPdfPath
]);
throw $th;
}
}
/**
* Generate unique PDF filename if file exists
*/
private function generateUniquePdfFilename(string $filePath): string
{
$counter = 1;
$pathInfo = pathinfo($filePath);
do {
$newFileName = $pathInfo['dirname'] . '/' .
$pathInfo['filename'] . '_' . $counter . '.' .
$pathInfo['extension'];
$counter++;
} while (file_exists($newFileName));
return $newFileName;
}
}
@@ -0,0 +1,100 @@
<?php
namespace App\Services\RegisterCreator;
use Illuminate\Support\Facades\Log;
class PlaceholderReplacer
{
/**
* Prepare replacements array from weld log data
*/
public function prepareReplacements(array $weldLogData, array $additionalData = []): array
{
$replacements = [];
// Add project name
$replacements['{project_name}'] = setting('project_name_ru') ?? '';
// Add all weld log fields
foreach ($weldLogData as $column => $value) {
$replacements['{' . $column . '}'] = $value ?? '';
}
// Add additional data (title placeholders, etc.)
foreach ($additionalData as $key => $value) {
if (!str_starts_with($key, '{')) {
$key = '{' . $key . '}';
}
$replacements[$key] = $value ?? '';
}
Log::debug('Placeholders prepared', [
'count' => count($replacements),
'keys' => array_keys($replacements)
]);
return $replacements;
}
/**
* Get latest test date from weld log
*/
public function getLatestDate(array $weldLogData, string $registerColumnBased): string
{
try {
$latestDateQuery = db("weld_logs")
->where($registerColumnBased, $weldLogData[$registerColumnBased])
->select(
\DB::raw("GREATEST(
IFNULL(vt_test_date, '0000-00-00'),
IFNULL(rt_test_date, '0000-00-00'),
IFNULL(ut_test_date, '0000-00-00'),
IFNULL(pt_test_date, '0000-00-00'),
IFNULL(mt_test_date, '0000-00-00'),
IFNULL(pmi_test_date, '0000-00-00'),
IFNULL(ht_test_date, '0000-00-00'),
IFNULL(pwht_test_date, '0000-00-00'),
IFNULL(ferrite_test_date, '0000-00-00')
) AS latest_date")
)
->first();
// If query returns result, use latest date, otherwise use welding_date
$latestDate = ($latestDateQuery && $latestDateQuery->latest_date != '0000-00-00')
? $latestDateQuery->latest_date
: ($weldLogData['welding_date'] ?? now()->format('Y-m-d'));
Log::debug('Latest date calculated', [
'line' => $weldLogData[$registerColumnBased],
'date' => $latestDate
]);
return $latestDate;
} catch (\Throwable $th) {
Log::error('Error calculating latest date', [
'error' => $th->getMessage()
]);
return $weldLogData['welding_date'] ?? now()->format('Y-m-d');
}
}
}
@@ -0,0 +1,260 @@
<?php
namespace App\Services\RegisterCreator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
class ProgressTracker
{
private string $jobId;
private int $totalSteps;
private int $currentStep = 0;
private int $totalRegisters = 0;
private ?string $lineData = null;
public function __construct(string $jobId, int $totalSteps = 100, int $totalRegisters = 0, ?string $lineData = null)
{
$this->jobId = $jobId;
$this->totalSteps = $totalSteps;
$this->totalRegisters = $totalRegisters;
$this->lineData = $lineData;
// Update queue status to "running" when job starts processing
$this->updateQueueToRunning();
}
/**
* Update progress in cache
*/
public function update(string $description, ?int $customProgress = null, ?int $customCurrent = null): void
{
try {
$progress = $customProgress ?? $this->calculateProgress();
// If customCurrent is provided, use it; otherwise use currentStep
$current = $customCurrent ?? $this->currentStep;
$progressData = [
'total' => $this->totalSteps,
'current' => $current,
'progress' => $progress,
'description' => $description,
'line_data' => $this->lineData,
'updated_at' => now()->toDateTimeString(),
'status' => 'running'
];
Cache::put("register-creator-progress-{$this->jobId}", $progressData, now()->addHours(24));
Log::debug("Progress updated for job {$this->jobId}", [
'line_data' => $this->lineData,
'progress' => $progress,
'current' => $current,
'total' => $this->totalSteps,
'description' => $description
]);
} catch (\Throwable $th) {
// Don't let progress tracking failure stop the job
Log::error("Failed to update progress for job {$this->jobId}", [
'error' => $th->getMessage()
]);
}
}
/**
* Increment current step
*/
public function increment(string $description): void
{
$this->currentStep++;
$this->update($description);
}
/**
* Set current step
*/
public function setStep(int $step, string $description): void
{
$this->currentStep = $step;
$this->update($description);
}
/**
* Mark as completed
*/
public function complete(string $message = 'Completed'): void
{
// Set current to total when completing
$this->update($message, 100, $this->totalSteps);
Log::info("Job {$this->jobId} completed", [
'line_data' => $this->lineData
]);
// Remove from queue after completion
$this->removeFromQueue();
}
/**
* Mark as failed
*/
public function fail(string $error): void
{
Cache::put("register-creator-progress-{$this->jobId}", [
'total' => $this->totalSteps,
'current' => $this->currentStep,
'progress' => $this->calculateProgress(),
'description' => "Error: {$error}",
'line_data' => $this->lineData,
'status' => 'failed',
'updated_at' => now()->toDateTimeString()
], now()->addHours(24));
Log::error("Job {$this->jobId} failed", [
'line_data' => $this->lineData,
'current' => $this->currentStep,
'total' => $this->totalSteps,
'error' => $error
]);
// Remove from queue after failure
$this->removeFromQueue();
}
/**
* Calculate progress percentage
*/
private function calculateProgress(): int
{
if ($this->totalSteps === 0) {
return 0;
}
return min(100, (int) round(($this->currentStep / $this->totalSteps) * 100));
}
/**
* Get current progress
*/
public function get(): ?array
{
return Cache::get("register-creator-progress-{$this->jobId}");
}
/**
* Clear progress from cache
*/
public function clear(): void
{
Cache::forget("register-creator-progress-{$this->jobId}");
}
/**
* Update queue status to running when job starts processing
* Note: Only updates status fields, preserves user and line_identifier from controller
*/
private function updateQueueToRunning(): void
{
$queue = Cache::get('register-creator-queue-2', []);
if (isset($queue[$this->jobId])) {
// Update only status-related fields, preserve user and line_identifier from controller
$queue[$this->jobId]['status'] = 'running';
$queue[$this->jobId]['started_at'] = now()->toDateTimeString();
// Update line_identifier only if we have lineData and it's not already set
if ($this->lineData && empty($queue[$this->jobId]['line_identifier'])) {
$queue[$this->jobId]['line_identifier'] = $this->lineData;
}
// Sync total_registers with totalSteps if available
if ($this->totalSteps > 0) {
$queue[$this->jobId]['total_registers'] = $this->totalSteps;
}
Cache::put('register-creator-queue-2', $queue, now()->addHours(24));
Log::info("Job {$this->jobId} status updated to running", [
'line_data' => $this->lineData,
'total_documents' => $this->totalSteps,
'preserved_user' => isset($queue[$this->jobId]['user']) ? 'yes' : 'no',
'preserved_line_identifier' => $queue[$this->jobId]['line_identifier'] ?? 'none'
]);
} else {
// Fallback: If queue entry doesn't exist, create it
// This shouldn't happen in normal flow, but added for safety
Log::warning("Queue entry not found for job {$this->jobId}, creating new entry");
try {
$user = Auth::user();
} catch (\Throwable $th) {
$user = null;
}
$queue[$this->jobId] = [
'user' => $user,
'total_registers' => $this->totalSteps > 0 ? $this->totalSteps : $this->totalRegisters,
'line_identifier' => $this->lineData,
'started_at' => now()->toDateTimeString(),
'status' => 'running'
];
Cache::put('register-creator-queue-2', $queue, now()->addHours(24));
}
}
/**
* Remove this job from the queue
*/
private function removeFromQueue(): void
{
$queue = Cache::get('register-creator-queue-2', []);
if (isset($queue[$this->jobId])) {
unset($queue[$this->jobId]);
Cache::put('register-creator-queue-2', $queue, now()->addHours(24));
Log::info("Job {$this->jobId} removed from queue", [
'line_data' => $this->lineData
]);
}
}
/**
* Get job ID
*/
public function getJobId(): string
{
return $this->jobId;
}
/**
* Get total registers count
*/
public function getTotalRegisters(): int
{
return $this->totalRegisters;
}
/**
* Get line data
*/
public function getLineData(): ?string
{
return $this->lineData;
}
}
@@ -0,0 +1,517 @@
<?php
namespace App\Services\RegisterCreator;
use App\Services\RegisterCreator\DocumentProcessors\DocumentProcessorFactory;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Exception;
class RegisterCreatorService
{
private ExcelHandler $excelHandler;
private PdfConverter $pdfConverter;
private PlaceholderReplacer $placeholderReplacer;
private ProgressTracker $progressTracker;
private array $settings;
private array $statistics;
public function __construct()
{
$this->excelHandler = new ExcelHandler();
$this->pdfConverter = new PdfConverter();
$this->placeholderReplacer = new PlaceholderReplacer();
// Initialize statistics
$this->statistics = [
'documents_processed' => 0,
'documents_success' => 0,
'documents_failed' => 0,
'by_type' => []
];
}
/**
* Process single line with all documents
*/
public function processLine(array $lineData, array $documents, array $settings): array
{
$startTime = microtime(true);
$this->settings = $settings;
// Initialize progress tracker
$jobId = $settings['job_id'] ?? uniqid('rc_', true);
$registerColumnBased = $settings['register_column_based'] ?? 'line_number';
$lineIdentifier = $lineData[$registerColumnBased] ?? 'unknown';
// Set total to document count for proper progress tracking
$totalDocuments = count($documents);
$this->progressTracker = new ProgressTracker($jobId, $totalDocuments, 0, $lineIdentifier);
Log::info("Processing line started", [
'job_id' => $jobId,
'line' => $lineIdentifier,
'documents_count' => $totalDocuments
]);
$this->progressTracker->update("Loading template for {$lineIdentifier}", 0, 0);
try {
// Get document template info
$documentInfo = document_template("register");
if (!$documentInfo) {
throw new Exception("Register template not found!");
}
// Prepare folder structure - matching blade structure
$path = $settings['path'] ?? '';
$basePath = "storage/documents/{$path}";
$fullFolder = "{$basePath}/{$lineIdentifier}/";
$fullFolder2 = str_replace("storage/documents/", "", $fullFolder);
$justFolder = "{$path}/{$lineIdentifier}/";
// Clean target directory before processing
if (Storage::exists($fullFolder2)) {
Storage::deleteDirectory($fullFolder2);
}
Storage::makeDirectory($fullFolder2);
// Create log file using fullFolder2 (storage-relative path)
$this->initializeLogFile($fullFolder2, $lineIdentifier);
// Create info.txt file with job and user information
try {
Log::info("About to create info.txt", ['folder' => $fullFolder2, 'line' => $lineIdentifier]);
$this->createInfoFile($fullFolder2, $lineIdentifier, $settings, $totalDocuments, $lineData);
Log::info("info.txt creation completed", ['folder' => $fullFolder2]);
} catch (\Throwable $th) {
Log::error("Failed to create info.txt", [
'error' => $th->getMessage(),
'file' => $th->getFile(),
'line' => $th->getLine()
]);
}
// Load Excel template
$this->excelHandler->loadTemplate($documentInfo->files);
// Prepare placeholders
$replacements = $this->placeholderReplacer->prepareReplacements($lineData);
// Replace placeholders in Excel
$this->excelHandler->replacePlaceholders($replacements);
// Get contractor information
$contractor = $this->getContractorName($lineData);
Cache::put("rc_contractor", $contractor);
// Get WPS data if available
$wpsData = $this->getWpsData($lineData);
// Set template row
$templateRow = setting("register_creator_start_row") ?: 16;
Cache::put("rc_template_row", $templateRow);
$currentRow = $templateRow;
$rowNo = 1; // Initialize global row number counter
// DEBUG: Log documents BEFORE sorting
Log::info("📋 Documents BEFORE sorting", [
'documents' => array_map(function($doc, $idx) {
return [
'index' => $idx,
'type' => $doc['type'] ?? 'N/A',
'order' => $doc['order'] ?? 'N/A',
'title2' => $doc['title2'] ?? 'N/A'
];
}, $documents, array_keys($documents))
]);
// Sort documents by order field before processing
// This ensures Excel rows are created in the correct document type order
usort($documents, function($a, $b) {
$orderA = $a['order'] ?? 999;
$orderB = $b['order'] ?? 999;
// If orders are equal, maintain original order by using type as secondary sort
if ($orderA === $orderB) {
$typeA = $a['type'] ?? '';
$typeB = $b['type'] ?? '';
return strcmp($typeA, $typeB);
}
return $orderA <=> $orderB;
});
// DEBUG: Log documents AFTER sorting
Log::info("📋 Documents AFTER sorting", [
'documents' => array_map(function($doc, $idx) {
return [
'index' => $idx,
'type' => $doc['type'] ?? 'N/A',
'order' => $doc['order'] ?? 'N/A',
'title2' => $doc['title2'] ?? 'N/A'
];
}, $documents, array_keys($documents))
]);
Log::debug("Documents sorted by order field", [
'documents_count' => count($documents),
'first_order' => $documents[0]['order'] ?? 'N/A',
'last_order' => $documents[count($documents) - 1]['order'] ?? 'N/A'
]);
// Process each document
foreach ($documents as $index => $document) {
$startDocTime = microtime(true);
try {
// Safe string conversion for title2
$docTitle = $document['title2'] ?? 'unknown';
if (is_array($docTitle)) {
$docTitle = json_encode($docTitle);
}
$currentDoc = $index + 1;
$progressPercent = (int) (($currentDoc) / count($documents) * 100);
$this->progressTracker->update(
"Processing document {$currentDoc}/" . count($documents) . ": {$docTitle}",
$progressPercent,
$currentDoc // Update current to show which document we're on
);
Log::debug("Starting document processing", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'document' => $docTitle,
'index' => $index + 1,
'total' => count($documents)
]);
$oldRowNo = $rowNo;
$rowNo = $this->processDocument(
$lineData,
$document,
$this->excelHandler->getSheet(),
$currentRow,
$wpsData,
$rowNo,
$documents // Pass all documents array for dynamic mapping
);
$docDuration = round(microtime(true) - $startDocTime, 2);
Log::info("📊 Document processed", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'document' => $docTitle,
'document_type' => $document['type'] ?? 'N/A',
'document_order' => $document['order'] ?? 'N/A',
'rowNo_before' => $oldRowNo,
'rowNo_after' => $rowNo,
'rows_added' => ($rowNo - $oldRowNo),
'duration' => $docDuration . 's'
]);
$this->statistics['documents_success']++;
} catch (\Throwable $th) {
$this->statistics['documents_failed']++;
// Safe string conversion for error logging
$docTitle = $document['title2'] ?? 'unknown';
if (is_array($docTitle)) {
$docTitle = json_encode($docTitle);
}
$docType = $document['type'] ?? 'unknown';
if (is_array($docType)) {
$docType = json_encode($docType);
}
$docPath = $document['path'] ?? 'unknown';
if (is_array($docPath)) {
$docPath = json_encode($docPath);
}
// Detailed error logging for documents
Log::error("Document processing error", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'document' => $docTitle,
'document_type' => $docType,
'document_path' => $docPath,
'error_type' => get_class($th),
'error_message' => $th->getMessage(),
'file' => $th->getFile(),
'line_number' => $th->getLine(),
'trace' => $th->getTraceAsString()
]);
// Write detailed error to log file
$errorLog = "❌ Document error: {$docTitle}\n";
$errorLog .= " Type: {$docType}\n";
$errorLog .= " Path: {$docPath}\n";
$errorLog .= " Error: " . get_class($th) . " - {$th->getMessage()}\n";
$errorLog .= " File: {$th->getFile()}:{$th->getLine()}\n";
$this->writeLog($fullFolder2, $errorLog);
continue;
}
$this->statistics['documents_processed']++;
}
// Apply work permit replacements
try {
$spreadsheet = workPermitReplacerExcel2(
$this->excelHandler->getSpreadsheet(),
$lineData,
$documentInfo,
"replacer"
);
} catch (\Throwable $th) {
Log::warning("Work permit replacer error", [
'error' => $th->getMessage()
]);
}
// Remove template row
$this->excelHandler->removeTemplateRow($templateRow);
// Save Excel file
$registerFileName = $justFolder . "Register.xlsx";
$this->excelHandler->save($registerFileName, $settings['override'] ?? true);
// Convert to PDF
$pdfPath = $justFolder;
$this->pdfConverter->convert($registerFileName, $pdfPath, $settings['override'] ?? true);
// Calculate duration
$duration = round(microtime(true) - $startTime, 2);
// Write summary to log
$this->writeSummary($justFolder, $lineIdentifier, $duration);
// Mark as complete - will show total/total (e.g., 23/23)
$this->progressTracker->complete("Completed successfully");
Log::info("Line processing completed", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'duration' => $duration . 's',
'documents_processed' => $this->statistics['documents_processed'],
'documents_success' => $this->statistics['documents_success'],
'documents_failed' => $this->statistics['documents_failed']
]);
return [
'status' => 'success',
'line' => $lineIdentifier,
'duration' => $duration,
'statistics' => $this->statistics
];
} catch (\Throwable $th) {
$this->progressTracker->fail($th->getMessage());
Log::error("Line processing failed", [
'job_id' => $this->progressTracker->getJobId(),
'line' => $lineIdentifier,
'error' => $th->getMessage(),
'trace' => $th->getTraceAsString()
]);
throw $th;
} finally {
// Cleanup
$this->excelHandler->cleanup();
Cache::forget("rc_lastPage");
Cache::forget("rc_firstPage");
Cache::forget("rc_contractor");
Cache::forget("rc_template_row");
}
}
/**
* Process single document
*/
private function processDocument(
array $lineData,
array $document,
$sheet,
int &$currentRow,
$wpsData,
int $rowNo,
array $allDocuments = [] // All documents array for dynamic type order mapping
): int {
// Validate document structure - ensure 'type' field exists
if (!isset($document['type']) || empty($document['type'])) {
// Log detailed document information for debugging
Log::warning("⚠️ Document missing 'type' field, attempting to infer", [
'document_keys' => array_keys($document),
'document_id' => $document['id'] ?? 'unknown',
'document_path' => $document['path'] ?? 'unknown'
]);
// Try to infer type from document structure
if (isset($document['is_dynamic']) && $document['is_dynamic']) {
$document['type'] = 'dynamic';
Log::info(" ✓ Inferred type as 'dynamic' based on is_dynamic flag");
} else if (isset($document['sql_query'])) {
// If has SQL query, it's likely a dynamic document
$document['type'] = 'dynamic';
Log::info(" ✓ Inferred type as 'dynamic' based on sql_query presence");
} else if (isset($document['id']) && strpos($document['id'], 'template') !== false) {
$document['type'] = 'template';
Log::info(" ✓ Inferred type as 'template' based on id");
} else {
// Default fallback
$document['type'] = 'qa';
Log::info(" ✓ Using default type 'qa' as fallback");
}
}
$processor = DocumentProcessorFactory::make($document['type'], $document);
// Prepare settings for processor
$processorSettings = array_merge($this->settings, [
'wps_data' => $wpsData,
'row_no' => $rowNo, // Pass global row number to processor
'all_documents' => $allDocuments // Pass all documents for dynamic mapping
]);
$newRow = $processor->process(
$lineData,
$document,
$sheet,
$currentRow,
$processorSettings
);
// Update current row if changed
if ($newRow > $currentRow) {
$currentRow = $newRow;
}
// Update statistics
$docType = $document['type'];
if (!isset($this->statistics['by_type'][$docType])) {
$this->statistics['by_type'][$docType] = 0;
}
$this->statistics['by_type'][$docType]++;
// Get number of documents added from processor and increment rowNo accordingly
$documentsAdded = $processor->getDocumentsAdded();
Log::debug("Documents added by processor", [
'processor' => get_class($processor),
'documents_added' => $documentsAdded,
'current_rowNo' => $rowNo,
'next_rowNo' => $rowNo + $documentsAdded
]);
return $rowNo + $documentsAdded;
}
/**
* Get contractor name
*/
private function getContractorName(array $lineData): string
{
$subcontractors = Cache::get("subcontractors", []);
$contractorKey = $lineData['contractor'] ?? '';
if (isset($subcontractors[$contractorKey])) {
return $subcontractors[$contractorKey]->company_name_ru ?? $contractorKey;
}
return $contractorKey;
}
/**
* Get WPS data
*/
private function getWpsData(array $lineData)
{
$wpsNo = $lineData['wps_no'] ?? '';
if (empty($wpsNo)) {
return null;
}
return db("w_p_s")->where("details", $wpsNo)->first();
}
/**
* Initialize log file
*/
private function initializeLogFile(string $folder, string $lineIdentifier): void
{
$logPath = $folder . 'log.txt';
Storage::delete($logPath);
$header = str_repeat("🚀", 30) . "\n";
$header .= "REGISTER CREATOR LOG FILE\n";
$header .= "Line: {$lineIdentifier}\n";
$header .= "Started: " . now()->toDateTimeString() . "\n";
$header .= str_repeat("🚀", 30) . "\n\n";
Storage::put($logPath, $header);
}
/**
* Create info.txt file with user information (JSON format)
*/
private function createInfoFile(string $folder, string $lineIdentifier, array $settings, int $totalDocuments, array $lineData): void
{
$infoPath = $folder . 'info.txt';
// Delete old info file if exists
Storage::delete($infoPath);
// Create simple JSON with user information
$infoData = [
'user_name' => $settings['user_name'] ?? 'Unknown',
'user_id' => $settings['user_id'] ?? null,
'created_at' => now()->format('Y-m-d H:i:s'),
'line_identifier' => $lineIdentifier
];
Storage::put($infoPath, json_encode($infoData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
/**
* Write to log file
*/
private function writeLog(string $folder, string $message): void
{
$logPath = $folder . 'log.txt';
Storage::append($logPath, $message . "\n");
}
/**
* Write summary to log file
*/
private function writeSummary(string $folder, string $lineIdentifier, float $duration): void
{
$summary = "\n" . str_repeat("=", 80) . "\n";
$summary .= "PROCESSING SUMMARY\n";
$summary .= str_repeat("=", 80) . "\n";
$summary .= "Line: {$lineIdentifier}\n";
$summary .= "Duration: {$duration}s\n";
$summary .= "Documents Processed: {$this->statistics['documents_processed']}\n";
$summary .= "Successful: {$this->statistics['documents_success']}\n";
$summary .= "Failed: {$this->statistics['documents_failed']}\n";
$summary .= "\nBy Type:\n";
foreach ($this->statistics['by_type'] as $type => $count) {
$summary .= " - {$type}: {$count}\n";
}
$summary .= str_repeat("=", 80) . "\n";
$this->writeLog($folder, $summary);
}
}
@@ -0,0 +1,84 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
abstract class AbstractTpDocumentProcessor
{
protected array $settings;
/**
* Process the document and add rows to Excel
*
* @param array $lineData Test Package data
* @param array $document Document configuration
* @param Worksheet $sheet Excel sheet
* @param int $currentRow Current row index in Excel
* @param int $rowNo Current row number (counter)
* @param string $fullFolder Full path to destination folder
* @param string $testPackageNo Test Package Number
* @param bool $override Override existing files
* @return int New row number (counter)
*/
abstract public function process(
array $lineData,
array $document,
Worksheet $sheet,
int $currentRow,
int $rowNo,
string $fullFolder,
string $testPackageNo,
bool $override
): int;
protected function alternativeGlob($pattern)
{
return glob($pattern);
}
protected function extractFileName($fullPath, $basePath)
{
if(empty($fullPath)) return '';
// Remove storage/documents prefix if present to get relative path
$name = str_replace("storage/documents/{$basePath}/", "", $fullPath);
// Also try removing just basePath if fullPath is relative
$name = str_replace("{$basePath}/", "", $name);
return str_replace(".pdf", "", $name);
}
protected function addRow(array $search, array $doc, string $folder, string $line, string $date, int &$rowNo, Worksheet $sheet, int &$currentRow, bool $override): void
{
// Check if file exists
if(empty($search)) {
$logPath = $folder . 'log.txt';
$msg = "❌ NOT FOUND: " . ($doc['title2'] ?? 'Unknown') . " (" . ($doc['path'] ?? 'Unknown Path') . ")\n";
file_put_contents($logPath, $msg, FILE_APPEND);
return;
}
$file = $search[0];
$fileName = basename($file);
$destPath = $folder . $fileName;
// Copy file
if(!file_exists($destPath) || $override) {
copy($file, $destPath);
}
// Insert new row
$sheet->insertNewRowBefore($currentRow + 1, 1);
// Set values (Columns A, B, C, D as per TpCreatorService logic)
$sheet->setCellValue("A" . $currentRow, $rowNo);
$sheet->setCellValue("B" . $currentRow, $doc['title2'] ?? '');
$sheet->setCellValue("C" . $currentRow, $doc['title3'] ?? ''); // Title 3 is usually extracted filename or specific detail
$sheet->setCellValue("D" . $currentRow, $date);
// Increment
$currentRow++;
$rowNo++;
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DocumentProcedureProcessor extends AbstractTpDocumentProcessor
{
public function process(array $lineData, array $document, Worksheet $sheet, int $currentRow, array $settings): int
{
$override = $settings['override'] ?? true;
$folder = $settings['full_folder'];
$rowNo = $settings['row_no'];
$docTitle = $document['title2'] ?? '';
$docPath = $document['path'];
$documentProcedure = db("document_procedures")->where("document_no", $docTitle)->first();
if($documentProcedure) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$docTitle}*.pdf");
$docDate = $documentProcedure->publish_date;
$document['title2'] = $documentProcedure->description;
$this->addRow($search, $document, $folder, $documentProcedure->document_no, $docDate, $rowNo, $sheet, $currentRow, $override);
}
return $currentRow;
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DocumentProcedureTpProcessor extends AbstractTpDocumentProcessor
{
public function process(
array $lineData,
array $document,
Worksheet $sheet,
int $currentRow,
int $rowNo,
string $fullFolder,
string $testPackageNo,
bool $override
): int {
$docTitle = $document['title2'] ?? '';
$docPath = $document['path'];
$documentProcedure = db("document_procedures")->where("document_no", $docTitle)->first();
if($documentProcedure) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$docTitle}*.pdf");
$docDate = $documentProcedure->publish_date;
$document['title2'] = $documentProcedure->description;
$this->addRow($search, $document, $fullFolder, $documentProcedure->document_no, $docDate, $rowNo, $sheet, $currentRow, $override);
return $rowNo + 1;
}
return $rowNo;
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DrawingsProcessor extends AbstractTpDocumentProcessor
{
public function process(array $lineData, array $document, Worksheet $sheet, int $currentRow, array $settings): int
{
$testPackageNo = $lineData['test_package_number'];
$docPath = $document['path'];
$override = $settings['override'] ?? true;
$folder = $settings['full_folder'];
$rowNo = $settings['row_no'];
$testPackage = db("test_pack_base_statuses")->where("test_package_no", $testPackageNo)->first();
// Search by line_number as per blade logic
$lineNumber = $testPackage->line_number;
$search = $this->alternativeGlob("{$docPath}/*{$lineNumber}*.pdf");
$this->addRow($search, $document, $folder, $lineNumber, $testPackage->welding_date, $rowNo, $sheet, $currentRow, $override);
return $currentRow;
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DrawingsTpProcessor extends AbstractTpDocumentProcessor
{
public function process(
array $lineData,
array $document,
Worksheet $sheet,
int $currentRow,
int $rowNo,
string $fullFolder,
string $testPackageNo,
bool $override
): int {
$docPath = $document['path'];
$lineNumber = $lineData['line_number'] ?? $testPackageNo;
$weldingDate = $lineData['welding_date'] ?? date('Y-m-d');
$search = $this->alternativeGlob("{$docPath}/*{$lineNumber}*.pdf");
$this->addRow($search, $document, $fullFolder, $lineNumber, $weldingDate, $rowNo, $sheet, $currentRow, $override);
return $rowNo + 1;
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class GenericProcessor extends AbstractTpDocumentProcessor
{
public function process(array $lineData, array $document, Worksheet $sheet, int $currentRow, array $settings): int
{
$testPackageNo = $lineData['test_package_number'];
$docPath = $document['path'];
$override = $settings['override'] ?? true;
$folder = $settings['full_folder'];
$rowNo = $settings['row_no'];
$testPackage = db("test_pack_base_statuses")->where("test_package_no", $testPackageNo)->first();
// Fallback search logic as per blade: search by line number
$lineNumber = $testPackage->line_number;
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$lineNumber}*.pdf");
$this->addRow($search, $document, $folder, $lineNumber, $testPackage->welding_date, $rowNo, $sheet, $currentRow, $override);
return $currentRow;
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class GenericTpProcessor extends AbstractTpDocumentProcessor
{
public function process(
array $lineData,
array $document,
Worksheet $sheet,
int $currentRow,
int $rowNo,
string $fullFolder,
string $testPackageNo,
bool $override
): int {
// Generic fallback
$docPath = $document['path'];
$lineNumber = $lineData['line_number'] ?? $testPackageNo;
$weldingDate = $lineData['welding_date'] ?? date('Y-m-d'); // Assuming available in lineData join
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$lineNumber}*.pdf");
$this->addRow($search, $document, $fullFolder, $lineNumber, $weldingDate, $rowNo, $sheet, $currentRow, $override);
return $rowNo + 1;
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class NdtClearanceProcessor extends AbstractTpDocumentProcessor
{
public function process(array $lineData, array $document, Worksheet $sheet, int $currentRow, array $settings): int
{
$testPackageNo = $lineData['test_package_number'];
$docPath = $document['path'];
$override = $settings['override'] ?? true;
$folder = $settings['full_folder'];
$rowNo = $settings['row_no'];
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$testPackageNo}*.pdf");
$document['title2'] = str_replace("004_QA/", "", $document['title2']);
$this->addRow($search, $document, $folder, $testPackageNo, date('Y-m-d'), $rowNo, $sheet, $currentRow, $override);
return $currentRow;
}
}
@@ -0,0 +1,34 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class NdtClearanceTpProcessor extends AbstractTpDocumentProcessor
{
public function process(
array $lineData,
array $document,
Worksheet $sheet,
int $currentRow,
int $rowNo,
string $fullFolder,
string $testPackageNo,
bool $override
): int {
$docPath = $document['path'];
$docTitle = $document['title2'] ?? '';
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$testPackageNo}*.pdf");
$document['title2'] = str_replace("004_QA/", "", $docTitle);
$date = date('Y-m-d'); // Default to current date or welding date if available
$this->addRow($search, $document, $fullFolder, $testPackageNo, $date, $rowNo, $sheet, $currentRow, $override);
return $rowNo + 1;
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class QaProcessor extends AbstractTpDocumentProcessor
{
public function process(array $lineData, array $document, Worksheet $sheet, int $currentRow, array $settings): int
{
$testPackageNo = $lineData['test_package_number'];
$docPath = $document['path'];
$override = $settings['override'] ?? true;
$folder = $settings['full_folder'];
$rowNo = $settings['row_no'];
$allJoints = db("weld_logs")->where("test_package_no", $testPackageNo)->get();
$logTypes = array_keys(log_test_types());
foreach($allJoints as $joint) {
$jointArray = (array)$joint;
foreach($logTypes as $logType) {
if (strpos(strtolower($docPath), $logType) !== false) {
$reportNoPrefix = ($logType == "pmi") ? "no_of_testing_report" : $logType . "_report";
if (!empty($jointArray[$reportNoPrefix])) {
$reportNo = $jointArray[$reportNoPrefix];
$docDate = $jointArray[$logType . '_test_date'];
$search = $this->alternativeGlob("storage/documents/{$docPath}/{$reportNo}.pdf");
$document['title2'] = $reportNo;
// Translation
$translationKey = $logType . "_register_title";
// Note: If using helper e2(), ensure it's available or pass via settings.
// Assuming e2 is global helper.
$document['title4'] = e2($translationKey);
$this->addRow($search, $document, $folder, $reportNo, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
}
}
// Update row_no in settings passed by reference if needed, but simple return of new currentRow is standard.
// However, we also need to return new rowNo count or just the new currentRow index.
// The Abstract class addRow increments currentRow and rowNo by reference.
// But since rowNo is primitive, it won't persist back to caller unless we return it or use object.
// TpCreatorService expects new currentRow.
// We actually need to return how many rows added or the new current Row.
return $currentRow;
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class QaTpProcessor extends AbstractTpDocumentProcessor
{
public function process(
array $lineData,
array $document,
Worksheet $sheet,
int $currentRow,
int $rowNo,
string $fullFolder,
string $testPackageNo,
bool $override
): int {
$docPath = $document['path'];
$allJoints = db("weld_logs")->where("test_package_no", $testPackageNo)->get();
$logTypes = array_keys(log_test_types());
foreach($allJoints as $joint) {
$jointArray = (array)$joint;
foreach($logTypes as $logType) {
if (strpos(strtolower($docPath), $logType) !== false) {
$reportNoPrefix = ($logType == "pmi") ? "no_of_testing_report" : $logType . "_report";
if (!empty($jointArray[$reportNoPrefix])) {
$reportNo = $jointArray[$reportNoPrefix];
$docDate = $jointArray[$logType . '_test_date'];
$search = $this->alternativeGlob("storage/documents/{$docPath}/{$reportNo}.pdf");
$document['title2'] = $reportNo;
$translationKey = $logType . "_register_title";
$document['title4'] = e2($translationKey);
$this->addRow($search, $document, $fullFolder, $reportNo, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
}
}
return $rowNo;
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use Exception;
class TpDocumentProcessorFactory
{
public static function make(string $type, string $path): AbstractTpDocumentProcessor
{
// Path based detection logic first (as in blade/service)
if (strpos($path, "NDT_Clearance") !== false) {
return new NdtClearanceTpProcessor();
}
switch ($type) {
case 'qa':
return new QaTpProcessor();
case 'wdb':
return new WdbTpProcessor();
case 'drawings':
return new DrawingsTpProcessor();
case 'document-procedure':
return new DocumentProcedureTpProcessor();
default:
return new GenericTpProcessor();
}
}
}
@@ -0,0 +1,138 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Illuminate\Support\Facades\Cache;
class WdbProcessor extends AbstractTpDocumentProcessor
{
public function process(array $lineData, array $document, Worksheet $sheet, int $currentRow, array $settings): int
{
$testPackageNo = $lineData['test_package_number'];
$docPath = $document['path'];
$docTitle = $document['title2'] ?? '';
$override = $settings['override'] ?? true;
$folder = $settings['full_folder'];
$rowNo = $settings['row_no'];
$cleanTitle = str_replace("003_Welding_Database/", "", $docTitle);
$document['title2'] = $cleanTitle;
// Load dependencies
$testPackage = db("test_pack_base_statuses")->where("test_package_no", $testPackageNo)->first();
$wpsData = db("w_p_s")->where("details", $testPackage->wps_no)->first();
$naksCertificates = [];
if($wpsData) {
$naksCertificates = explode(" + ", $wpsData->naks_certificate_no);
}
// 1. Naks Technology
if (strpos($docPath, "Naks Technology") !== false) {
foreach($naksCertificates as $cert) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$cert}*.pdf");
$document['title3'] = $this->extractFileName($search[0] ?? '', $docPath);
$this->addRow($search, $document, $folder, $cert, $wpsData->date ?? date('Y-m-d'), $rowNo, $sheet, $currentRow, $override);
}
}
// 2. Naks_Welder
elseif (strpos($docPath, "Naks_Welder") !== false) {
$welders = $this->getWeldersForTp($testPackageNo);
foreach($welders as $welder) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$welder}*.pdf");
$document['title2'] = $welder;
$document['title3'] = $this->extractFileName($search[0] ?? '', $docPath);
$welderDate = db("naks_welders")->where("naks_certificate_no", $welder)->first()?->period_of_validity ?? date('Y-m-d');
$this->addRow($search, $document, $folder, $welder, $welderDate, $rowNo, $sheet, $currentRow, $override);
}
}
// 3. Naks_Equipments
elseif (strpos($docPath, "Naks_Equipments") !== false) {
foreach($naksCertificates as $cert) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$cert}*.pdf");
$document['title3'] = $cert;
$docDate = db("naks_welders")->where("welder_id", $cert)->first()?->period_of_validity ?? date('Y-m-d'); // Logic copied from blade, might need adjustment
$this->addRow($search, $document, $folder, $cert, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
// 4. Naks_Consumables
elseif (strpos($docPath, "Naks_Consumables") !== false) {
$naksConsumables = db("naks_consumables")->whereIn("naks_certificate_no", $naksCertificates)->pluck("batch_number")->toArray();
foreach($naksConsumables as $consumable) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$consumable}*.pdf");
$docDate = db("naks_welders")->where("welder_id", $consumable)->first()?->period_of_validity ?? date('Y-m-d');
$this->addRow($search, $document, $folder, $consumable, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
// 5. Welding Experts
elseif (strpos($docPath, "Welding Experts") !== false) {
// Using title2 as certificate no (from selection)
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$docTitle}*.pdf");
$expert = db("register_of_experts")->where("certificate_no", $docTitle)->first();
$docDate = $expert?->permit_date ?? date('Y-m-d');
$document['title2'] = e2("welding_expert_register_title");
$this->addRow($search, $document, $folder, $docTitle, $docDate, $rowNo, $sheet, $currentRow, $override);
}
// 6. WPQ
elseif (strpos($docPath, "WPQ") !== false) {
$wpqs = db("welder_tests")->whereIn("wpq_document_no", [
$testPackage->wpq_report_1,
$testPackage->wpq_report_2,
])->get();
foreach($wpqs as $wpq) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$wpq->wpq_document_no}*.pdf");
$document['title3'] = $wpq->wpq_document_no;
$docDate = $wpq->naks_validity;
$this->addRow($search, $document, $folder, $wpq->wpq_document_no, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
// 7. WPS
elseif (strpos($docPath, "WPS") !== false) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$wpsData->details}*.pdf");
$document['title3'] = $wpsData->details;
$docDate = $wpsData->date;
$this->addRow($search, $document, $folder, $wpsData->details, $docDate, $rowNo, $sheet, $currentRow, $override);
}
// 8. PQR
elseif (strpos($docPath, "PQR") !== false) {
$pqr = db("prosedure_qualification_records")->where("pqr_no", $wpsData->pqr_no)->first();
if($pqr) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$pqr->pqr_no}*.pdf");
$document['title2'] = $pqr->pqr_no;
$docDate = $pqr->approved_date;
$this->addRow($search, $document, $folder, $pqr->pqr_no, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
return $currentRow;
}
private function getWeldersForTp($tpNo) {
$logs = db("weld_logs")->where("test_package_no", $tpNo)->get();
$welders = [];
foreach($logs as $log) {
if($log->welder_1) $welders[] = $log->certificate_no_1;
if($log->welder_2) $welders[] = $log->certificate_no_2;
}
return array_unique(array_filter($welders));
}
}
@@ -0,0 +1,137 @@
<?php
namespace App\Services\TpCreator\DocumentProcessors;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class WdbTpProcessor extends AbstractTpDocumentProcessor
{
public function process(
array $lineData,
array $document,
Worksheet $sheet,
int $currentRow,
int $rowNo,
string $fullFolder,
string $testPackageNo,
bool $override
): int {
$docPath = $document['path'];
$docTitle = $document['title2'] ?? '';
$cleanTitle = str_replace("003_Welding_Database/", "", $docTitle);
$document['title2'] = $cleanTitle;
// Load dependencies
$testPackage = db("test_pack_base_statuses")->where("test_package_no", $testPackageNo)->first();
$wpsData = db("w_p_s")->where("details", $testPackage->wps_no)->first();
$naksCertificates = [];
if($wpsData) {
$naksCertificates = explode(" + ", $wpsData->naks_certificate_no);
}
// 1. Naks Technology
if (strpos($docPath, "Naks Technology") !== false) {
foreach($naksCertificates as $cert) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$cert}*.pdf");
$document['title3'] = $this->extractFileName($search[0] ?? '', $docPath);
$this->addRow($search, $document, $fullFolder, $cert, $wpsData->date ?? date('Y-m-d'), $rowNo, $sheet, $currentRow, $override);
}
}
// 2. Naks_Welder
elseif (strpos($docPath, "Naks_Welder") !== false) {
$welders = $this->getWeldersForTp($testPackageNo);
foreach($welders as $welder) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$welder}*.pdf");
$document['title2'] = $welder;
$document['title3'] = $this->extractFileName($search[0] ?? '', $docPath);
$welderDate = db("naks_welders")->where("naks_certificate_no", $welder)->first()?->period_of_validity ?? date('Y-m-d');
$this->addRow($search, $document, $fullFolder, $welder, $welderDate, $rowNo, $sheet, $currentRow, $override);
}
}
// 3. Naks_Equipments
elseif (strpos($docPath, "Naks_Equipments") !== false) {
foreach($naksCertificates as $cert) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$cert}*.pdf");
$document['title3'] = $cert;
$docDate = db("naks_welders")->where("welder_id", $cert)->first()?->period_of_validity ?? date('Y-m-d');
$this->addRow($search, $document, $fullFolder, $cert, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
// 4. Naks_Consumables
elseif (strpos($docPath, "Naks_Consumables") !== false) {
$naksConsumables = db("naks_consumables")->whereIn("naks_certificate_no", $naksCertificates)->pluck("batch_number")->toArray();
foreach($naksConsumables as $consumable) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$consumable}*.pdf");
$docDate = db("naks_welders")->where("welder_id", $consumable)->first()?->period_of_validity ?? date('Y-m-d');
$this->addRow($search, $document, $fullFolder, $consumable, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
// 5. Welding Experts
elseif (strpos($docPath, "Welding Experts") !== false) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$docTitle}*.pdf");
$expert = db("register_of_experts")->where("certificate_no", $docTitle)->first();
$docDate = $expert?->permit_date ?? date('Y-m-d');
$document['title2'] = e2("welding_expert_register_title");
$this->addRow($search, $document, $fullFolder, $docTitle, $docDate, $rowNo, $sheet, $currentRow, $override);
}
// 6. WPQ
elseif (strpos($docPath, "WPQ") !== false) {
$wpqs = db("welder_tests")->whereIn("wpq_document_no", [
$testPackage->wpq_report_1,
$testPackage->wpq_report_2,
])->get();
foreach($wpqs as $wpq) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$wpq->wpq_document_no}*.pdf");
$document['title3'] = $wpq->wpq_document_no;
$docDate = $wpq->naks_validity;
$this->addRow($search, $document, $fullFolder, $wpq->wpq_document_no, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
// 7. WPS
elseif (strpos($docPath, "WPS") !== false) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$wpsData->details}*.pdf");
$document['title3'] = $wpsData->details;
$docDate = $wpsData->date;
$this->addRow($search, $document, $fullFolder, $wpsData->details, $docDate, $rowNo, $sheet, $currentRow, $override);
}
// 8. PQR
elseif (strpos($docPath, "PQR") !== false) {
$pqr = db("prosedure_qualification_records")->where("pqr_no", $wpsData->pqr_no)->first();
if($pqr) {
$search = $this->alternativeGlob("storage/documents/{$docPath}/*{$pqr->pqr_no}*.pdf");
$document['title2'] = $pqr->pqr_no;
$docDate = $pqr->approved_date;
$this->addRow($search, $document, $fullFolder, $pqr->pqr_no, $docDate, $rowNo, $sheet, $currentRow, $override);
}
}
return $rowNo;
}
private function getWeldersForTp($tpNo) {
$logs = db("weld_logs")->where("test_package_no", $tpNo)->get();
$welders = [];
foreach($logs as $log) {
if($log->welder_1) $welders[] = $log->certificate_no_1;
if($log->welder_2) $welders[] = $log->certificate_no_2;
}
return array_unique(array_filter($welders));
}
}
+208
View File
@@ -0,0 +1,208 @@
<?php
namespace App\Services\TpCreator;
use App\Services\RegisterCreator\ExcelHandler;
use App\Services\RegisterCreator\PdfConverter;
use App\Services\TpCreator\DocumentProcessors\TpDocumentProcessorFactory;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Exception;
class TpCreatorService
{
private ExcelHandler $excelHandler;
private PdfConverter $pdfConverter;
public function __construct()
{
$this->excelHandler = new ExcelHandler();
$this->pdfConverter = new PdfConverter();
}
/**
* Process single TP line
*/
public function processLine(array $lineData, array $documents, array $settings): array
{
$startTime = microtime(true);
$jobId = $settings['job_id'];
$testPackageNo = $lineData['test_package_number'];
$path = $settings['path'] ?? '';
$override = $settings['override'] ?? true;
$this->updateProgress($jobId, 0, "Starting TP: $testPackageNo");
// 1. Prepare Data
$testPackage = db("test_pack_base_statuses")
->where("test_package_no", $testPackageNo)
->first();
if(!$testPackage) {
throw new Exception("Test Package not found: $testPackageNo");
}
// 2. Prepare Folders
$basePath = "storage/documents/$path";
$fullFolder = "$basePath/$testPackageNo/";
$justFolder = "$path/$testPackageNo/";
if (!file_exists($fullFolder)) {
mkdir($fullFolder, 0777, true);
}
// Initialize Log File and Create Info File
$this->initializeLogFile($fullFolder, $testPackageNo);
$this->createInfoFile($fullFolder, $testPackageNo, $settings);
// 3. Load Template
$documentInfo = document_template("register");
if (!$documentInfo) {
throw new Exception("Register template not found!");
}
$this->excelHandler->loadTemplate($documentInfo->files);
$sheet = $this->excelHandler->getSheet();
// 4. Initial Setup
$startRow = setting("register_creator_start_row") ?: 16;
$currentRow = $startRow;
$rowNo = 1;
// 5. Process Documents
$totalDocs = count($documents);
$processedDocs = 0;
foreach($documents as $doc) {
$docType = $doc['type'];
$docPath = $doc['path'];
$docTitle = $doc['title2'] ?? '';
$this->updateProgress($jobId, round(($processedDocs / $totalDocs) * 90), "Processing $docTitle");
try {
// Use Factory to get appropriate processor
$processor = TpDocumentProcessorFactory::make($docType, $docPath);
// Prepare settings for processor
$processorSettings = [
'override' => $override,
'full_folder' => $fullFolder,
'row_no' => $rowNo,
// Add other settings if needed by specific processors
];
// Process document
$newRow = $processor->process(
$lineData,
$doc,
$sheet,
$currentRow,
$rowNo,
$fullFolder,
$testPackageNo,
$override
);
// If rows were added, update currentRow and rowNo
// The processor returns the *next* row number (e.g., if it added 2 rows, it returns currentRow + 2)
// Actually, my interface implementation returns the new rowNo/count.
// Let's standardize: Processors should return the new currentRow index in Excel.
// Wait, QaTpProcessor returns rowNo (count) not currentRow (Excel index).
// Let's re-check the AbstractTpDocumentProcessor and implementations.
// QaTpProcessor returns $rowNo (the counter, not Excel row index).
// WdbTpProcessor returns $rowNo.
// AbstractTpDocumentProcessor adds rows and increments currentRow and rowNo by reference passed to addRow helper.
// But rowNo is passed by value to process() method unless specified otherwise.
// CORRECTION: In my implementation of processors, I passed $rowNo by value.
// And process() returns int. QaTpProcessor returns $rowNo.
// This means the returned value is the new Row Number counter.
// I need to calculate how many rows were added to update $currentRow.
// Let's assume process() returns the new $rowNo.
// Then diff = $newRowNo - $oldRowNo.
// $currentRow += diff.
// Let's verify AbstractTpDocumentProcessor signature I wrote:
// public function process(..., int $rowNo, ...): int
$oldRowNo = $rowNo;
$rowNo = $newRow; // Update global row counter
$rowsAdded = $rowNo - $oldRowNo;
$currentRow += $rowsAdded;
} catch (Exception $e) {
Log::error("Error processing document type $docType: " . $e->getMessage());
// Continue to next document
}
$processedDocs++;
}
// 6. Save & Convert
$registerFileName = $justFolder . "Register.xlsx";
$this->excelHandler->save($registerFileName, $override);
$this->pdfConverter->convert($registerFileName, $justFolder, $override);
$this->updateProgress($jobId, 100, "Completed");
return ['status' => 'success'];
}
private function updateProgress($jobId, $percent, $desc) {
$queue = Cache::get('tp-creator-queue', []);
if(isset($queue[$jobId])) {
$queue[$jobId]['progress'] = $percent;
$queue[$jobId]['description'] = $desc;
Cache::put('tp-creator-queue', $queue, now()->addHours(24));
}
}
/**
* Initialize log file
*/
private function initializeLogFile(string $fullFolder, string $lineIdentifier): void
{
$logPath = $fullFolder . 'log.txt';
if (file_exists($logPath)) {
unlink($logPath);
}
$header = str_repeat("🚀", 30) . "\n";
$header .= "TP CREATOR LOG FILE\n";
$header .= "Line: {$lineIdentifier}\n";
$header .= "Started: " . now()->toDateTimeString() . "\n";
$header .= str_repeat("🚀", 30) . "\n\n";
file_put_contents($logPath, $header);
}
/**
* Create info.txt file with user information (JSON format)
*/
private function createInfoFile(string $fullFolder, string $lineIdentifier, array $settings): void
{
$infoPath = $fullFolder . 'info.txt';
// Delete old info file if exists
if (file_exists($infoPath)) {
unlink($infoPath);
}
// Create simple JSON with user information
$infoData = [
'user_name' => $settings['user_name'] ?? 'Unknown',
'user_id' => $settings['user_id'] ?? null,
'created_at' => now()->format('Y-m-d H:i:s'),
'line_identifier' => $lineIdentifier
];
file_put_contents($infoPath, json_encode($infoData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
}
@@ -0,0 +1,140 @@
<?php
namespace App\Services\WeldLogTriggers\Base;
use App\Services\WeldLogTriggers\Contracts\WeldLogTriggerInterface;
use Illuminate\Support\Facades\Log;
/**
* Base class for all WeldLog triggers
*
* Provides common functionality like timing, logging, and shouldRun logic
*/
abstract class BaseTrigger implements WeldLogTriggerInterface
{
/**
* @var float Start time for performance tracking
*/
protected $startTime;
/**
* @var array Execution context
*/
protected $context = [];
/**
* Get trigger name - must be implemented by child classes
*/
abstract public function getName(): string;
/**
* Get trigger order - must be implemented by child classes
*/
abstract public function getOrder(): int;
/**
* Get dependent fields - must be implemented by child classes
*/
abstract public function getDependentFields(): array;
/**
* Process trigger logic - must be implemented by child classes
*
* @param object $weldLogData Current weld log data
* @param object|null $beforeData Previous weld log data
* @param array $context Execution context
* @return array Result data
*/
abstract protected function process($weldLogData, $beforeData, array $context): array;
/**
* Execute the trigger with logging and error handling
*
* @param object $weldLogData Current weld log data
* @param object|null $beforeData Previous weld log data
* @param array $context Execution context
* @return array Result data
* @throws \Throwable
*/
public function execute($weldLogData, $beforeData = null, array $context = []): array
{
$this->startTime = microtime(true);
$this->context = $context;
Log::info("WeldLog Trigger [{$this->getOrder()}/14] {$this->getName()} - STARTED", [
'weld_log_id' => $weldLogData->id,
'process' => $this->getName()
]);
try {
$result = $this->process($weldLogData, $beforeData, $context);
$this->logCompletion($weldLogData->id, 'success', $result);
return $result;
} catch (\Throwable $th) {
$this->logCompletion($weldLogData->id, 'error', [
'error' => $th->getMessage(),
'file' => $th->getFile(),
'line' => $th->getLine()
]);
throw $th;
}
}
/**
* Determine if trigger should run based on changed fields
*
* @param array $changedFields List of changed field names
* @param bool $isNewRecord Whether this is a new record
* @return bool
*/
public function shouldRun(array $changedFields, bool $isNewRecord): bool
{
if ($isNewRecord) {
return true;
}
$dependentFields = $this->getDependentFields();
// If no dependent fields defined, always run
if (empty($dependentFields)) {
return true;
}
// Check if any changed field is in dependent fields
return !empty(array_intersect($changedFields, $dependentFields));
}
/**
* Check if trigger is async (default: false)
* Override in child classes if needed
*
* @return bool
*/
public function isAsync(): bool
{
return false;
}
/**
* Log trigger completion with performance metrics
*
* @param int $weldLogId Weld log ID
* @param string $status Execution status (success/error)
* @param array $additionalData Additional log data
*/
protected function logCompletion($weldLogId, string $status, array $additionalData = [])
{
$duration = round((microtime(true) - $this->startTime) * 1000, 2);
$logLevel = $status === 'error' ? 'error' : 'info';
Log::$logLevel("WeldLog Trigger [{$this->getOrder()}/14] {$this->getName()} - COMPLETED", array_merge([
'weld_log_id' => $weldLogId,
'process' => $this->getName(),
'status' => $status,
'duration_ms' => $duration,
'duration_sec' => round($duration / 1000, 3)
], $additionalData));
}
}
@@ -0,0 +1,62 @@
<?php
namespace App\Services\WeldLogTriggers\Contracts;
/**
* Interface for WeldLog triggers
*
* Each trigger must implement this interface to be registered
* in the WeldLogTriggerRegistry and managed by WeldLogTriggerManager
*/
interface WeldLogTriggerInterface
{
/**
* Get trigger name for logging purposes
*
* @return string The human-readable name of the trigger
*/
public function getName(): string;
/**
* Get trigger execution order/priority (1-13)
* Lower numbers execute first
*
* @return int The execution order
*/
public function getOrder(): int;
/**
* Check if trigger should run based on changed fields
*
* @param array $changedFields List of field names that changed
* @param bool $isNewRecord Whether this is a new record
* @return bool True if trigger should execute
*/
public function shouldRun(array $changedFields, bool $isNewRecord): bool;
/**
* Get fields that this trigger depends on
* Used to determine if trigger should run when fields change
*
* @return array List of field names this trigger depends on
*/
public function getDependentFields(): array;
/**
* Execute the trigger logic
*
* @param object $weldLogData Current weld log data
* @param object|null $beforeData Previous weld log data (null for new records)
* @param array $context Additional context (changed_fields, is_new_record, etc)
* @return array Result data from trigger execution
*/
public function execute($weldLogData, $beforeData = null, array $context = []): array;
/**
* Check if trigger can be executed asynchronously (queued)
*
* @return bool True if can be queued, false for synchronous execution
*/
public function isAsync(): bool;
}
+284
View File
@@ -0,0 +1,284 @@
# WeldLog Triggers System
## 🎯 Overview
Modular, service-based trigger system for WeldLog save operations. Replaces the monolithic 2,435-line trigger file with 12 separate, maintainable trigger classes.
## 📊 Quick Stats
- **Original**: 1 file, 2,435 lines
- **New System**: 16 files, ~3,600 lines (better organized)
- **Triggers**: 12 independent operations
- **Execution Order**: 1-12 (explicit ordering)
- **Test Coverage**: Ready for unit testing
## 🗂️ Structure
```
app/Services/WeldLogTriggers/
├── Contracts/
│ └── WeldLogTriggerInterface.php # Interface
├── Base/
│ └── BaseTrigger.php # Base class
├── Triggers/
│ ├── SpoolStatusChangerTrigger.php # Order 1
│ ├── LineListsUpdateTrigger.php # Order 2
│ ├── NdeMatrixUpdateTrigger.php # Order 3
│ ├── RequestDateOperationsTrigger.php # Order 4
│ ├── RepairLogsUpdateTrigger.php # Order 5
│ ├── NdeProjectUpdateTrigger.php # Order 6
│ ├── TestPackageOperationsTrigger.php # Order 7
│ ├── ConstructionPaintLogsTrigger.php # Order 8
│ ├── TestPackBaseStatusChangerTrigger.php # Order 9
│ ├── PaintFollowUpsSyncTrigger.php # Order 10
│ ├── HandoversSyncTrigger.php # Order 11
│ └── TestPackCleanupTrigger.php # Order 12
├── WeldLogTriggerRegistry.php # Central registry
├── WeldLogTriggerManager.php # Execution manager
└── README.md # This file
```
## 🚀 Quick Start
### Basic Usage
```php
use App\Services\WeldLogTriggers\WeldLogTriggerManager;
use App\Services\WeldLogTriggers\WeldLogTriggerRegistry;
// Initialize
$registry = new WeldLogTriggerRegistry();
$manager = new WeldLogTriggerManager($registry);
// Execute all triggers
$results = $manager->executeTriggers(
$weldLogData, // Current data
$beforeData, // Previous data (null for new)
$changedFields, // Array of changed field names
$isNewRecord // Boolean
);
```
### Adding a New Trigger
1. Create trigger class in `Triggers/` directory
2. Extend `BaseTrigger`
3. Implement required methods
4. Register in `WeldLogTriggerRegistry.php`
```php
class MyNewTrigger extends BaseTrigger
{
public function getName(): string { return 'My New Trigger'; }
public function getOrder(): int { return 14; }
public function getDependentFields(): array { return ['field1', 'field2']; }
protected function process($data, $beforeData, array $context): array
{
// Your logic here
return ['success' => true];
}
}
```
## ✨ Key Features
### 1. Modular Design
Each trigger is a separate, focused class with single responsibility.
### 2. Automatic Change Detection
System automatically detects which fields changed and runs only relevant triggers.
### 3. Standardized Logging
All triggers use consistent log format with timing information.
### 4. Error Isolation
One trigger's error doesn't affect others (except critical triggers).
### 5. Performance Tracking
Each trigger's execution time is measured and logged separately.
### 6. Future-Ready
Built-in support for async execution (to be implemented).
## 📚 Documentation
### Complete Documentation
See [resources/views/guide/weld-log-triggers-system.md](../../../resources/views/guide/weld-log-triggers-system.md) for:
- Detailed architecture
- Complete trigger descriptions
- API documentation
- Best practices
- Troubleshooting guide
### Migration Guide
See [resources/views/guide/weld-log-triggers-migration.md](../../../resources/views/guide/weld-log-triggers-migration.md) for:
- Step-by-step migration
- Rollback procedures
- Validation checklist
- Common issues and solutions
## 🔍 How It Works
```
1. WeldLog saved in database
↓
2. SaveTrigger called (weld_logs.php)
↓
3. Detect changed fields
↓
4. Initialize Registry & Manager
↓
5. For each trigger (1-12):
- Check if should run (based on changed fields)
- Execute if needed
- Log results
↓
6. Return consolidated results
```
## 🎨 Design Patterns
- **Strategy Pattern**: Each trigger is a strategy
- **Registry Pattern**: Central trigger registry
- **Template Method**: BaseTrigger defines execution template
- **Chain of Responsibility**: Triggers execute in sequence
## 📋 Trigger List
| # | Trigger | Purpose | Avg Time |
|---|---------|---------|----------|
| 1 | Spool Status Changer | Updates spool statuses | 50-100ms |
| 2 | Line Lists Update | Syncs line lists data | 100-200ms |
| 3 | NDE Matrix Update | Manages NDE matrices | 200-500ms |
| 4 | Request Date Operations | Generates request numbers | 100-300ms |
| 5 | Repair Logs Update | Updates repair statuses | 50-100ms |
| 6 | NDE Project Update | Updates project fields | 50-100ms |
| 7 | Test Package Operations | Complex test package logic | 500-2000ms |
| 8 | Construction Paint Logs | Construction paint sync | 200-400ms |
| 9 | Test Pack Base Status | Updates base statuses | 300-600ms |
| 10 | Paint Follow Ups Sync | Comprehensive paint sync | 250-450ms |
| 11 | Handovers Sync | Syncs handover data | 100-200ms |
| 12 | Test Pack Cleanup | Cleans orphaned records | 50-100ms |
**Total**: 2-4 seconds average
## ⚡ Performance
### Optimization Features
- Chunk processing for large datasets
- Transaction retry logic for deadlocks
- Configurable delays between chunks
- Memory management
- Database timeout settings
### Monitoring
Check logs for performance metrics:
```bash
tail -f storage/logs/laravel.log | grep "WeldLog Trigger"
```
Look for:
- `duration_ms`: Execution time per trigger
- `total_execution_time_sec`: Overall time
- `peak_memory_usage_mb`: Memory usage
## 🧪 Testing
### Manual Testing
```php
// Test with real weld log
$weldLog = WeldLog::find(1);
$beforeData = clone $weldLog;
$weldLog->spool_number = 'NEW-SPOOL';
$weldLog->save();
// Check logs for trigger execution
```
### Unit Testing (Future)
Each trigger can be unit tested independently:
```php
$trigger = new SpoolStatusChangerTrigger();
$result = $trigger->execute($data, $beforeData, []);
$this->assertTrue($result['success']);
```
## 🐛 Troubleshooting
### Common Issues
**Trigger not executing?**
- Check `getDependentFields()` includes changed field
- Check logs for "Skipping trigger" messages
- Verify trigger is registered in Registry
**Performance slow?**
- Check chunk sizes in TransactionHelper calls
- Add database indexes
- Monitor slow query log
**Deadlock errors?**
- Ensure queries ordered by 'id ASC'
- Use TransactionHelper with retry logic
- Increase delays between chunks
### Debug Mode
Enable detailed logging:
```php
Log::setDefaultDriver('daily');
Log::info("Debug info", ['data' => $data]);
```
## 📝 Changelog
### Version 1.1.0 (2025-11-15)
- ✅ Consolidated paint follow-up logic (LineList parity)
- ✅ Removed legacy PaintFollowUpTrigger
- ✅ Added cleanup + protection to construction paint sync
- ✅ Updated docs and registry ordering
## 🤝 Contributing
### Adding New Triggers
1. Create class in `Triggers/` directory
2. Extend `BaseTrigger`
3. Implement all required methods
4. Add to Registry
5. Update documentation
6. Test thoroughly
### Code Style
- Follow PSR-12 standards
- Use type hints
- Add PHPDoc comments
- Keep methods focused (single responsibility)
- Log important operations
## 📖 Additional Resources
- [Main Documentation](../../../resources/views/guide/weld-log-triggers-system.md)
- [Migration Guide](../../../resources/views/guide/weld-log-triggers-migration.md)
- [DevQMS Documentation](../../../resources/views/guide/)
## 🔐 Security
- All triggers use prepared statements
- Transaction safety ensured
- Input validation in place
- Error messages don't expose sensitive data
## 📞 Support
For issues or questions:
1. Check documentation
2. Review logs
3. Contact development team
---
**Version**: 1.0.0
**Status**: Production Ready ✅
**Last Updated**: October 30, 2025
**Maintainer**: DevQMS Development Team
@@ -0,0 +1,333 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use App\Models\PaintMatrix;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\Log;
/**
* Construction Paint Logs Trigger
*
* Aligns weld log driven construction paint sync with LineList trigger behaviour.
* - Requires matching Line List + painting cycle
* - Processes each spool once, only when shop joints exist
* - Protects finished records and cleans up orphan entries
*/
class ConstructionPaintLogsTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Construction Paint Logs Sync';
}
public function getOrder(): int
{
return 9;
}
public function getDependentFields(): array
{
return [
'line_number',
'spool_number',
'type_of_joint',
'project',
'design_area',
'iso_number',
'fluid_code',
'painting_cycle',
'test_package_no',
'nps_1',
'nps_2',
'spool_status',
];
}
protected function process($data, $beforeData, array $context): array
{
$constructionPaintLogUpdatedCount = 0;
$constructionPaintLogCreatedCount = 0;
$deletedCleanupCount = 0;
try {
Log::debug('ConstructionPaintLogsTrigger: Starting process', [
'weldLogId' => $data->id
]);
$weldLog = db("weld_logs")->where("id", $data->id)->first();
if (!$weldLog) {
return ['success' => false, 'reason' => 'weld_log_not_found'];
}
if (empty($weldLog->line_number)) {
return ['success' => false, 'reason' => 'line_number_empty'];
}
$lineList = db("line_lists")
->where('line_no', $weldLog->line_number)
->first();
if (!$lineList) {
return ['success' => false, 'reason' => 'line_list_not_found'];
}
if (empty($lineList->painting_cycle)) {
return ['skipped' => true, 'reason' => 'painting_cycle_empty'];
}
$allWeldLogs = db("weld_logs")
->where("line_number", $lineList->line_no)
->where("design_area", $lineList->unit)
->where("fluid_code", $lineList->fluid_code)
->orderBy('id', 'ASC')
->get();
if ($allWeldLogs->isEmpty()) {
return ['skipped' => true, 'reason' => 'no_matching_weld_logs'];
}
$processedSpools = [];
$validShopSpools = $this->getValidShopSpools($lineList->line_no, $lineList->unit, $lineList->fluid_code);
TransactionHelper::chunkTransaction(
$allWeldLogs,
function ($weldLogChunk) use (
$lineList,
&$constructionPaintLogUpdatedCount,
&$constructionPaintLogCreatedCount,
&$processedSpools
) {
foreach ($weldLogChunk as $currentWeldLog) {
if (empty($currentWeldLog->spool_number)) {
continue;
}
if (in_array($currentWeldLog->spool_number, $processedSpools, true)) {
continue;
}
if (!$this->hasShopJoint($lineList->line_no, $currentWeldLog->spool_number)) {
continue;
}
$this->processWeldLogRecord(
$currentWeldLog,
$lineList,
$constructionPaintLogUpdatedCount,
$constructionPaintLogCreatedCount
);
$processedSpools[] = $currentWeldLog->spool_number;
}
return $weldLogChunk->count();
},
1,
10000
);
$deletedCleanupCount = $this->cleanupConstructionRecords($lineList->line_no, $validShopSpools);
} catch (\Throwable $th) {
Log::error("Construction Paint Logs sync error: " . $th->getMessage(), [
'weld_log_id' => $data->id
]);
throw $th;
}
return [
'success' => true,
'created' => $constructionPaintLogCreatedCount,
'updated' => $constructionPaintLogUpdatedCount,
'deleted_cleanup' => $deletedCleanupCount
];
}
protected function processWeldLogRecord($currentWeldLog, $lineList, &$updatedCount, &$createdCount)
{
$paintMatrix = PaintMatrix::where('line', $lineList->line_no)
->where('fluid_code', $lineList->fluid_code)
->first();
$baseConstructionPaintLogData = [
'construction_report_no' => $currentWeldLog->weld_map_no ?? '',
'test_package' => $currentWeldLog->test_package_no ?? '',
'rev' => $lineList->rev ?? '',
'engineering' => $lineList->engineering ?? '',
'area' => $currentWeldLog->project ?? '',
'unit' => $currentWeldLog->design_area ?? '',
'line' => $lineList->line_no,
'iso_drawings' => $currentWeldLog->iso_number ?? '',
'fluid_code' => $lineList->fluid_code ?? '',
'fluid_code_description' => $lineList->fluid_ru ?? '',
'isolation_info' => $lineList->external_finish_type ?? '',
'updated_at' => now()
];
if ($paintMatrix) {
$baseConstructionPaintLogData = array_merge($baseConstructionPaintLogData, [
'painting_system_type_1' => $paintMatrix->paint_cycle ?? '',
'painting_system_type_2' => $paintMatrix->paint_cycle ?? '',
'brend_name_1' => $paintMatrix->brend_name_1 ?? '',
'ral_1' => $paintMatrix->ral_code_1 ?? '',
'color_1' => $paintMatrix->colour_1 ?? '',
'thickness_1' => $paintMatrix->thickness_1 ?? '',
'brend_name_2' => $paintMatrix->brend_name_2 ?? '',
'ral_2' => $paintMatrix->ral_code_2 ?? '',
'color_2' => $paintMatrix->colour_2 ?? '',
'thickness_2' => $paintMatrix->thickness_2 ?? '',
'brend_name_3' => $paintMatrix->brend_name_3 ?? '',
'ral_3' => $paintMatrix->ral_code_3 ?? '',
'color_3' => $paintMatrix->colour_3 ?? '',
'thickness_3' => $paintMatrix->thickness_3 ?? '',
]);
}
$constructionPaintLogData = $baseConstructionPaintLogData;
$constructionPaintLogData['test_package'] = $currentWeldLog->test_package_no ?? '';
$constructionPaintLogData['spool'] = $currentWeldLog->spool_number ?? '';
$constructionPaintLogData['spool_status'] = $currentWeldLog->spool_status ?? 'Waiting';
$constructionPaintLogData['report_no'] = '';
$constructionPaintLogData['dn_1'] = $currentWeldLog->nps_1 ?? '';
$constructionPaintLogData['dn_2'] = $currentWeldLog->nps_2 ?? '';
$constructionPaintLogData['dn_3'] = '';
$uniqueConstraintCondition = [
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number,
'painting_system_type_1' => $paintMatrix->paint_cycle ?? ''
];
$whereCondition = [
'line' => $lineList->line_no,
'spool' => $currentWeldLog->spool_number
];
$existingRecordSameCycle = db("construction_paint_logs")
->where($uniqueConstraintCondition)
->first();
$recordsWithDifferentPaintCycle = db("construction_paint_logs")
->where($whereCondition)
->where('painting_system_type_1', '!=', $paintMatrix->paint_cycle ?? '')
->get();
if ($existingRecordSameCycle) {
$hasAllEmptyDates = $this->hasAllEmptyDates($existingRecordSameCycle);
if ($hasAllEmptyDates) {
db("construction_paint_logs")
->where('id', $existingRecordSameCycle->id)
->update($constructionPaintLogData);
$updatedCount++;
}
return;
}
if ($recordsWithDifferentPaintCycle->count() > 0) {
foreach ($recordsWithDifferentPaintCycle as $oldRecord) {
$hasAnyDateFilled = !$this->hasAllEmptyDates($oldRecord);
if (!$hasAnyDateFilled) {
db("construction_paint_logs")
->where('id', $oldRecord->id)
->update($constructionPaintLogData);
$updatedCount++;
return;
}
}
}
$constructionPaintLogData['created_at'] = now();
db("construction_paint_logs")->insert($constructionPaintLogData);
$createdCount++;
}
protected function hasAllEmptyDates($record): bool
{
$dateFields = [
'blasting_date',
'blasting_finish_date',
'painting_date_1',
'painting_finish_date_1',
'rfi_date_1',
'painting_date_2',
'painting_finish_date_2',
'rfi_date_2',
'painting_date_3',
'painting_finish_date_3',
'rfi_date_3'
];
foreach ($dateFields as $field) {
if (!empty($record->$field)) {
return false;
}
}
return true;
}
protected function hasShopJoint(string $lineNumber, string $spoolNumber): bool
{
return db("weld_logs")
->where('line_number', $lineNumber)
->where('spool_number', $spoolNumber)
->where('type_of_joint', 'S')
->exists();
}
protected function getValidShopSpools(string $lineNumber, ?string $unit, ?string $fluidCode): array
{
return db("weld_logs")
->where('line_number', $lineNumber)
->when($unit, function ($query, $unit) {
return $query->where('design_area', $unit);
})
->when($fluidCode, function ($query, $fluidCode) {
return $query->where('fluid_code', $fluidCode);
})
->where('type_of_joint', 'S')
->whereNotNull('spool_number')
->pluck('spool_number')
->unique()
->values()
->toArray();
}
protected function cleanupConstructionRecords(string $lineNumber, array $validShopSpools): int
{
$query = db("construction_paint_logs")
->where('line', $lineNumber);
if (!empty($validShopSpools)) {
$recordsToEvaluate = $query
->whereNotIn('spool', $validShopSpools)
->get();
} else {
$recordsToEvaluate = $query->get();
}
if ($recordsToEvaluate->isEmpty()) {
return 0;
}
$deletableIds = $recordsToEvaluate
->filter(function ($record) {
return $this->hasAllEmptyDates($record);
})
->pluck('id')
->toArray();
if (empty($deletableIds)) {
return 0;
}
return db("construction_paint_logs")
->whereIn('id', $deletableIds)
->delete();
}
}
@@ -0,0 +1,219 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\Log;
/**
* Handovers Sync Trigger
*
* Syncs data from WeldLogs to Handovers table
* Groups by line_number and creates/updates handover records
*/
class HandoversSyncTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Handovers Sync';
}
public function getOrder(): int
{
return 12;
}
public function getDependentFields(): array
{
return [
];
}
protected function process($data, $beforeData, array $context): array
{
try {
// Get current weld log data
$weldLog = db("weld_logs")->where("id", $data->id)->first();
if (!$weldLog) {
return ['success' => false, 'reason' => 'weld_log_not_found'];
}
// Skip processing if essential fields are missing
if (empty($weldLog->line_number)) {
return ['success' => false, 'reason' => 'line_number_empty'];
}
// Get all weld logs with the same line_number
$allWeldLogs = db("weld_logs")
->where("line_number", $weldLog->line_number)
->orderBy('id', 'ASC') // Deadlock prevention
->get();
// Process each weld log
$handoversCreated = 0;
$handoversUpdated = 0;
// Group by line - handovers are stored by line
$uniqueLines = [];
foreach ($allWeldLogs as $currentWeldLog) {
$lineKey = $currentWeldLog->line_number;
if (!isset($uniqueLines[$lineKey])) {
$uniqueLines[$lineKey] = $currentWeldLog;
}
}
// Process Handovers in chunks with TransactionHelper
TransactionHelper::chunkTransaction(
collect($uniqueLines),
function ($handoverChunk) use (&$handoversCreated, &$handoversUpdated) {
foreach ($handoverChunk as $currentWeldLog) {
$this->processHandoverRecord($currentWeldLog, $handoversCreated, $handoversUpdated);
}
return $handoverChunk->count();
},
10, // Chunk size
10000 // 10ms delay
);
} catch (\Throwable $th) {
Log::error("Handovers sync error: " . $th->getMessage());
// Log detailed error for debugging
Log::error("Error details: ", [
'weldLogId' => $data->id,
'exception' => get_class($th),
'file' => $th->getFile(),
'line' => $th->getLine(),
'trace' => $th->getTraceAsString()
]);
throw $th;
}
return [
'success' => true,
'created' => $handoversCreated,
'updated' => $handoversUpdated
];
}
/**
* Process a single handover record
*/
protected function processHandoverRecord($currentWeldLog, &$createdCount, &$updatedCount)
{
// Check for required field
if (empty($currentWeldLog->line_number)) {
return;
}
// Get line list data (for additional information)
$lineList = db("line_lists")
->where('line_no', $currentWeldLog->line_number)
->first();
// Handover data
$handoverData = [
// Mapping: weld_logs -> handovers
'project' => $currentWeldLog->line_number, // line_number -> location (zone instead)
'work_type' => 'ТРУБКА', // piping_type -> work_type
'object' => $currentWeldLog->project ?? '', // project -> object
'location' => $currentWeldLog->design_area ?? '', // design_area -> project
// Status information
'status1' => 'In Progress',
'updated_at' => now()
];
// Where condition - location (line_number) and object (project) for unique record
$whereCondition = [
'project' => $currentWeldLog->line_number,
];
// Check if existing record exists
$existingRecord = db("handovers")
->where($whereCondition)
->first();
// Calculate volumes logic (mirrors hand-over.blade.php)
$collected = 0;
$notCollected = 1;
if ($existingRecord) {
$collected = (float) $existingRecord->collected_volumes;
// If existing record has specific value, use it, but check the "default to 1 if 0" rule
$notCollected = (float) $existingRecord->not_collected_volumes;
}
// Logic: if not_collected is 0 or empty, make it 1
if (empty($notCollected) || $notCollected == 0) {
$notCollected = 1;
}
$totalVolumes = $collected + $notCollected;
$handoverData['not_collected_volumes'] = $notCollected;
$handoverData['total_volumes'] = $totalVolumes;
// Calculate id_status logic
$handoverControlCount = (int) setting('handover_control_count', 3);
$idStatus = '';
$allAgreed = true;
$hasController = false;
for ($i = 1; $i <= $handoverControlCount; $i++) {
$statusField = "control{$i}_id_status";
$controllerField = "control{$i}_controller";
$statusValue = $existingRecord ? $existingRecord->$statusField : null;
$controllerValue = $existingRecord ? $existingRecord->$controllerField : null;
if ($controllerValue) {
$hasController = true;
if ($statusValue == 'Comment / замечание') {
$idStatus = 'Comment / замечание';
$allAgreed = false;
break;
}
if ($statusValue == 'Revision / Редакция') {
$idStatus = 'Revision / Редакция';
$allAgreed = false;
break;
}
if ($statusValue != 'Agreed / подписано') {
$allAgreed = false;
}
}
}
if ($idStatus === '') {
if ($allAgreed && $hasController) {
$archiveValue = $existingRecord ? $existingRecord->archive : null;
$idStatus = $archiveValue ? $archiveValue : 'Agreed / подписано';
} else {
$idStatus = 'Wait - Not Prepared';
}
}
$handoverData['id_status'] = $idStatus;
if ($existingRecord) {
// Update existing record
db("handovers")
->where('id', $existingRecord->id)
->update($handoverData);
$updatedCount++;
} else {
// Create new record
$handoverData['created_at'] = now();
db("handovers")->insert($handoverData);
$createdCount++;
}
}
}
@@ -0,0 +1,75 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use Illuminate\Support\Facades\Log;
/**
* Line Lists Update Trigger
*
* Updates line_lists table and syncs data from line_lists to weld_logs
* Triggers when line_number or design_area changes
*/
class LineListsUpdateTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Line Lists Update';
}
public function getOrder(): int
{
return 2;
}
public function getDependentFields(): array
{
return [
'line_number',
'design_area',
'fluid_code',
'type_of_welds'
];
}
protected function process($data, $beforeData, array $context): array
{
try {
// Line lists updated_at trigger for cron
db("line_lists")
->where(['line_no'=> $data->line_number])
->where(['unit'=> $data->design_area])
->update(["updated_at" => simdi()]);
// Sync data from Line Lists to WeldLog via view render
$lineListsToWeldLogView = view('cron.line_lists-sync-from-linelists-to-weldlog', [
'line_number' => $data->line_number,
'design_area' => $data->design_area,
'commitSize' => 3, // Chunk size for transaction batching
'triggerMode' => true // Called from SaveTrigger
])->render();
Log::info("Line Lists to WeldLog sync completed via view render", [
'line_number' => $data->line_number,
'design_area' => $data->design_area
]);
return [
'success' => true,
'line_number' => $data->line_number,
'design_area' => $data->design_area
];
} catch (\Throwable $th) {
Log::error("Line Lists sync error: " . $th->getMessage(), [
'line_number' => $data->line_number,
'design_area' => $data->design_area
]);
throw $th;
}
}
}
@@ -0,0 +1,206 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use App\Models\NdeMatrix;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\Log;
/**
* NDE Matrix Update Trigger
*
* Syncs data between Line Lists, NDE Matrix, and WeldLogs
* Handles type_of_welds changes and updates NDE test scope percentages
*/
class NdeMatrixUpdateTrigger extends BaseTrigger
{
public function getName(): string
{
return 'NDE Matrix Update';
}
public function getOrder(): int
{
return 3;
}
public function getDependentFields(): array
{
return [
'type_of_welds',
'fluid_code',
'type_of_joint',
'line_number',
'design_area',
'project'
];
}
protected function process($data, $beforeData, array $context): array
{
try {
// Sync data from Line Lists to NDE Matrix via view render
$ndeMatrixView = view('cron.line_lists-sync-from-linelists-nde-matrix', [
'line_number' => $data->line_number,
'design_area' => $data->design_area,
'commitSize' => 5, // Chunk size for transaction batching
'triggerMode' => true // Called from SaveTrigger
])->render();
Log::info("Line Lists to NDE Matrix sync completed via view render", [
'line_number' => $data->line_number,
'design_area' => $data->design_area
]);
// Special handling for type_of_welds change
if (!is_null($beforeData) && $beforeData->type_of_welds !== $data->type_of_welds) {
TransactionHelper::retryTransaction(function () use ($data, $beforeData) {
// Where condition for old type
$oldTypeWhereData = [
'line' => $data->line_number,
'type_of_joint' => $beforeData->type_of_welds,
'fluid' => $data->fluid_code,
];
// Check if old type is still used by other WeldLog records
$otherRecordsUsingOldType = db("weld_logs")
->where('line_number', $data->line_number)
->where('type_of_welds', $beforeData->type_of_welds)
->where('fluid_code', $data->fluid_code)
->where('id', '!=', $data->id)
->orderBy('id', 'ASC') // Deadlock prevention
->exists();
// Delete from NDE Matrix only if old type is not used elsewhere
if (!$otherRecordsUsingOldType) {
$deleteResult = db("nde_matrices")
->where($oldTypeWhereData)
->delete();
Log::info("NDE Matrix old type cleaned up - no other weld logs using it", [
'line' => $data->line_number,
'old_type_of_joint' => $beforeData->type_of_welds,
'new_type_of_joint' => $data->type_of_welds,
'fluid' => $data->fluid_code,
'delete_result' => $deleteResult
]);
} else {
Log::info("NDE Matrix old type preserved - still used by other weld logs", [
'line' => $data->line_number,
'old_type_of_joint' => $beforeData->type_of_welds,
'new_type_of_joint' => $data->type_of_welds,
'fluid' => $data->fluid_code
]);
}
}, 5); // 5 attempts with exponential backoff
}
} catch (\Throwable $th) {
Log::error("NDE Matrix sync error: " . $th->getMessage(), [
'line_number' => $data->line_number,
'design_area' => $data->design_area
]);
throw $th;
}
// Sync from NDE Matrix to WeldLog
return $this->syncNdeMatrixToWeldLog($data);
}
/**
* Sync NDE Matrix data to WeldLog
* Updates scope percentages and other NDE-related fields
*/
protected function syncNdeMatrixToWeldLog($data): array
{
// fluid_code null check - get from line_lists if needed
$currentFluidCode = $data->fluid_code ?? '';
if(empty($currentFluidCode)) {
$lineListForFluid = db("line_lists")->where('line_no', $data->line_number)->first();
if($lineListForFluid) {
$currentFluidCode = $lineListForFluid->fluid_code ?? '';
}
}
Log::info("NDE Matrix to WeldLog sync started", [
'weld_log_id' => $data->id,
'line_number' => $data->line_number,
'type_of_welds' => $data->type_of_welds,
'fluid_code' => $currentFluidCode
]);
// Skip if fluid_code is null
if(empty($currentFluidCode)) {
Log::info("NDE Matrix to WeldLog sync skipped - fluid_code is null", [
'weld_log_id' => $data->id,
'line_number' => $data->line_number
]);
return ['skipped' => true, 'reason' => 'fluid_code_null'];
}
// Get NDE Matrix records
$ndeMatrices = NdeMatrix::where("line", $data->line_number)
->where("type_of_joint", $data->type_of_welds)
->where("fluid", $currentFluidCode)
->get();
Log::debug("NDE Matrix records retrieved", [
'matrix_count' => $ndeMatrices->count(),
'matrix_examples' => $ndeMatrices->take(2)->toArray()
]);
$ndeToWeldLogUpdateCount = 0;
// Process NDE Matrix in chunks with TransactionHelper
Log::debug("NDE Matrix to WeldLog sync starting", [
'total_records' => $ndeMatrices->count()
]);
TransactionHelper::chunkTransaction(
$ndeMatrices,
function ($ndeMatrixChunk) use (&$ndeToWeldLogUpdateCount) {
foreach($ndeMatrixChunk as $ndeMatrix) {
$weldLogUpdateData = [
'vt_scope' => 100,
'rt_scope' => $ndeMatrix->rt,
'ut_scope' => $ndeMatrix->ut,
'mt_scope' => $ndeMatrix->mt,
'pt_scope' => $ndeMatrix->pt,
'pmi_scope' => $ndeMatrix->pmi,
'ht_scope' => $ndeMatrix->ht,
'pwht' => $ndeMatrix->pwht_field,
'ferrite_scope' => $ndeMatrix->fn,
'ndt_percent' => $ndeMatrix->ndt,
'operating_temperature_s' => $ndeMatrix->operation_temp,
'operating_pressure_mpa' => $ndeMatrix->operations_pressure_kg,
'fluid_group' => $ndeMatrix->piping_group,
'main_material' => $ndeMatrix->material,
'updated_at' => simdi()
];
$result = db("weld_logs")
->where("line_number", $ndeMatrix->line)
->where("type_of_welds", $ndeMatrix->type_of_joint)
->orderBy('id', 'ASC') // Deadlock prevention
->update($weldLogUpdateData);
$ndeToWeldLogUpdateCount += $result;
}
return $ndeMatrixChunk->count();
},
10, // Chunk size increased from 3 to 10 for better performance
10000 // 10ms delay (reduced from 150ms) - sufficient for deadlock prevention
);
Log::info("NDE Matrix to WeldLog sync completed", [
'total_updated_records' => $ndeToWeldLogUpdateCount
]);
return [
'success' => true,
'updated_records' => $ndeToWeldLogUpdateCount
];
}
}
@@ -0,0 +1,63 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* NDE Project Update Trigger
*
* Updates NDE Matrix project field from weld_logs when it's null
* Syncs project information between tables
*/
class NdeProjectUpdateTrigger extends BaseTrigger
{
public function getName(): string
{
return 'NDE Project Update';
}
public function getOrder(): int
{
return 6;
}
public function getDependentFields(): array
{
return [
'line_number',
'project'
];
}
protected function process($data, $beforeData, array $context): array
{
// Update NDE matrices where project is null
$updatedCount = DB::table('nde_matrices')
->join('weld_logs', function($join) {
$join->on('nde_matrices.line', '=', 'weld_logs.line_number')
->on('nde_matrices.design_area', '=', 'weld_logs.design_area');
})
->whereNull('nde_matrices.project') // Find records where project column is empty
->where("weld_logs.id", $data->id)
->update([
'nde_matrices.project' => DB::raw('weld_logs.project') // Update with project value from weld_logs
]);
Log::info("NDE Matrix project field updated", [
'weld_log_id' => $data->id,
'line_number' => $data->line_number,
'design_area' => $data->design_area,
'project' => $data->project,
'updated_records' => $updatedCount
]);
return [
'success' => true,
'updated_records' => $updatedCount
];
}
}
@@ -0,0 +1,143 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use Illuminate\Support\Facades\Log;
/**
* NDT Calculation Cache Trigger
*
* Updates the NDT calculation cache when relevant fields are modified.
* This ensures the frontend displays up-to-date NDT backlog and status information
* without needing to run heavy calculations on every page load.
*/
class NdtCalculationCacheTrigger extends BaseTrigger
{
/**
* Get trigger name
*/
public function getName(): string
{
return 'NdtCalculationCacheTrigger';
}
/**
* Get trigger execution order
* Running as #14 (after all other updates)
*/
public function getOrder(): int
{
return 14;
}
/**
* Get fields that this trigger depends on
* Includes all fields used in ndt-calculation-no-cache.blade.php
*/
public function getDependentFields(): array
{
return [
// Identifiers
'iso_number',
'test_package_no',
'welder_1',
'welder_2',
'type_of_welds',
'welding_date',
// Scopes and Results
'rt_scope', 'rt_result', 'rt_test_date', 'rt_request_date',
'ut_scope', 'ut_result', 'ut_test_date', 'ut_request_date',
'pt_scope', 'pt_result', 'pt_test_date', 'pt_request_date',
'mt_scope', 'mt_result', 'mt_test_date', 'mt_request_date',
'pmi_scope', 'pmi_result', 'pmi_test_date', 'pmi_request_date',
'ferrite_scope', 'ferrite_result', 'ferrite_test_date', 'ferrite_request_date',
'ht_scope', 'ht_result', 'ht_test_date', 'ht_request_date',
'test_laboratory_ht',
'test_laboratory_rt',
'test_laboratory_ut',
'test_laboratory_pt',
'test_laboratory_mt',
'test_laboratory_pmi',
'test_laboratory_ferrite',
'test_laboratory_pwht',
'test_laboratory_vt',
'ut_request_date',
'pt_request_date',
'mt_request_date',
'pmi_request_date',
'ferrite_request_date',
'ht_request_date',
'rt_request_date',
'vt_request_date',
'pwht_request_date',
// PWHT
'pwht', 'pwht_date', 'pwht_result'
];
}
/**
* Process trigger logic
* Dispatches cache update job for NDT calculation
*/
protected function process($weldLogData, $beforeData, array $context): array
{
// Clone işleminde cache hesaplaması yapma
if (isset($context['action']) && $context['action'] === 'clone') {
Log::info('NdtCalculationCacheTrigger: Skipped due to clone action');
return ['skipped' => true, 'reason' => 'clone_action'];
}
$dispatched = false;
// Using the global helper function to dispatch the cache update
// This matches the usage in test2.blade.php provided by the user
if (function_exists('dispatchCacheBladeViews')) {
dispatchCacheBladeViews([
[
'view' => 'admin-ajax.request-ndt-no-cache',
'cache' => 'request-ndt'
],
[
'view' => 'admin-ajax.ndt-order.order-list-no-cache',
'cache' => 'ndt-order-list'
],
[
'view' => 'admin-ajax.repair-log-no-cache',
'cache' => 'repair-log'
],
[
'view' => 'admin-ajax.ndt-calculation-no-cache',
'cache' => 'ndt-calculation'
]
]);
$dispatched = true;
Log::info('NdtCalculationCacheTrigger: Cache update dispatched', [
'weld_log_id' => $weldLogData->id,
'iso' => $weldLogData->iso_number ?? 'unknown'
]);
} else {
Log::warning('NdtCalculationCacheTrigger: dispatchCacheBladeViews function not found');
}
return [
'cache_dispatched' => $dispatched
];
}
/**
* Allow async execution since this is just dispatching another job
*/
public function isAsync(): bool
{
return true;
}
}
@@ -0,0 +1,758 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use App\Models\PaintMatrix;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
/**
* Paint Follow Ups Sync Trigger
*
* Mirrors the LineList trigger logic while running during WeldLog saves:
* - Requires matching Line List + painting cycle
* - Generates SHOP + FIELD entries with temperature/volume data
* - Protects finished records, manages cycle changes, cleans up orphans
*/
class PaintFollowUpsSyncTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Paint Follow Ups Sync';
}
public function getOrder(): int
{
return 11;
}
public function getDependentFields(): array
{
return [
'line_number',
'spool_number',
'no_of_the_joint_as_per_as_built_survey',
'fluid_code',
'iso_number',
'project',
'design_area',
'type_of_joint'
];
}
protected function process($data, $beforeData, array $context): array
{
$paintFollowUpCreated = 0;
$paintFollowUpUpdated = 0;
$deletedOrphanedCount = 0;
$finalCleanupCount = 0;
try {
$weldLog = db("weld_logs")->where("id", $data->id)->first();
if (!$weldLog) {
return ['success' => false, 'reason' => 'weld_log_not_found'];
}
if (empty($weldLog->line_number)) {
return ['success' => false, 'reason' => 'line_number_empty'];
}
$lineList = db("line_lists")
->where('line_no', $weldLog->line_number)
->first();
if (!$lineList) {
return ['success' => false, 'reason' => 'line_list_not_found'];
}
if (empty($lineList->painting_cycle)) {
return ['skipped' => true, 'reason' => 'painting_cycle_empty'];
}
$matchingWeldLogExists = db("weld_logs")
->where("line_number", $lineList->line_no)
->where("design_area", $lineList->unit)
->where("fluid_code", $lineList->fluid_code)
->exists();
if (!$matchingWeldLogExists) {
return ['skipped' => true, 'reason' => 'no_matching_weld_log'];
}
$deletedOrphanedCount = $this->cleanupOrphanedRecords($weldLog, $beforeData);
$resultFields = [
'vt_result',
'rt_result',
'ut_result',
'pt_result',
'mt_result',
'pmi_result',
'ht_result',
'ferrite_result'
];
$allWeldLogs = db("weld_logs")
->where("line_number", $lineList->line_no)
->where("design_area", $lineList->unit)
->where("fluid_code", $lineList->fluid_code)
->where(function ($query) {
$query->where("no_of_the_joint_as_per_as_built_survey", "not like", "%clone%");
})
->where(function ($query) use ($resultFields) {
foreach ($resultFields as $field) {
$query->where(function ($subQuery) use ($field) {
$subQuery->whereNull($field)
->orWhere($field, '')
->orWhere($field, 'Accept / Годен');
});
}
})
->orderBy('id', 'ASC')
->get();
if ($allWeldLogs->isEmpty()) {
return ['skipped' => true, 'reason' => 'no_weld_logs_for_line'];
}
$paintMatrix = PaintMatrix::where('line', $lineList->line_no)
->where('fluid_code', $lineList->fluid_code)
->where('area', $lineList->unit)
->where('paint_cycle', $lineList->painting_cycle)
->first();
if (!$paintMatrix) {
$paintMatrix = PaintMatrix::create([
'project' => $allWeldLogs->first()->project ?? '',
'area' => $lineList->unit,
'description' => "PIPE",
'line' => $lineList->line_no,
'fluid_code_description' => $lineList->fluid_ru,
'fluid_code' => $lineList->fluid_code,
'paint_cycle' => $lineList->painting_cycle,
'created_at' => now(),
'updated_at' => now()
]);
}
$temperatures = j(setting("temperatures"));
$todayTempData = null;
$blastingDateRecord = db('construction_paint_logs')
->where('line', $lineList->line_no)
->whereNotNull('blasting_date')
->first();
if (!is_null($temperatures) && $blastingDateRecord && !empty($blastingDateRecord->blasting_date)) {
$blastingDate = Carbon::parse($blastingDateRecord->blasting_date);
$dayOfYear = $blastingDate->format('z');
$todayTempData = $temperatures[$dayOfYear] ?? null;
}
$fAvgNps = $allWeldLogs->where("type_of_joint", "F")->avg("nps_1");
$sAvgNps = $allWeldLogs->where("type_of_joint", "S")->avg("nps_1");
$mtoTotal = db("m_t_o_s")
->where("description_en", "like", "%pipe%")
->where("line", $lineList->line_no)
->selectRaw("SUM(quantity * POWER(odmm_1/2000, 2) * PI()) AS total")
->first()->total ?? 0;
$fVolume = round(pi() * pow($fAvgNps, 2) * 200, 2);
$sVolume = round($mtoTotal, 2);
$processedPaintFollowUpIds = [];
$processedShopSpools = [];
$replacedJointNumbers = $this->determineReplacedJointNumbers($allWeldLogs);
TransactionHelper::chunkTransaction(
$allWeldLogs,
function ($paintWeldLogChunk) use (
$lineList,
$paintMatrix,
$todayTempData,
$fVolume,
$sVolume,
&$paintFollowUpCreated,
&$paintFollowUpUpdated,
&$processedPaintFollowUpIds,
&$processedShopSpools,
$replacedJointNumbers
) {
foreach ($paintWeldLogChunk as $currentWeldLog) {
if (empty($currentWeldLog->fluid_code)) {
continue;
}
if ($this->shouldSkipJoint($currentWeldLog, $replacedJointNumbers)) {
continue;
}
$basePaintFollowUpData = [
'project' => $currentWeldLog->project ?? '',
'description' => "PIPE",
'area' => $lineList->unit ?? '',
'line' => $lineList->line_no,
'iso_number' => $currentWeldLog->iso_number ?? '',
'fluid_code' => $lineList->fluid_code,
'fluid_code_description' => $lineList->fluid_ru ?? '',
'cycle' => $lineList->painting_cycle ?? '',
'primer_coat_name_1' => $paintMatrix->primer_coat ?? '',
'brend_name_1' => $paintMatrix->brend_name_1 ?? '',
'colour_1' => $paintMatrix->colour_1 ?? '',
'ral_code_1' => $paintMatrix->ral_code_1 ?? '',
'thickness_1' => $paintMatrix->thickness_1 ?? '',
'intermediate_coat_name_2' => $paintMatrix->intermediate_coat ?? '',
'brend_name_2' => $paintMatrix->brend_name_2 ?? '',
'colour_2' => $paintMatrix->colour_2 ?? '',
'ral_code_2' => $paintMatrix->ral_code_2 ?? '',
'thickness_2' => $paintMatrix->thickness_2 ?? '',
'final_coat_name_3' => $paintMatrix->final_coat ?? '',
'brend_name_3' => $paintMatrix->brend_name_3 ?? '',
'colour_3' => $paintMatrix->colour_3 ?? '',
'ral_code_3' => $paintMatrix->ral_code_3 ?? '',
'thickness_3' => $paintMatrix->thickness_3 ?? '',
'status' => 'In Progress',
'updated_at' => now()
];
$result = $this->processShopRecord(
$lineList,
$currentWeldLog,
$basePaintFollowUpData,
$paintMatrix,
$todayTempData,
$sVolume,
$paintFollowUpCreated,
$paintFollowUpUpdated,
$processedShopSpools
);
$paintFollowUpCreated = $result['created'];
$paintFollowUpUpdated = $result['updated'];
$processedShopSpools = $result['processed_spools'];
$fieldResult = $this->processFieldRecord(
$lineList,
$currentWeldLog,
$basePaintFollowUpData,
$paintMatrix,
$todayTempData,
$fVolume,
$paintFollowUpCreated,
$paintFollowUpUpdated,
$processedPaintFollowUpIds
);
$paintFollowUpCreated = $fieldResult['created'];
$paintFollowUpUpdated = $fieldResult['updated'];
$processedPaintFollowUpIds = $fieldResult['processed_ids'];
}
return $paintWeldLogChunk->count();
},
1,
10000
);
$finalCleanupCount = $this->cleanupNonMatchingRecords($lineList->line_no);
} catch (\Throwable $th) {
Log::error("Paint Follow Ups sync error: " . $th->getMessage(), [
'weld_log_id' => $data->id
]);
throw $th;
}
return [
'success' => true,
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'deleted_orphaned' => $deletedOrphanedCount,
'deleted_final_cleanup' => $finalCleanupCount
];
}
protected function processShopRecord(
$lineList,
$weldLog,
$basePaintFollowUpData,
$paintMatrix,
$todayTempData,
$sVolume,
$paintFollowUpCreated,
$paintFollowUpUpdated,
$processedShopSpools
): array {
$hasShopJoint = db("weld_logs")
->where('line_number', $lineList->line_no)
->where('spool_number', $weldLog->spool_number)
->where('type_of_joint', 'S')
->exists();
if (!$hasShopJoint || empty($weldLog->spool_number)) {
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_spools' => $processedShopSpools
];
}
if (in_array($weldLog->spool_number, $processedShopSpools, true)) {
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_spools' => $processedShopSpools
];
}
$processedShopSpools[] = $weldLog->spool_number;
$shopData = $basePaintFollowUpData;
$shopData['location'] = 'SHOP';
$shopData['spool_no_joint_no'] = $weldLog->spool_number;
$shopData['surface_roughness'] = $paintMatrix->surface_preparation ?? '';
if ($todayTempData) {
$shopData['substrate_temprature'] = $todayTempData['temp_material_shop'] ?? null;
$shopData['ambient_temprature'] = $todayTempData['shop_ambient'] ?? null;
}
$shopData['volume_1'] = $sVolume;
$shopData['volume_2'] = $sVolume;
$shopData['volume_3'] = $sVolume;
$shopData['total_volume'] = $sVolume * 3;
$shopUniqueConstraintCondition = [
'line' => $lineList->line_no,
'spool_no_joint_no' => $weldLog->spool_number,
'cycle' => $lineList->painting_cycle,
'location' => 'SHOP'
];
$shopWhereCondition = [
'line' => $lineList->line_no,
'spool_no_joint_no' => $weldLog->spool_number,
'location' => 'SHOP'
];
$existingShopRecord = db("paint_follow_ups")
->where($shopUniqueConstraintCondition)
->first();
$recordsWithDifferentPaintCycle = db("paint_follow_ups")
->where($shopWhereCondition)
->where('cycle', '!=', $lineList->painting_cycle)
->get();
if ($existingShopRecord) {
$hasAllEmptyDates = $this->hasAllEmptyDates($existingShopRecord);
if ($hasAllEmptyDates) {
db("paint_follow_ups")
->where('id', $existingShopRecord->id)
->update($shopData);
$paintFollowUpUpdated++;
} else {
$tempVolumeData = $this->getTempVolumeUpdateData(
$existingShopRecord,
$todayTempData,
$sVolume,
'SHOP'
);
if (!empty($tempVolumeData)) {
db("paint_follow_ups")
->where('id', $existingShopRecord->id)
->update($tempVolumeData);
}
}
} else {
if ($recordsWithDifferentPaintCycle->count() > 0) {
foreach ($recordsWithDifferentPaintCycle as $oldRecord) {
$hasAnyDateFilled = !$this->hasAllEmptyDates($oldRecord);
if ($hasAnyDateFilled) {
db("paint_follow_ups")
->where('id', $oldRecord->id)
->update([
'status' => 'HOLD',
'updated_at' => now()
]);
} else {
db("paint_follow_ups")
->where('id', $oldRecord->id)
->update([
'cycle' => $lineList->painting_cycle,
'updated_at' => now()
]);
$paintFollowUpUpdated++;
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_spools' => $processedShopSpools
];
}
}
}
$shopData['primer_coating_start_date'] = null;
$shopData['primer_coating_finish_date'] = null;
$shopData['start_intermediate_date2'] = null;
$shopData['finish_intermediate_date2'] = null;
$shopData['final_coat_start_date3'] = null;
$shopData['final_coat_finish_date3'] = null;
$shopData['created_at'] = now();
$shopData['updated_at'] = now();
db("paint_follow_ups")->insert($shopData);
$paintFollowUpCreated++;
}
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_spools' => $processedShopSpools
];
}
protected function processFieldRecord(
$lineList,
$weldLog,
$basePaintFollowUpData,
$paintMatrix,
$todayTempData,
$fVolume,
$paintFollowUpCreated,
$paintFollowUpUpdated,
$processedPaintFollowUpIds
): array {
if (empty($weldLog->no_of_the_joint_as_per_as_built_survey)) {
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_ids' => $processedPaintFollowUpIds
];
}
$jointNo = $weldLog->no_of_the_joint_as_per_as_built_survey;
$fieldData = $basePaintFollowUpData;
$fieldData['location'] = 'FIELD';
$fieldData['spool_no_joint_no'] = $jointNo;
$fieldData['surface_roughness'] = $paintMatrix->touch_up_of_damaged_parts ?? '';
if ($todayTempData) {
$fieldData['substrate_temprature'] = $todayTempData['temp_material_field'] ?? null;
$fieldData['ambient_temprature'] = $todayTempData['field_ambient'] ?? null;
}
$fieldData['volume_1'] = $fVolume;
$fieldData['volume_2'] = $fVolume;
$fieldData['volume_3'] = $fVolume;
$fieldData['total_volume'] = $fVolume * 3;
$fieldUniqueConstraintCondition = [
'line' => $lineList->line_no,
'spool_no_joint_no' => $jointNo,
'cycle' => $lineList->painting_cycle,
'location' => 'FIELD'
];
$fieldWhereCondition = [
'line' => $lineList->line_no,
'spool_no_joint_no' => $jointNo,
'location' => 'FIELD'
];
$existingFieldRecord = db("paint_follow_ups")
->where($fieldUniqueConstraintCondition)
->first();
$recordsWithDifferentPaintCycle = db("paint_follow_ups")
->where($fieldWhereCondition)
->where('cycle', '!=', $lineList->painting_cycle)
->get();
if ($existingFieldRecord) {
if (in_array($existingFieldRecord->id, $processedPaintFollowUpIds)) {
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_ids' => $processedPaintFollowUpIds
];
}
$processedPaintFollowUpIds[] = $existingFieldRecord->id;
$hasAllEmptyDates = $this->hasAllEmptyDates($existingFieldRecord);
if ($hasAllEmptyDates) {
db("paint_follow_ups")
->where('id', $existingFieldRecord->id)
->update($fieldData);
$paintFollowUpUpdated++;
} else {
$tempVolumeData = $this->getTempVolumeUpdateData(
$existingFieldRecord,
$todayTempData,
$fVolume,
'FIELD'
);
if (!empty($tempVolumeData)) {
db("paint_follow_ups")
->where('id', $existingFieldRecord->id)
->update($tempVolumeData);
}
}
} else {
if ($recordsWithDifferentPaintCycle->count() > 0) {
foreach ($recordsWithDifferentPaintCycle as $oldRecord) {
$hasAnyDateFilled = !$this->hasAllEmptyDates($oldRecord);
if ($hasAnyDateFilled) {
db("paint_follow_ups")
->where('id', $oldRecord->id)
->update([
'status' => 'HOLD',
'updated_at' => now()
]);
} else {
db("paint_follow_ups")
->where('id', $oldRecord->id)
->update([
'cycle' => $lineList->painting_cycle,
'updated_at' => now()
]);
$paintFollowUpUpdated++;
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_ids' => $processedPaintFollowUpIds
];
}
}
}
$fieldData['primer_coating_start_date'] = null;
$fieldData['primer_coating_finish_date'] = null;
$fieldData['start_intermediate_date2'] = null;
$fieldData['finish_intermediate_date2'] = null;
$fieldData['final_coat_start_date3'] = null;
$fieldData['final_coat_finish_date3'] = null;
$fieldData['created_at'] = now();
$fieldData['updated_at'] = now();
db("paint_follow_ups")->insert($fieldData);
$paintFollowUpCreated++;
}
return [
'created' => $paintFollowUpCreated,
'updated' => $paintFollowUpUpdated,
'processed_ids' => $processedPaintFollowUpIds
];
}
protected function cleanupOrphanedRecords($weldLog, $beforeData): int
{
if (!$beforeData) {
return 0;
}
$deletedCount = 0;
try {
if (isset($beforeData->no_of_the_joint_as_per_as_built_survey) &&
$beforeData->no_of_the_joint_as_per_as_built_survey != $weldLog->no_of_the_joint_as_per_as_built_survey &&
!empty($beforeData->no_of_the_joint_as_per_as_built_survey)) {
$oldJointNo = $beforeData->no_of_the_joint_as_per_as_built_survey;
$deleted = db("paint_follow_ups")
->where('line', $weldLog->line_number)
->where('spool_no_joint_no', $oldJointNo)
->where('location', 'FIELD')
->delete();
$deletedCount += $deleted;
}
if (isset($beforeData->spool_number) &&
$beforeData->spool_number != $weldLog->spool_number &&
!empty($beforeData->spool_number)) {
$oldSpoolNumber = $beforeData->spool_number;
$deleted = db("paint_follow_ups")
->where('line', $weldLog->line_number)
->where('spool_no_joint_no', $oldSpoolNumber)
->where('location', 'SHOP')
->delete();
$deletedCount += $deleted;
}
} catch (\Throwable $th) {
Log::warning("Failed to cleanup orphaned records", [
'error' => $th->getMessage(),
'weld_log_id' => $weldLog->id
]);
}
return $deletedCount;
}
protected function cleanupNonMatchingRecords(string $lineNumber): int
{
$deletedCount = 0;
try {
$validFieldJointNos = db("weld_logs")
->where("line_number", $lineNumber)
->whereNotNull("no_of_the_joint_as_per_as_built_survey")
->where("no_of_the_joint_as_per_as_built_survey", "!=", "")
->pluck("no_of_the_joint_as_per_as_built_survey")
->toArray();
$validShopSpoolNos = db("weld_logs")
->where("line_number", $lineNumber)
->where("type_of_joint", "S")
->whereNotNull("spool_number")
->where("spool_number", "!=", "")
->pluck("spool_number")
->toArray();
if (!empty($validFieldJointNos)) {
$deletedField = db("paint_follow_ups")
->where('line', $lineNumber)
->where('location', 'FIELD')
->whereNotIn('spool_no_joint_no', $validFieldJointNos)
->delete();
$deletedCount += $deletedField;
} else {
$deletedField = db("paint_follow_ups")
->where('line', $lineNumber)
->where('location', 'FIELD')
->delete();
$deletedCount += $deletedField;
}
if (!empty($validShopSpoolNos)) {
$deletedShop = db("paint_follow_ups")
->where('line', $lineNumber)
->where('location', 'SHOP')
->whereNotIn('spool_no_joint_no', $validShopSpoolNos)
->delete();
$deletedCount += $deletedShop;
} else {
$deletedShop = db("paint_follow_ups")
->where('line', $lineNumber)
->where('location', 'SHOP')
->delete();
$deletedCount += $deletedShop;
}
} catch (\Throwable $th) {
Log::warning("Failed to cleanup non-matching records", [
'error' => $th->getMessage(),
'line_number' => $lineNumber
]);
}
return $deletedCount;
}
protected function hasAllEmptyDates($record): bool
{
return empty($record->primer_coating_start_date) &&
empty($record->primer_coating_finish_date) &&
empty($record->start_intermediate_date2) &&
empty($record->finish_intermediate_date2) &&
empty($record->final_coat_start_date3) &&
empty($record->final_coat_finish_date3);
}
protected function getTempVolumeUpdateData($record, $todayTempData, $volume, $location): array
{
$updateData = [];
if ($todayTempData) {
$tempField = $location === 'SHOP' ? 'temp_material_shop' : 'temp_material_field';
$ambientField = $location === 'SHOP' ? 'shop_ambient' : 'field_ambient';
if (empty($record->substrate_temprature) && isset($todayTempData[$tempField])) {
$updateData['substrate_temprature'] = $todayTempData[$tempField];
}
if (empty($record->ambient_temprature) && isset($todayTempData[$ambientField])) {
$updateData['ambient_temprature'] = $todayTempData[$ambientField];
}
}
if (empty($record->volume_1)) {
$updateData['volume_1'] = $volume;
}
if (empty($record->volume_2)) {
$updateData['volume_2'] = $volume;
}
if (empty($record->volume_3)) {
$updateData['volume_3'] = $volume;
}
if (empty($record->total_volume)) {
$updateData['total_volume'] = $volume * 3;
}
return $updateData;
}
protected function determineReplacedJointNumbers($weldLogs): array
{
$toSkip = [];
foreach ($weldLogs as $log) {
$jointNo = $log->no_of_the_joint_as_per_as_built_survey;
if ($this->isRepairJoint($jointNo)) {
$baseJoint = $this->stripRepairSuffix($jointNo);
if (!empty($baseJoint)) {
$toSkip[] = $baseJoint;
}
}
}
return array_unique($toSkip);
}
protected function shouldSkipJoint($weldLog, array $replacedJointNumbers): bool
{
$jointNo = $weldLog->no_of_the_joint_as_per_as_built_survey;
if (empty($jointNo)) {
return false;
}
return in_array($jointNo, $replacedJointNumbers, true);
}
protected function isRepairJoint(?string $jointNo): bool
{
return !empty($jointNo) && strpos($jointNo, 'R') !== false;
}
protected function stripRepairSuffix(?string $jointNo): ?string
{
if (empty($jointNo)) {
return $jointNo;
}
$pos = strpos($jointNo, 'R');
if ($pos === false) {
return $jointNo;
}
return substr($jointNo, 0, $pos);
}
}
@@ -0,0 +1,92 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use Illuminate\Support\Facades\Log;
/**
* Register Creator Cache Trigger
*
* Updates the Register Creator cache when relevant fields are modified.
* This ensures the frontend displays up-to-date information in the register creator view.
*/
class RegisterCreatorCacheTrigger extends BaseTrigger
{
/**
* Get trigger name
*/
public function getName(): string
{
return 'RegisterCreatorCacheTrigger';
}
/**
* Get trigger execution order
* Running as #15 (after all other updates)
*/
public function getOrder(): int
{
return 15;
}
/**
* Get fields that this trigger depends on
*/
public function getDependentFields(): array
{
return [
'nps_1',
'welding_date',
'test_package_no',
'date_test',
'line_number',
'iso_number'
];
}
/**
* Process trigger logic
* Dispatches cache update job for Register Creator
*/
protected function process($weldLogData, $beforeData, array $context): array
{
// Skip for clone action
if (isset($context['action']) && $context['action'] === 'clone') {
Log::info('RegisterCreatorCacheTrigger: Skipped due to clone action');
return ['skipped' => true, 'reason' => 'clone_action'];
}
$dispatched = false;
if (function_exists('dispatchCacheBladeViews')) {
dispatchCacheBladeViews([
[
'view' => 'admin-ajax.register-no-cache',
'cache' => 'register-creator'
]
]);
$dispatched = true;
Log::info('RegisterCreatorCacheTrigger: Cache update dispatched', [
'weld_log_id' => $weldLogData->id,
'iso' => $weldLogData->iso_number ?? 'unknown'
]);
} else {
Log::warning('RegisterCreatorCacheTrigger: dispatchCacheBladeViews function not found');
}
return [
'cache_dispatched' => $dispatched
];
}
/**
* Allow async execution since this is just dispatching another job
*/
public function isAsync(): bool
{
return true;
}
}
@@ -0,0 +1,114 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use Illuminate\Support\Facades\Log;
/**
* Repair Logs Update Trigger
*
* Updates repair logs based on test results
* Determines repair status: "Not Done", "Done", or "Repair"
*/
class RepairLogsUpdateTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Repair Logs Update';
}
public function getOrder(): int
{
return 5;
}
public function getDependentFields(): array
{
return [
'iso_number',
'no_of_the_joint_as_per_as_built_survey',
'welding_date',
'vt_result',
'rt_result',
'ut_result',
'pt_result',
'mt_result',
'pmi_result',
'ht_result',
'ferrite_result',
];
}
protected function process($data, $beforeData, array $context): array
{
// Test result fields to check
$testResultFields = [
'vt_result',
'rt_result',
'ut_result',
'pt_result',
'mt_result',
'pmi_result',
'ht_result',
'ferrite_result'
];
$hasRepairOrCut = false;
$allFieldsEmpty = true;
$rtAndUtEmpty = false;
// Check if RT or UT results are empty
if(empty($data->rt_result) && empty($data->ut_result)) {
$rtAndUtEmpty = true;
}
// Check all test result fields
foreach($testResultFields as $field) {
if(isset($data->$field) && !empty($data->$field)) {
$allFieldsEmpty = false;
if(in_array($data->$field, ['Repair / Ремонт', 'Cut / Резать'])) {
$hasRepairOrCut = true;
break;
}
}
}
// Determine repair status based on test results
if($allFieldsEmpty || $rtAndUtEmpty) {
$repairStatus = "Not Done";
} elseif(!$hasRepairOrCut) {
$repairStatus = "Done";
} else {
$repairStatus = "Repair";
}
// Update repair logs
$updateResult = db("repair_logs")
->where([
'iso_number' => $data->iso_number,
'new_joint_no' => $data->no_of_the_joint_as_per_as_built_survey,
])
->update([
'repair_date' => $data->welding_date,
'repair_status' => $repairStatus,
]);
Log::info("Repair logs updated", [
'iso_number' => $data->iso_number,
'joint_no' => $data->no_of_the_joint_as_per_as_built_survey,
'repair_status' => $repairStatus,
'updated_records' => $updateResult,
'has_repair_or_cut' => $hasRepairOrCut,
'all_fields_empty' => $allFieldsEmpty,
'rt_and_ut_empty' => $rtAndUtEmpty
]);
return [
'success' => true,
'repair_status' => $repairStatus,
'updated_records' => $updateResult
];
}
}
@@ -0,0 +1,384 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use Illuminate\Support\Facades\Log;
/**
* Request Date Operations Trigger
*
* Handles request number generation for all test types (RT, UT, MT, PT, VT, PWHT, Ferrite, PMI)
* Updates request numbers in both weld_logs and test-specific tables
*/
class RequestDateOperationsTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Request Date Operations';
}
public function getOrder(): int
{
return 4;
}
public function getDependentFields(): array
{
return [
'iso_number',
'no_of_the_joint_as_per_as_built_survey',
'rt_request_date', 'rt_request_no', 'test_laboratory_rt',
'ht_request_date', 'ht_request_no', 'test_laboratory_ht',
'ut_request_date', 'ut_request_no', 'test_laboratory_ut',
'mt_request_date', 'mt_request_no', 'test_laboratory_mt',
'pt_request_date', 'pt_request_no', 'test_laboratory_pt',
'vt_request_date', 'vt_request_no', 'test_laboratory_vt',
'pwht_request_date', 'pwht_request_no', 'test_laboratory_pwht',
'ferrite_request_date', 'ferrite_request_no', 'test_laboratory_ferrite',
'pmi_request_date', 'pmi_request_no', 'test_laboratory_pmi',
'rt_test_date',
'ut_test_date',
'mt_test_date',
'pt_test_date',
'vt_test_date',
'pwht_test_date',
'pmi_test_date',
'ferrite_test_date',
];
}
protected function process($data, $beforeData, array $context): array
{
$logs_request_number_pattern = setting("logs_request_number_pattern");
$logTestTypes = log_test_types();
Log::info("Available test types for request date processing", [
'test_types' => array_keys($logTestTypes),
'iso_number' => $data->iso_number,
'joint_no' => $data->no_of_the_joint_as_per_as_built_survey
]);
$testNames = array_keys($logTestTypes);
$processedTests = [];
foreach($testNames AS $testName) {
$result = $this->processTestType(
$testName,
$logTestTypes[$testName],
$data,
$beforeData,
$logs_request_number_pattern
);
if ($result['processed']) {
$processedTests[] = $testName;
}
}
return [
'success' => true,
'processed_test_types' => $processedTests,
'total_processed' => count($processedTests)
];
}
/**
* Process a single test type for request number generation
*/
protected function processTestType(
string $testName,
string $testTableName,
$data,
$beforeData,
string $logs_request_number_pattern
): array {
$requestDate = $testName . "_request_date";
$run = false;
Log::info("Processing test type", [
'test_name' => $testName,
'request_date_field' => $requestDate,
'current_request_date' => $data->$requestDate ?? 'null',
'before_request_date' => $beforeData->$requestDate ?? 'null'
]);
// Check if request date is not empty
if($data->$requestDate != "") {
$requestNo = $testName . "_request_no";
$laboratoryCompany = "test_laboratory_" . $testName;
Log::info("Request date is not empty, checking conditions", [
'test_name' => $testName,
'request_date' => $data->$requestDate,
'current_request_no' => $data->$requestNo ?? 'null',
'current_laboratory' => $data->$laboratoryCompany ?? 'null',
'before_laboratory' => $beforeData->$laboratoryCompany ?? 'null'
]);
// Check conditions to run
if($data->$requestNo == "") {
// Request no not assigned yet
$run = true;
Log::info("Run condition met: Request number is empty", [
'test_name' => $testName,
'reason' => 'request_no_empty'
]);
}
if($data->$laboratoryCompany != $beforeData->$laboratoryCompany) {
// Laboratory changed
$run = true;
Log::info("Run condition met: Laboratory changed", [
'test_name' => $testName,
'reason' => 'laboratory_changed',
'old_laboratory' => $beforeData->$laboratoryCompany ?? 'null',
'new_laboratory' => $data->$laboratoryCompany ?? 'null'
]);
}
$requestDateChange = false;
if($data->$requestDate != $beforeData->$requestDate) {
// Request date changed
$run = true;
Log::info("Run condition met: Request date changed", [
'test_name' => $testName,
'reason' => 'request_date_changed',
'old_date' => $beforeData->$requestDate ?? 'null',
'new_date' => $data->$requestDate
]);
// Check if this date was already used
$isOldSignReqDate = db("weld_logs")
->where([
$requestDate => $data->$requestDate,
$laboratoryCompany => $data->$laboratoryCompany,
])
->where("id", "<>", $data->id)
->first();
if(!$isOldSignReqDate) {
$requestDateChange = true;
Log::info("Request date is unique, will generate new request number", [
'test_name' => $testName,
'request_date' => $data->$requestDate,
'laboratory' => $data->$laboratoryCompany ?? 'null'
]);
} else {
Log::info("Request date already exists, will reuse existing request number", [
'test_name' => $testName,
'request_date' => $data->$requestDate,
'laboratory' => $data->$laboratoryCompany ?? 'null',
'existing_record_id' => $isOldSignReqDate->id ?? 'null'
]);
}
}
if($run) {
$this->generateAndUpdateRequestNumber(
$testName,
$testTableName,
$data,
$requestDate,
$requestNo,
$laboratoryCompany,
$requestDateChange,
$logs_request_number_pattern
);
return ['processed' => true, 'test_name' => $testName];
} else {
Log::info("Request number generation skipped", [
'test_name' => $testName,
'reason' => 'run_condition_false',
'current_request_date' => $data->$requestDate ?? 'null',
'current_request_no' => $data->$requestNo ?? 'null'
]);
}
} else {
Log::info("Test processing skipped", [
'test_name' => $testName,
'reason' => 'request_date_empty',
'request_date_field' => $requestDate
]);
}
return ['processed' => false];
}
/**
* Generate and update request number for a test type
*/
protected function generateAndUpdateRequestNumber(
string $testName,
string $testTableName,
$data,
string $requestDate,
string $requestNo,
string $laboratoryCompany,
bool $requestDateChange,
string $logs_request_number_pattern
) {
Log::info("Starting request number generation process", [
'test_name' => $testName,
'request_date_changed' => $requestDateChange,
'current_request_no' => $data->$requestNo ?? 'null',
'laboratory' => $data->$laboratoryCompany ?? 'null'
]);
$thisRequestNumberPattern = $logs_request_number_pattern;
// Get company code
$companyCode = db("subcontractors")
->where("company_name_en", $data->$laboratoryCompany)
->first();
if(!is_null($companyCode)) {
$companyCode = $companyCode->company_code;
} else {
$companyCode = "";
}
Log::info("Company code resolved", [
'test_name' => $testName,
'laboratory_name' => $data->$laboratoryCompany ?? 'null',
'company_code' => $companyCode
]);
$thisRequestNumberPattern = str_replace("{company_code}", $companyCode, $thisRequestNumberPattern);
// Get counter
if($requestDateChange) {
// Different date, get new counter
$lastRequestNumber = get_counter($companyCode . $testName);
Log::info("Getting new counter for date change", [
'test_name' => $testName,
'counter_key' => $companyCode . $testName,
'new_counter' => $lastRequestNumber
]);
} else {
// Same date, get existing or new counter
$lastRequestNumber = get_counter($companyCode . $testName, "");
Log::info("Getting existing or new counter for same date", [
'test_name' => $testName,
'counter_key' => $companyCode . $testName,
'counter' => $lastRequestNumber
]);
}
$thisRequestNumberPattern = str_replace("{number}", $lastRequestNumber, $thisRequestNumberPattern);
$thisRequestNumberPattern = str_replace("{log_name}", strtoupper($testName), $thisRequestNumberPattern);
Log::info("Request number pattern generated", [
'test_name' => $testName,
'pattern' => $thisRequestNumberPattern,
'counter' => $lastRequestNumber
]);
// Check if same company/date already has a request number today
$todayDataThisCompanyThisTest = db("weld_logs")->where([
$requestDate => $data->$requestDate,
$laboratoryCompany => $data->$laboratoryCompany,
])->first();
if($todayDataThisCompanyThisTest) {
if($todayDataThisCompanyThisTest->$requestNo !="") {
$thisRequestNumberPattern = $todayDataThisCompanyThisTest->$requestNo;
Log::info("Reusing existing request number for same date/company", [
'test_name' => $testName,
'existing_request_no' => $thisRequestNumberPattern,
'existing_record_id' => $todayDataThisCompanyThisTest->id
]);
}
}
// Update weld_logs
$whereData = [
'iso_number' => $data->iso_number,
'no_of_the_joint_as_per_as_built_survey' => $data->no_of_the_joint_as_per_as_built_survey,
];
$updateData = [
$requestNo => $thisRequestNumberPattern,
"updated_at" => simdi()
];
Log::info("Updating weld_logs with request number", [
'test_name' => $testName,
'where_data' => $whereData,
'update_data' => $updateData,
'final_request_number' => $thisRequestNumberPattern
]);
$weldLogUpdateResult = db("weld_logs")
->where($whereData)
->update($updateData);
Log::info("Weld_logs update completed", [
'test_name' => $testName,
'affected_rows' => $weldLogUpdateResult,
'request_number' => $thisRequestNumberPattern
]);
// Update test table
$weldLogData = db("weld_logs")->where($whereData)->first();
$testTableUpdateData = [];
$testTableColumns = table_columns($testTableName);
foreach($weldLogData AS $weldLogColumn => $weldLogValue)
{
if(in_array($weldLogColumn, $testTableColumns))
{
$testTableUpdateData[$weldLogColumn] = $weldLogValue;
}
}
unset($testTableUpdateData['id']);
// Check if control_standart exists in test table columns
if(in_array('control_standart', $testTableColumns)) {
$ndeMatrixInfo = db("nde_matrices")
->where("line", $weldLogData->line_number)
->where("type_of_joint", $weldLogData->type_of_welds)
->first();
if ($ndeMatrixInfo) {
$testTableUpdateData['control_standart'] = $ndeMatrixInfo->control_standart;
Log::info("Added control_standart from NDE Matrix", [
'test_name' => $testName,
'control_standart' => $ndeMatrixInfo->control_standart
]);
} else {
Log::warning("Control standart not found in NDE Matrix", [
'test_name' => $testName,
'line' => $weldLogData->line_number,
'type_of_joint' => $weldLogData->type_of_welds
]);
}
} else {
Log::warning("Control standart column not found in test table", [
'test_name' => $testName,
'test_table' => $testTableName
]);
}
Log::info("Updating test table with weld log data", [
'test_name' => $testName,
'test_table' => $testTableName,
'where_data' => $whereData,
'update_fields_count' => count($testTableUpdateData)
]);
$testTableUpdateResult = db($testTableName)
->updateOrInsert($whereData, $testTableUpdateData);
Log::info("Test table update completed", [
'test_name' => $testName,
'test_table' => $testTableName,
'operation_result' => $testTableUpdateResult ? 'success' : 'failed'
]);
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use Illuminate\Support\Facades\Log;
/**
* Spool Status Changer Trigger
*
* Handles spool status updates when spool_number, iso_number, or type_of_joint changes
* Also triggers the spool-status-changer cron view for both new and old values
*/
class SpoolStatusChangerTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Spool Status Changer';
}
public function getOrder(): int
{
return 14;
}
public function getDependentFields(): array
{
return [
'spool_number',
'no_of_the_joint_as_per_as_built_survey',
'iso_number',
'type_of_joint',
'line_number',
'project',
'welding_date',
'welder_1',
'welder_2',
'real_welder_1',
'real_welder_2',
'design_area'
];
}
protected function process($data, $beforeData, array $context): array
{
$triggerSpoolStatusChanger = false;
$spoolStatusChangerParams = [];
// spool_number change check
if (isset($data->spool_number)) {
if(isset($data->iso_number)) {
$triggerSpoolStatusChanger = true;
spoolStatusChanger($data->iso_number, $data->spool_number);
Log::info("Spool Status Changer triggered", [
'iso_number' => $data->iso_number,
'spool_number' => $data->spool_number
]);
}
}
return [
'success' => true,
'triggered' => $triggerSpoolStatusChanger
];
}
}
@@ -0,0 +1,316 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use App\Models\TestPackage;
use App\Models\TestPackBaseStatus;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\Log;
/**
* Test Pack Base Status Changer Trigger
*
* Updates test package base statuses including:
* - WDI (Weld Diameter Inch) calculations
* - Welding progress
* - Repair statistics
* - Shop/Field weld tracking
*/
class TestPackBaseStatusChangerTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Test Pack Base Status Changer';
}
public function getOrder(): int
{
return 9;
}
public function getDependentFields(): array
{
return [
'test_package_no',
'iso_number',
'line_number',
'welding_date',
'type_of_joint',
'nps_1'
];
}
protected function process($data, $beforeData, array $context): array
{
$testPackageNo = $data->test_package_no;
$lineNumber = $data->line_number;
$testPackages = TestPackage::where("test_package_number", $testPackageNo)->get();
$testPackagesIso = TestPackBaseStatus::where("test_package_no", $testPackageNo)->get();
$weldLogs = db("weld_logs")
->where("test_package_no", $testPackageNo)
->get();
$repairLogs = db("repair_logs")
->where("test_package_no", $testPackageNo)
->get();
$testPackagesSummary = [];
$testPackagesIsoSummary = [];
$totalFields = [
'total_wdi',
'total_complated_wdi',
'welding_progress',
'total_shop_wdi',
'total_complated_shop_wdi',
'total_field_wdi',
'total_complated_field_wdi',
];
// Calculate repair log summaries
$repairSummaries = $this->calculateRepairSummaries($repairLogs);
// Calculate weld log summaries
foreach($weldLogs AS $weldLog) {
foreach($totalFields AS $field) {
if(!isset($testPackagesSummary[$weldLog->test_package_no][$field]))
$testPackagesSummary[$weldLog->test_package_no][$field] = 0;
if(!isset($testPackagesIsoSummary[$weldLog->iso_number][$weldLog->test_package_no][$field]))
$testPackagesIsoSummary[$weldLog->iso_number][$weldLog->test_package_no][$field] = 0;
}
$wdi = (float) $weldLog->nps_1;
$testPackagesSummary[$weldLog->test_package_no]['total_wdi'] += $wdi ;
$testPackagesIsoSummary[$weldLog->iso_number][$weldLog->test_package_no]['total_wdi'] += $wdi;
if($weldLog->type_of_joint == "F") {
$testPackagesSummary[$weldLog->test_package_no]['total_field_wdi'] += $wdi;
$testPackagesIsoSummary[$weldLog->iso_number][$weldLog->test_package_no]['total_field_wdi'] += $wdi ;
if(!rejected_date($weldLog->welding_date)) {
$testPackagesSummary[$weldLog->test_package_no]['total_complated_field_wdi'] += $wdi;
$testPackagesIsoSummary[$weldLog->iso_number][$weldLog->test_package_no]['total_complated_field_wdi'] += $wdi;
}
}
if($weldLog->type_of_joint == "S") {
$testPackagesSummary[$weldLog->test_package_no]['total_shop_wdi'] += $wdi;
$testPackagesIsoSummary[$weldLog->iso_number][$weldLog->test_package_no]['total_shop_wdi'] += $wdi;
if(!rejected_date($weldLog->welding_date)) {
$testPackagesSummary[$weldLog->test_package_no]['total_complated_shop_wdi'] += $wdi;
$testPackagesIsoSummary[$weldLog->iso_number][$weldLog->test_package_no]['total_complated_shop_wdi'] += $wdi;
}
}
if(!rejected_date($weldLog->welding_date)) {
$testPackagesSummary[$weldLog->test_package_no]['total_complated_wdi'] += $wdi;
$testPackagesIsoSummary[$weldLog->iso_number][$weldLog->test_package_no]['total_complated_wdi'] += $wdi;
}
// Welding progress calculation
$totalWdi = $testPackagesSummary[$weldLog->test_package_no]['total_wdi'];
$totalComplatedWdi = $testPackagesSummary[$weldLog->test_package_no]['total_complated_wdi'];
if ($totalWdi > 0) {
$testPackagesSummary[$weldLog->test_package_no]['welding_progress'] =
round(($totalComplatedWdi * 100) / $totalWdi, 2);
} else {
$testPackagesSummary[$weldLog->test_package_no]['welding_progress'] = 0;
}
}
// Update test pack base statuses
$this->updateTestPackBaseStatuses($testPackagesIsoSummary, $repairSummaries);
// Update test packages summary
$this->updateTestPackagesSummary($testPackagesSummary, $repairSummaries['repairLogsSummary2']);
// Update test packages with WDI fields and status
$processedCount = $this->updateTestPackagesWDI($testPackages, $testPackagesSummary, $totalFields);
return [
'success' => true,
'processed_records' => $processedCount
];
}
/**
* Calculate repair log summaries
*/
protected function calculateRepairSummaries($repairLogs): array
{
$repairLogsSummary = [];
$repairLogsSummary2 = [];
$repairLogsSummaryCompleted = [];
$repairLogsSummaryCompleted2 = [];
$repairLogsSummaryRemaining = [];
$repairLogsSummaryRemaining2 = [];
foreach($repairLogs AS $repairLog) {
if(!isset($repairLogsSummary[$repairLog->iso_number][$repairLog->test_package_no]))
$repairLogsSummary[$repairLog->iso_number][$repairLog->test_package_no] = 0;
if(!isset($repairLogsSummary2[$repairLog->test_package_no]))
$repairLogsSummary2[$repairLog->test_package_no] = 0;
if(!isset($repairLogsSummaryCompleted[$repairLog->iso_number][$repairLog->test_package_no]))
$repairLogsSummaryCompleted[$repairLog->iso_number][$repairLog->test_package_no] = 0;
if(!isset($repairLogsSummaryRemaining[$repairLog->iso_number][$repairLog->test_package_no]))
$repairLogsSummaryRemaining[$repairLog->iso_number][$repairLog->test_package_no] = 0;
if(!isset($repairLogsSummaryCompleted2[$repairLog->test_package_no]))
$repairLogsSummaryCompleted2[$repairLog->test_package_no] = 0;
if(!isset($repairLogsSummaryRemaining2[$repairLog->test_package_no]))
$repairLogsSummaryRemaining2[$repairLog->test_package_no] = 0;
if($repairLog->repair_status == "Not Done") {
$repairLogsSummary[$repairLog->iso_number][$repairLog->test_package_no]++;
$repairLogsSummary2[$repairLog->test_package_no]++;
}
if(!rejected_date($repairLog->repair_date)) {
$repairLogsSummaryCompleted[$repairLog->iso_number][$repairLog->test_package_no]++;
$repairLogsSummaryCompleted2[$repairLog->test_package_no]++;
} else {
$repairLogsSummaryRemaining[$repairLog->iso_number][$repairLog->test_package_no]++;
$repairLogsSummaryRemaining2[$repairLog->test_package_no]++;
}
}
return [
'repairLogsSummary' => $repairLogsSummary,
'repairLogsSummary2' => $repairLogsSummary2,
'repairLogsSummaryCompleted' => $repairLogsSummaryCompleted,
'repairLogsSummaryCompleted2' => $repairLogsSummaryCompleted2,
'repairLogsSummaryRemaining' => $repairLogsSummaryRemaining,
'repairLogsSummaryRemaining2' => $repairLogsSummaryRemaining2,
];
}
/**
* Update test pack base statuses
*/
protected function updateTestPackBaseStatuses($testPackagesIsoSummary, $repairSummaries)
{
TransactionHelper::retryTransaction(function () use ($testPackagesIsoSummary, $repairSummaries) {
foreach($testPackagesIsoSummary AS $isoNumber => $data) {
foreach($data AS $tpNo => $data2) {
if($data2['total_complated_wdi'] == $data2['total_wdi']) {
$status = "Completed";
}
if($data2['total_complated_wdi'] < $data2['total_wdi']) {
$status = "On Going";
}
if($data2['total_complated_wdi'] == 0) {
$status = "Waiting";
}
$updateData = [
'welding_status' => $status,
'repair_qty' => @$repairSummaries['repairLogsSummary'][$isoNumber][$tpNo],
'repair_completed' => @$repairSummaries['repairLogsSummaryCompleted'][$isoNumber][$tpNo],
'repair_remaining' => @$repairSummaries['repairLogsSummaryRemaining'][$isoNumber][$tpNo],
];
db("test_pack_base_statuses")
->where("drawing_no", $isoNumber)
->where("test_package_no", $tpNo)
->update($updateData);
$updateData = [
'welding_status' => $status,
'repair_qty' => @$repairSummaries['repairLogsSummary2'][$tpNo],
'repair_completed' => @$repairSummaries['repairLogsSummaryCompleted2'][$tpNo],
'repair_remaining' => @$repairSummaries['repairLogsSummaryRemaining2'][$tpNo],
];
db("test_packages")
->where("test_package_number", $tpNo)
->update($updateData);
}
}
}, 5);
}
/**
* Update test packages summary
*/
protected function updateTestPackagesSummary($testPackagesSummary, $repairLogsSummary2)
{
TransactionHelper::retryTransaction(function () use ($testPackagesSummary, $repairLogsSummary2) {
foreach($testPackagesSummary AS $tpNo => $data2) {
if($data2['total_complated_wdi'] == $data2['total_wdi']) {
$status = "Completed";
}
if($data2['total_complated_wdi'] < $data2['total_wdi']) {
$status = "On Going";
}
if($data2['total_complated_wdi'] == 0) {
$status = "Waiting";
}
db("test_packages")
->where("test_package_number", $tpNo)
->update([
'welding_status' => $status,
'repair_status_total' => @$repairLogsSummary2[$tpNo]
]);
}
}, 5);
}
/**
* Update test packages with WDI fields and status
*/
protected function updateTestPackagesWDI($testPackages, $testPackagesSummary, $totalFields): int
{
$k = 0;
TransactionHelper::retryTransaction(function () use ($testPackages, $testPackagesSummary, $totalFields, &$k) {
foreach($testPackages AS $testPackage) {
$updateData = [];
if(isset($testPackagesSummary[$testPackage->test_package_number])) {
foreach($totalFields AS $field) {
$testPackage->$field = $testPackagesSummary[$testPackage->test_package_number][$field];
$updateData[$field] = $testPackagesSummary[$testPackage->test_package_number][$field];
}
}
$updateData = array_merge($updateData, updateTestPackageStatus($testPackage));
db("test_packages")->where("id", $testPackage->id)
->update($updateData);
db("test_pack_base_statuses")->where("test_package_no", $testPackage->test_package_number)
->update([
'tp_status' => $updateData['status'] ?? 'Waiting',
'priority' => $testPackage->priority,
'priority_info' => $testPackage->priority_info,
'responsible_person' => $testPackage->responsible_test,
'target_test_date' => $testPackage->planned_test_date,
]);
$k++;
}
return $k;
}, 5);
return $k;
}
}
@@ -0,0 +1,66 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use Illuminate\Support\Facades\Log;
/**
* Test Pack Cleanup Trigger
*
* Deletes non-matching test pack statuses
* Cleans up orphaned test package records
*/
class TestPackCleanupTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Test Pack Cleanup';
}
public function getOrder(): int
{
return 13;
}
public function getDependentFields(): array
{
return [
'test_package_no',
'iso_number'
];
}
protected function process($data, $beforeData, array $context): array
{
try {
// Call the cleanup view to delete non-matching test pack statuses
$result = view('cron.weld_logs-delete-non-matching-test-pack-statuses')->render();
Log::info("Test Pack Cleanup completed successfully", [
'weld_log_id' => $data->id
]);
return [
'success' => true,
'result' => 'cleanup_completed'
];
} catch (\Throwable $th) {
Log::error("Test Pack Cleanup failed (non-critical)", [
'weld_log_id' => $data->id,
'error' => $th->getMessage(),
'file' => $th->getFile(),
'line' => $th->getLine()
]);
// This is a non-critical operation, don't throw exception
return [
'success' => false,
'error' => $th->getMessage(),
'note' => 'non_critical_operation'
];
}
}
}
@@ -0,0 +1,178 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use Illuminate\Support\Facades\Log;
/**
* Test Pack Sync Trigger
*
* Syncs weld log data to test packages when test_package_no changes or new record is inserted.
* This trigger calls the sync view with specific test_package_no filter.
*/
class TestPackSyncTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Test Pack Sync from WeldLog';
}
public function getOrder(): int
{
return 8; // Run before TestPackBaseStatusChangerTrigger (order 9)
}
public function getDependentFields(): array
{
return [
'test_package_no',
'iso_number',
'welding_date',
'type_of_joint',
'nps_1',
'design_area',
'piping_type',
'circuit_number',
'p_id',
'type_of_test',
'test_pressure',
'no_of_the_joint_as_per_as_built_survey', // golden joints
'quantity_of_iso',
// NDT test dates
'rt_test_date',
'ut_test_date',
'pwht_test_date',
'pmi_test_date',
'ferrite_test_date',
'pt_test_date',
'mt_test_date',
'vt_test_date',
// NDT Request Dates and Numbers
'rt_request_date', 'rt_request_no',
'ut_request_date', 'ut_request_no',
'mt_request_no', 'mt_request_date',
'pt_request_no', 'pt_request_date',
'vt_request_no', 'vt_request_date',
'pwht_request_no', 'pwht_request_date',
'ferrite_request_no', 'ferrite_request_date',
'pmi_request_no', 'pmi_request_date',
'ht_request_no', 'ht_request_date',
];
}
/**
* Static registry to track already synced test packages in the current process
* to avoid redundant heavy sync calls.
*/
protected static $isSynced = [];
protected static $lastJobId = null;
protected function process($data, $beforeData, array $context): array
{
// Get current job ID if running in queue
$currentJobId = 'manual';
if (\Illuminate\Support\Facades\Request::header('X-Laravel-Job-Id')) {
$currentJobId = \Illuminate\Support\Facades\Request::header('X-Laravel-Job-Id');
} elseif (function_exists('app') && app()->bound(\Illuminate\Contracts\Queue\Job::class)) {
try {
$currentJobId = resolve(\Illuminate\Contracts\Queue\Job::class)->getJobId();
} catch (\Throwable $e) {
// Fallback if resolution fails despite being bound
}
}
// Reset registry if this is a new job
if (self::$lastJobId !== $currentJobId) {
self::$isSynced = [];
self::$lastJobId = $currentJobId;
}
$changedFields = $context['changed_fields'] ?? [];
$isNewRecord = $context['is_new_record'] ?? false;
// Collect all test package numbers that need to be synced
$testPackageNosToSync = [];
// Current test package
$currentTestPackageNo = $data->test_package_no;
if (!empty($currentTestPackageNo)) {
// Check if already synced in this process
if (!isset(self::$isSynced[$currentTestPackageNo])) {
$testPackageNosToSync[] = $currentTestPackageNo;
}
}
// If test_package_no changed, also sync the old test package
if (in_array('test_package_no', $changedFields) && $beforeData && !empty($beforeData->test_package_no)) {
$oldTestPackageNo = $beforeData->test_package_no;
if ($oldTestPackageNo !== $currentTestPackageNo && !in_array($oldTestPackageNo, $testPackageNosToSync)) {
if (!isset(self::$isSynced[$oldTestPackageNo])) {
$testPackageNosToSync[] = $oldTestPackageNo;
}
Log::info("TestPackSync: test_package_no changed, will sync both old and new", [
'old_test_package_no' => $oldTestPackageNo,
'new_test_package_no' => $currentTestPackageNo,
'weld_log_id' => $data->id
]);
}
}
// If no test packages to sync, return early
if (empty($testPackageNosToSync)) {
Log::info("TestPackSync: No test packages to sync", [
'weld_log_id' => $data->id,
'test_package_no' => $currentTestPackageNo
]);
return [
'success' => true,
'synced' => 0,
'reason' => 'no_test_package'
];
}
$syncedCount = 0;
$errors = [];
// Sync each test package
foreach ($testPackageNosToSync as $testPackageNo) {
try {
// Mark as synced before calling the view to prevent re-entry/collision
self::$isSynced[$testPackageNo] = true;
// Call the sync view with specific test_package_no
$result = view('cron.weld_logs-sync-from-weldlog-to-test-pack', [
'test_package_no' => $testPackageNo
])->render();
Log::info("TestPackSync: Sync completed for test package", [
'test_package_no' => $testPackageNo,
'weld_log_id' => $data->id
]);
$syncedCount++;
} catch (\Throwable $th) {
Log::error("TestPackSync: Sync failed for test package (non-critical)", [
'test_package_no' => $testPackageNo,
'weld_log_id' => $data->id,
'error' => $th->getMessage(),
'file' => $th->getFile(),
'line' => $th->getLine()
]);
$errors[] = [
'test_package_no' => $testPackageNo,
'error' => $th->getMessage()
];
}
}
return [
'success' => empty($errors),
'synced' => $syncedCount,
'test_packages_synced' => $testPackageNosToSync,
'errors' => $errors
];
}
}
@@ -0,0 +1,613 @@
<?php
namespace App\Services\WeldLogTriggers\Triggers;
use App\Services\WeldLogTriggers\Base\BaseTrigger;
use App\Models\TestPackage;
use App\Models\PunchList;
use App\Models\TestPackBaseStatus;
use App\Helpers\TransactionHelper;
use Illuminate\Support\Facades\Log;
/**
* Test Package Operations Trigger
*
* Comprehensive test package management including:
* - Supports updates
* - Punch list tracking
* - WDI calculations
* - Golden joints tracking
* - NDT calculations
* - Test package status updates
*/
class TestPackageOperationsTrigger extends BaseTrigger
{
public function getName(): string
{
return 'Test Package Operations';
}
public function getOrder(): int
{
return 7;
}
public function getDependentFields(): array
{
return [
'test_package_no',
'iso_number',
'nps_1',
'nps_2',
'outside_diameter_1',
'outside_diameter_2',
'line_number',
'project',
'design_area',
'piping_type',
'circuit_number',
'p_id',
'type_of_test',
'test_pressure',
'quantity_of_iso',
'welding_date',
'rt_test_date',
'ut_test_date',
'mt_test_date',
'pt_test_date',
'vt_test_date',
'pwht_test_date',
'pmi_test_date',
'ferrite_test_date',
'no_of_the_joint_as_per_as_built_survey'
];
}
protected function process($data, $beforeData, array $context): array
{
$weldLogs = db("weld_logs")->orderBy("test_package_no", "DESC")
->where("iso_number", $data->iso_number)
->where("test_package_no", $data->test_package_no)
->get();
$repairLogs = db("repair_logs")
->where("iso_number", $data->iso_number)
->where("test_package_no", $data->test_package_no)
->get();
$supports = db("supports")
->where("line_number", $data->line_number)
->get();
// Update supports weld_or_assembled_date when welding_date is available
if (!empty($data->welding_date) && !empty($data->line_number)) {
db("supports")
->where("line_number", $data->line_number)
->where(function($query) use ($data) {
$query->where("support_code", $data->element_code_1)
->orWhere("support_code", $data->element_code_2);
})
->update([
"weld_or_assembled_date" => $data->welding_date
]);
}
$punchLists = PunchList::where("line_isometric_no", $data->line_number)
->where("test_package", $data->test_package_no)
->get();
$logTypes = array_keys(log_test_types());
$testTypeToMedium = [
'PNEUMATIC' => 'Air',
'HYDRAULIC' => 'Water',
'VISUAL' => '-',
'ACUISTIC' => '-',
];
// Delete empty test packages
TestPackage::orWhere("test_package_number", "")->orWhereNull("test_package_number")->delete();
TestPackBaseStatus::orWhere("test_package_no", "")->orWhereNull("test_package_no")->delete();
$weldLogTestPackCount = 0;
// Calculate support statistics
$supportStats = $this->calculateSupportStats($supports);
// Calculate punch list statistics
$punchListStats = $this->calculatePunchListStats($punchLists);
// Calculate weld log statistics
$weldLogStats = $this->calculateWeldLogStats($weldLogs, $logTypes);
// Get NDT calculations
// Temporarily set $_GET['type'] for the view's getEsit() helper
$originalGetType = $_GET['type'] ?? null;
$_GET['type'] = 'test_packs';
try {
$ndtCalculation = j(view('admin-ajax.ndt-calculation-no-cache', [
'test_package_no' => $data->test_package_no,
'iso_number2' => $data->iso_number,
'type' => 'test_packs',
])->render());
} finally {
// Restore original $_GET['type']
if ($originalGetType !== null) {
$_GET['type'] = $originalGetType;
} else {
unset($_GET['type']);
}
}
$tpStats = $ndtCalculation['tpStats'] ?? [];
$tpIsoStats = $ndtCalculation['tpISOStats'] ?? [];
// Process Test Package data in chunks
$this->processTestPackageData(
$weldLogs,
$punchListStats,
$weldLogStats,
$tpStats,
$tpIsoStats,
$supportStats,
$testTypeToMedium,
$weldLogTestPackCount
);
// Sync weld logs to test packages for this specific test package
try {
if(isset($data->test_package_no) && $data->test_package_no != "") {
$syncParams = [
'test_package_no' => $data->test_package_no
];
$syncResult = view('cron.weld_logs-sync-from-weldlog-to-test-pack', $syncParams)->render();
Log::debug("Weld logs sync completed for test package: " . $data->test_package_no);
}
} catch (\Throwable $th) {
Log::error("Weld logs sync failed for test package: " . $data->test_package_no . " - " . $th->getMessage());
}
return [
'success' => true,
'processed_records' => $weldLogTestPackCount
];
}
/**
* Calculate support statistics
*/
protected function calculateSupportStats($supports): array
{
$supportStats = [];
$supportStatsTP = [];
$isoToTP = [];
foreach($supports AS $support) {
$tpNo = @$isoToTP[$support->line_number];
if(!isset($supportStatsTP[$tpNo]['support_remaining'])) {
$supportStatsTP[$tpNo]['support_remaining'] = 0;
$supportStatsTP[$tpNo]['welded_support_quantity'] = 0;
$supportStatsTP[$tpNo]['support_progress'] = 0;
}
if(!isset($supportStats[$support->line_number]['support_remaining']))
$supportStats[$support->line_number]['support_remaining'] = 0;
if(!isset($supportStats[$support->line_number]['welded_support_quantity']))
$supportStats[$support->line_number]['welded_support_quantity'] = 0;
if(!isset($supportStats[$support->line_number]['support_progress']))
$supportStats[$support->line_number]['support_progress'] = 0;
if(!rejected_date($support->weld_or_assembled_date) && $support->erection_type == "Weld") {
$supportStats[$support->line_number]['welded_support_quantity']+= $support->quantity;
$supportStatsTP[$tpNo]['welded_support_quantity']+= $support->quantity;
} else {
$supportStats[$support->line_number]['support_remaining']+= $support->quantity;
$supportStatsTP[$tpNo]['support_remaining']+= $support->quantity;
}
if($supportStats[$support->line_number]['welded_support_quantity'] > 0) {
$supportStats[$support->line_number]['support_progress'] =
100 - ($supportStats[$support->line_number]['support_remaining'] * 100) /
($supportStats[$support->line_number]['welded_support_quantity'] + $supportStats[$support->line_number]['support_remaining']);
} else {
$supportStats[$support->line_number]['support_progress'] = 0;
}
if($supportStatsTP[$tpNo]['welded_support_quantity'] > 0) {
$supportStatsTP[$tpNo]['support_progress'] =
100 - ($supportStatsTP[$tpNo]['support_remaining'] * 100) /
($supportStatsTP[$tpNo]['welded_support_quantity'] + $supportStatsTP[$tpNo]['support_remaining']);
} else {
$supportStatsTP[$tpNo]['support_progress'] = 0;
}
}
return ['supportStats' => $supportStats, 'supportStatsTP' => $supportStatsTP];
}
/**
* Calculate punch list statistics
*/
protected function calculatePunchListStats($punchLists): array
{
$stats = [];
$punchListNumbers = [];
foreach($punchLists AS $punchList) {
$drawingNo = $punchList->line_isometric_no;
if(!isset($stats[$drawingNo][$punchList->test_package]['punch_a_quantity']))
$stats[$drawingNo][$punchList->test_package]['punch_a_quantity'] = 0;
if(!isset($stats[$drawingNo][$punchList->test_package]['punch_b_quantity']))
$stats[$drawingNo][$punchList->test_package]['punch_b_quantity'] = 0;
if(!isset($stats[$drawingNo][$punchList->test_package]['punch_c_quantity']))
$stats[$drawingNo][$punchList->test_package]['punch_c_quantity'] = 0;
if(!isset($punchListNumbers[$punchList->test_package]))
$punchListNumbers[$punchList->test_package] = [];
if(!in_array($punchList->punch_list_no, $punchListNumbers[$punchList->test_package]))
$punchListNumbers[$punchList->test_package][] = $punchList->punch_list_no;
if(!isset($stats[$punchList->test_package]['punch_a_open_quantity']))
$stats[$punchList->test_package]['punch_a_open_quantity'] = 0;
if(!isset($stats[$punchList->test_package]['punch_b_open_quantity']))
$stats[$punchList->test_package]['punch_b_open_quantity'] = 0;
if(!isset($stats[$punchList->test_package]['punch_c_open_quantity']))
$stats[$punchList->test_package]['punch_c_open_quantity'] = 0;
if($punchList->category == "A") {
if($punchList->status == "Open") {
$stats[$punchList->test_package]['punch_a_open_quantity']++;
}
$stats[$drawingNo][$punchList->test_package]['punch_a_quantity']++;
}
if($punchList->category == "B") {
if($punchList->status == "Open") {
$stats[$punchList->test_package]['punch_b_open_quantity']++;
}
$stats[$drawingNo][$punchList->test_package]['punch_b_quantity']++;
}
if($punchList->category == "C") {
if($punchList->status == "Open") {
$stats[$punchList->test_package]['punch_c_open_quantity']++;
}
$stats[$drawingNo][$punchList->test_package]['punch_c_quantity']++;
}
$stats[$punchList->test_package]['found_date'] = $punchList->found_date;
$stats[$punchList->test_package]['punch_list'] = $punchList->punch_list;
}
return ['stats' => $stats, 'punchListNumbers' => $punchListNumbers];
}
/**
* Calculate weld log statistics including golden joints, WDI, and backlogs
*/
protected function calculateWeldLogStats($weldLogs, $logTypes): array
{
$stats = [];
$quantity_of_iso = [];
$addedSheet = [];
foreach($weldLogs AS $weldLog) {
$jointNo = $weldLog->no_of_the_joint_as_per_as_built_survey;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['goldenJoints']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['goldenJoints'] = 0;
if(!isset($quantity_of_iso[$weldLog->test_package_no]['total_qty']))
$quantity_of_iso[$weldLog->test_package_no]['total_qty'] = 0;
if(!isset($addedSheet[$weldLog->test_package_no]))
$addedSheet[$weldLog->test_package_no] = [];
if(!in_array($weldLog->quantity_of_iso, $addedSheet[$weldLog->test_package_no])) {
$quantity_of_iso[$weldLog->test_package_no]['total_qty']++;
$addedSheet[$weldLog->test_package_no][] = $weldLog->quantity_of_iso;
}
if(!isset($quantity_of_iso[$weldLog->iso_number][$weldLog->test_package_no]['total_qty']))
$quantity_of_iso[$weldLog->iso_number][$weldLog->test_package_no]['total_qty'] = 0;
if(!isset($addedSheet[$weldLog->iso_number][$weldLog->test_package_no]))
$addedSheet[$weldLog->iso_number][$weldLog->test_package_no] = [];
if(!in_array($weldLog->quantity_of_iso, $addedSheet[$weldLog->iso_number][$weldLog->test_package_no])) {
$quantity_of_iso[$weldLog->iso_number][$weldLog->test_package_no]['total_qty']++;
$addedSheet[$weldLog->iso_number][$weldLog->test_package_no][] = $weldLog->quantity_of_iso;
}
if(!isset($stats[$weldLog->test_package_no]['goldenJoints']))
$stats[$weldLog->test_package_no]['goldenJoints'] = 0;
if(strpos(strtoupper($jointNo), "GJ") !== false) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['goldenJoints']++;
$stats[$weldLog->test_package_no]['goldenJoints']++;
}
if(!isset($quantity_of_iso[$weldLog->iso_number][$weldLog->test_package_no]['total_iso_quantity']))
$quantity_of_iso[$weldLog->iso_number][$weldLog->test_package_no]['total_iso_quantity'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['rt_ut_status']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['rt_ut_status'] = 0;
$backlogs = [
'rt_backlog', 'ut_backlog', 'mt_backlog', 'pmi_backlog',
'vt_backlog', 'pt_backlog', 'ferrit_backlog', 'pwht_backlog', 'total_backlog',
];
foreach($backlogs AS $backlog) {
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no][$backlog]))
$stats[$weldLog->iso_number][$weldLog->test_package_no][$backlog] = 0;
if(!isset($stats[$weldLog->test_package_no][$backlog]))
$stats[$weldLog->test_package_no][$backlog] = 0;
}
if(!isset($stats[$weldLog->test_package_no]['total_backlog']))
$stats[$weldLog->test_package_no]['total_backlog'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['rt_ut_done']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['rt_ut_done'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['mt_pt_backlog']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['mt_pt_backlog'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['mt_pt_done']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['mt_pt_done'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['remaining_wdi']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['remaining_wdi'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['total_wdi']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['total_wdi'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['total_complated_wdi']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['total_complated_wdi'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['total_joint_qty']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['total_joint_qty'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['total_welded_joint_qty']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['total_welded_joint_qty'] = 0;
if(!isset($stats[$weldLog->iso_number][$weldLog->test_package_no]['welded_support_quantity']))
$stats[$weldLog->iso_number][$weldLog->test_package_no]['welded_support_quantity'] = 0;
foreach($logTypes AS $logType) {
$weldLogArray = (Array) $weldLog;
$stats[$weldLog->test_package_no]['total_backlog'] += (float) $weldLogArray[$logType . "_scope"];
}
if(!isset($addedSheet[$weldLog->iso_number][$weldLog->test_package_no]['total_iso']))
$addedSheet[$weldLog->iso_number][$weldLog->test_package_no]['total_iso'] = [];
if(!in_array($weldLog->quantity_of_iso, $addedSheet[$weldLog->iso_number][$weldLog->test_package_no]['total_iso'])) {
$quantity_of_iso[$weldLog->iso_number][$weldLog->test_package_no]['total_iso_quantity']++;
$addedSheet[$weldLog->iso_number][$weldLog->test_package_no]['total_iso'][] = $weldLog->quantity_of_iso;
}
if(rejected_date($weldLog->welding_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['remaining_wdi'] += (float) $weldLog->nps_1;
}
// Backlog calculations for each test type
if(rejected_date($weldLog->rt_test_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['rt_backlog']++;
$stats[$weldLog->test_package_no]['rt_backlog']++;
}
if(rejected_date($weldLog->ut_test_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['ut_backlog']++;
$stats[$weldLog->test_package_no]['ut_backlog']++;
}
if(rejected_date($weldLog->pwht_test_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['pwht_backlog']++;
$stats[$weldLog->test_package_no]['pwht_backlog']++;
}
if(rejected_date($weldLog->pmi_test_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['pmi_backlog']++;
$stats[$weldLog->test_package_no]['pmi_backlog']++;
}
if(rejected_date($weldLog->ferrite_test_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['ferrit_backlog']++;
$stats[$weldLog->test_package_no]['ferrit_backlog']++;
}
if(rejected_date($weldLog->pt_test_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['pt_backlog']++;
$stats[$weldLog->test_package_no]['pt_backlog']++;
}
if(rejected_date($weldLog->mt_test_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['mt_backlog']++;
$stats[$weldLog->test_package_no]['mt_backlog']++;
}
if(rejected_date($weldLog->vt_test_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['vt_backlog']++;
$stats[$weldLog->test_package_no]['vt_backlog']++;
}
if(!rejected_date($weldLog->welding_date)) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['total_complated_wdi'] += (float) $weldLog->nps_1;
$stats[$weldLog->iso_number][$weldLog->test_package_no]['total_welded_joint_qty']++;
}
$stats[$weldLog->iso_number][$weldLog->test_package_no]['total_wdi'] += (float) $weldLog->nps_1;
// Welding progress calculation - ISO based stats
$totalWdi = $stats[$weldLog->iso_number][$weldLog->test_package_no]['total_wdi'];
$totalComplatedWdi = $stats[$weldLog->iso_number][$weldLog->test_package_no]['total_complated_wdi'];
if ($totalWdi > 0) {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['welding_progress'] =
round(($totalComplatedWdi * 100) / $totalWdi, 2);
} else {
$stats[$weldLog->iso_number][$weldLog->test_package_no]['welding_progress'] = 0;
}
$stats[$weldLog->iso_number][$weldLog->test_package_no]['total_joint_qty']++;
}
return ['stats' => $stats, 'quantity_of_iso' => $quantity_of_iso];
}
/**
* Process and update test package data
*/
protected function processTestPackageData(
$weldLogs,
$punchListStats,
$weldLogStats,
$tpStats,
$tpIsoStats,
$supportStats,
$testTypeToMedium,
&$weldLogTestPackCount
) {
$punchListNumbers = $punchListStats['punchListNumbers'];
$stats = array_merge_recursive($punchListStats['stats'], $weldLogStats['stats']);
$quantity_of_iso = $weldLogStats['quantity_of_iso'];
$supportStatsData = $supportStats['supportStats'];
$supportStatsTP = $supportStats['supportStatsTP'];
// Process Test Package in chunks with TransactionHelper
TransactionHelper::chunkTransaction(
$weldLogs,
function ($weldLogChunk) use ($punchListNumbers, $stats, $tpStats, $tpIsoStats, $supportStatsTP, $quantity_of_iso, $testTypeToMedium, $supportStatsData, &$weldLogTestPackCount) {
foreach($weldLogChunk as $weldLog) {
try {
$punchListData = implode(",", $punchListNumbers[$weldLog->test_package_no] ?? []);
} catch (\Throwable $th) {
$punchListData = "";
}
$thisStats = @$stats[$weldLog->test_package_no];
$thisTpStats = @$tpStats[$weldLog->test_package_no];
$thisTpISOStats = @$tpIsoStats[$weldLog->test_package_no][$weldLog->iso_number];
$thisSupportStats = @$supportStatsTP[$weldLog->test_package_no];
$thisTotalBacklog =
(@$thisTpStats['vt'] ?? 0) +
(@$thisTpStats['ut'] ?? 0) +
(@$thisTpStats['mt'] ?? 0) +
(@$thisTpStats['pmi'] ?? 0) +
(@$thisTpStats['pt'] ?? 0) +
(@$thisTpStats['rt'] ?? 0) +
(@$thisTpStats['pwht'] ?? 0) +
(@$thisTpStats['ferrite'] ?? 0);
$testPackageData = [
'unit' => $weldLog->design_area,
'test_package_number' => $weldLog->test_package_no,
'discipline' => $weldLog->piping_type,
'circuit_number' => $weldLog->circuit_number,
'p_id' => $weldLog->p_id,
'iso_quantity' => $quantity_of_iso[$weldLog->test_package_no]['total_qty'],
'test_type' => $weldLog->type_of_test,
'test_medium' => @$testTypeToMedium[strtoupper($weldLog->type_of_test)],
'test_pressure' => $weldLog->test_pressure,
'golden_joints' => @$stats[$weldLog->iso_number][$weldLog->test_package_no]['goldenJoints'],
'walkdown_date' => @$stats[$weldLog->iso_number][$weldLog->test_package_no]['found_date'],
'punch_list' => $punchListData,
'welded_support_quantity' => @$thisSupportStats['welded_support_quantity'],
'support_remaining' => @$thisSupportStats['support_remaining'],
'support_progress' => @$thisSupportStats['support_progress'],
'a_punch_point_open' => @$stats[$weldLog->iso_number][$weldLog->test_package_no]['punch_a_open_quantity'],
'b_punch_point_open' => @$stats[$weldLog->iso_number][$weldLog->test_package_no]['punch_b_open_quantity'],
'c_punch_point_open' => @$stats[$weldLog->iso_number][$weldLog->test_package_no]['punch_c_open_quantity'],
'vt_backlog' => @$thisTpStats['vt'],
'ut_backlog' => @$thisTpStats['ut'],
'mt_backlog' => @$thisTpStats['mt'],
'pmi_backlog' => @$thisTpStats['pmi'],
'pt_backlog' => @$thisTpStats['pt'],
'rt_backlog' => @$thisTpStats['rt'],
'pwht_backlog' => @$thisTpStats['pwht'],
'ferrit_backlog' => @$thisTpStats['ferrite'],
'ndt_status' => @$thisTotalBacklog == 0 ? "Completed" : "Not Completed",
'welding_progress' => @$stats[$weldLog->iso_number][$weldLog->test_package_no]['welding_progress'],
];
$thisStats = @$stats[$weldLog->iso_number][$weldLog->test_package_no];
$thisSupportStats = @$supportStatsData[$weldLog->iso_number];
try {
$pipeProgress = round($thisStats['total_complated_wdi'] * 100 / $thisStats['total_wdi'], 2);
} catch (\Throwable $th) {
$pipeProgress = 0;
}
$testPackBaseStatusData = [
'area' => $weldLog->project,
'drawing_no' => $weldLog->iso_number,
'test_package_no' => $weldLog->test_package_no,
'discipline' => $weldLog->piping_type,
'iso_qty' => $quantity_of_iso[$weldLog->iso_number][$weldLog->test_package_no]['total_qty'],
'test_type' => $weldLog->type_of_test,
'test_medium' => @$testTypeToMedium[strtoupper($weldLog->type_of_test)],
'test_pressure_mpa' => $weldLog->test_pressure,
'tp_status' => '', // Will be filled from test pack base stat
'total_iso_quantity' => $quantity_of_iso[$weldLog->iso_number][$weldLog->test_package_no]['total_iso_quantity'],
'remaining_wdi' => $thisStats['remaining_wdi'],
'total_wdi' => $thisStats['total_wdi'],
'total_complated_wdi' => $thisStats['total_complated_wdi'],
'pipe_progress' => $pipeProgress,
'total_joint_qty' => $thisStats['total_joint_qty'],
'total_welded_joint_qty' => $thisStats['total_welded_joint_qty'],
'welded_support_quantity' => @$thisSupportStats['welded_support_quantity'],
'support_remaining' => @$thisSupportStats['support_remaining'],
'support_progress' => @$thisSupportStats['support_progress'],
'punch_a_quantity' => @$thisStats['punch_a_quantity'],
'punch_b_quantity' => @$thisStats['punch_b_quantity'],
'punch_c_quantity' => @$thisStats['punch_c_quantity'],
'rt_ut_status' => $thisStats['rt_ut_status'],
'rt_backlog' => @$thisTpISOStats['rt'],
'ut_backlog' => @$thisTpISOStats['ut'],
'mt_backlog' => @$thisTpISOStats['mt'],
'pmi_backlog' => @$thisTpISOStats['pmi'],
'vt_backlog' => @$thisTpISOStats['vt'],
'pt_backlog' => @$thisTpISOStats['pt'],
'pwht_backlog' => @$thisTpISOStats['pwht'],
'ferrit_backlog' => @$thisTpISOStats['ferrite'],
];
$testPackWhereData = [
'test_package_number' => $weldLog->test_package_no
];
$testPackBaseStatusWhereData = [
'test_package_no' => $weldLog->test_package_no,
'drawing_no' => $weldLog->iso_number,
];
if($weldLog->test_package_no != "" && !is_null($weldLog->test_package_no)) {
$testPackResult = TestPackage::updateOrCreate($testPackWhereData, $testPackageData);
$testPackBaseResult = TestPackBaseStatus::updateOrCreate($testPackBaseStatusWhereData, $testPackBaseStatusData);
$weldLogTestPackCount++;
}
}
return $weldLogChunk->count();
},
50, // Chunk size
1000 // 1ms delay
);
}
}
@@ -0,0 +1,249 @@
<?php
namespace App\Services\WeldLogTriggers;
use Illuminate\Support\Facades\Log;
use App\Helpers\TransactionHelper;
/**
* WeldLog Trigger Manager
*
* Manages the execution of all weld log triggers
* Handles trigger execution order, timing, and error handling
*/
class WeldLogTriggerManager
{
/**
* @var WeldLogTriggerRegistry Trigger registry
*/
protected $registry;
/**
* @var float Overall start time
*/
protected $overallStartTime;
/**
* Constructor
*
* @param WeldLogTriggerRegistry $registry
*/
public function __construct(WeldLogTriggerRegistry $registry)
{
$this->registry = $registry;
}
/**
* Execute all applicable triggers
*
* @param object $weldLogData Current weld log data
* @param object|null $beforeData Previous weld log data (null for new records)
* @param array $changedFields List of changed field names
* @param bool $isNewRecord Whether this is a new record
* @return array Execution results
*/
public function executeTriggers(
$weldLogData,
$beforeData = null,
array $changedFields = [],
bool $isNewRecord = false,
?string $action = null
): array {
$this->overallStartTime = microtime(true);
Log::info("=== WELD LOG SAVE TRIGGER STARTED ===", [
'weld_log_id' => $weldLogData->id,
'timestamp' => now()->toDateTimeString(),
'is_new_record' => $isNewRecord,
'changed_fields_count' => count($changedFields),
'changed_fields' => $changedFields,
'action' => $action,
'memory_usage_start' => round(memory_get_usage(true) / 1024 / 1024, 2) . ' MB'
]);
// Set database timeouts and memory limits
TransactionHelper::setDatabaseTimeouts(300, 600, 600);
ini_set('memory_limit', '2G');
ini_set('max_execution_time', 600); // 10 minutes
$results = [];
$triggers = $this->registry->getTriggersInOrder();
Log::info("Total triggers registered: " . count($triggers), [
'trigger_names' => array_map(function($t) { return $t->getName(); }, $triggers)
]);
foreach ($triggers as $trigger) {
// Check if trigger should run
if (!$trigger->shouldRun($changedFields, $isNewRecord)) {
Log::info("Skipping trigger: {$trigger->getName()}", [
'reason' => 'shouldRun returned false',
'order' => $trigger->getOrder(),
'dependent_fields' => $trigger->getDependentFields()
]);
$results[$trigger->getName()] = [
'skipped' => true,
'reason' => 'shouldRun returned false'
];
continue;
}
// Execute sync or async
if ($trigger->isAsync()) {
$this->executeAsync($trigger, $weldLogData, $beforeData, $changedFields);
$results[$trigger->getName()] = [
'queued' => true,
'execution' => 'async'
];
} else {
try {
$result = $trigger->execute($weldLogData, $beforeData, [
'changed_fields' => $changedFields,
'is_new_record' => $isNewRecord,
'action' => $action
]);
$results[$trigger->getName()] = array_merge($result, [
'executed' => true,
'execution' => 'sync'
]);
} catch (\Throwable $th) {
Log::error("Trigger execution failed: {$trigger->getName()}", [
'error' => $th->getMessage(),
'file' => $th->getFile(),
'line' => $th->getLine(),
'trace' => $th->getTraceAsString()
]);
$results[$trigger->getName()] = [
'executed' => false,
'error' => $th->getMessage(),
'execution' => 'sync'
];
// Re-throw critical errors, log others
if ($this->isCriticalTrigger($trigger->getName())) {
throw $th;
}
}
}
}
$this->logOverallCompletion($weldLogData->id, $results);
return $results;
}
/**
* Execute trigger asynchronously (via queue)
*
* @param \App\Services\WeldLogTriggers\Contracts\WeldLogTriggerInterface $trigger
* @param object $weldLogData
* @param object|null $beforeData
* @param array $changedFields
*/
protected function executeAsync($trigger, $weldLogData, $beforeData, $changedFields)
{
// Future implementation: Dispatch job to queue
// \App\Jobs\WeldLogTriggerJob::dispatch(
// get_class($trigger),
// $weldLogData->id,
// $beforeData,
// $changedFields
// );
Log::info("Trigger queued (async execution): {$trigger->getName()}", [
'weld_log_id' => $weldLogData->id,
'note' => 'Async execution not yet implemented, falling back to sync'
]);
// Fallback to sync execution for now
try {
$trigger->execute($weldLogData, $beforeData, [
'changed_fields' => $changedFields,
'is_new_record' => false,
// $action is not available here in the current signature, but async isn't used yet anyway
]);
} catch (\Throwable $th) {
Log::error("Async trigger execution failed: {$trigger->getName()}", [
'error' => $th->getMessage()
]);
}
}
/**
* Check if a trigger is critical (should stop execution on failure)
*
* @param string $triggerName
* @return bool
*/
protected function isCriticalTrigger(string $triggerName): bool
{
// Define which triggers are critical
$criticalTriggers = [
'Spool Status Changer',
'Line Lists Update',
'NDE Matrix Update',
'Construction Paint Logs Sync',
'Paint Follow Ups Sync',
];
return in_array($triggerName, $criticalTriggers);
}
/**
* Log overall execution completion
*
* @param int $weldLogId
* @param array $results
*/
protected function logOverallCompletion($weldLogId, array $results)
{
$overallDuration = round((microtime(true) - $this->overallStartTime) * 1000, 2);
// Count execution statistics
$executedCount = 0;
$skippedCount = 0;
$queuedCount = 0;
$failedCount = 0;
foreach ($results as $triggerName => $result) {
if (isset($result['executed']) && $result['executed']) {
$executedCount++;
} elseif (isset($result['skipped']) && $result['skipped']) {
$skippedCount++;
} elseif (isset($result['queued']) && $result['queued']) {
$queuedCount++;
} elseif (isset($result['executed']) && !$result['executed']) {
$failedCount++;
}
}
Log::info("=== WELD LOG SAVE TRIGGER COMPLETED ===", [
'weld_log_id' => $weldLogId,
'timestamp' => now()->toDateTimeString(),
'total_execution_time_ms' => $overallDuration,
'total_execution_time_sec' => round($overallDuration / 1000, 3),
'peak_memory_usage_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
'final_memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
'statistics' => [
'total_triggers' => count($results),
'executed' => $executedCount,
'skipped' => $skippedCount,
'queued' => $queuedCount,
'failed' => $failedCount
]
]);
}
/**
* Get the registry instance
*
* @return WeldLogTriggerRegistry
*/
public function getRegistry(): WeldLogTriggerRegistry
{
return $this->registry;
}
}
@@ -0,0 +1,120 @@
<?php
namespace App\Services\WeldLogTriggers;
use App\Services\WeldLogTriggers\Triggers\SpoolStatusChangerTrigger;
use App\Services\WeldLogTriggers\Triggers\LineListsUpdateTrigger;
use App\Services\WeldLogTriggers\Triggers\NdeMatrixUpdateTrigger;
use App\Services\WeldLogTriggers\Triggers\RequestDateOperationsTrigger;
use App\Services\WeldLogTriggers\Triggers\RepairLogsUpdateTrigger;
use App\Services\WeldLogTriggers\Triggers\NdeProjectUpdateTrigger;
use App\Services\WeldLogTriggers\Triggers\TestPackageOperationsTrigger;
use App\Services\WeldLogTriggers\Triggers\ConstructionPaintLogsTrigger;
use App\Services\WeldLogTriggers\Triggers\TestPackSyncTrigger;
use App\Services\WeldLogTriggers\Triggers\TestPackBaseStatusChangerTrigger;
use App\Services\WeldLogTriggers\Triggers\PaintFollowUpsSyncTrigger;
use App\Services\WeldLogTriggers\Triggers\HandoversSyncTrigger;
use App\Services\WeldLogTriggers\Triggers\TestPackCleanupTrigger;
use App\Services\WeldLogTriggers\Triggers\NdtCalculationCacheTrigger;
use App\Services\WeldLogTriggers\Triggers\RegisterCreatorCacheTrigger;
/**
* WeldLog Trigger Registry
*
* Central registry for all weld log triggers
* Manages trigger registration and retrieval
*/
class WeldLogTriggerRegistry
{
/**
* @var array Registered triggers
*/
protected $triggers = [];
/**
* Constructor - automatically registers all triggers
*/
public function __construct()
{
$this->registerTriggers();
}
/**
* Register all trigger instances
*/
protected function registerTriggers()
{
// Order 1-12 based on trigger execution order
$this->register(new SpoolStatusChangerTrigger()); // Order 1
$this->register(new LineListsUpdateTrigger()); // Order 2
$this->register(new NdeMatrixUpdateTrigger()); // Order 3
$this->register(new RequestDateOperationsTrigger()); // Order 4
$this->register(new RepairLogsUpdateTrigger()); // Order 5
$this->register(new NdeProjectUpdateTrigger()); // Order 6
$this->register(new TestPackageOperationsTrigger()); // Order 7
$this->register(new TestPackSyncTrigger()); // Order 8 - Sync from WeldLog to Test Pack
$this->register(new ConstructionPaintLogsTrigger()); // Order 9
$this->register(new TestPackBaseStatusChangerTrigger()); // Order 10
$this->register(new PaintFollowUpsSyncTrigger()); // Order 11
$this->register(new HandoversSyncTrigger()); // Order 12
$this->register(new TestPackCleanupTrigger()); // Order 13
$this->register(new NdtCalculationCacheTrigger()); // Order 14
$this->register(new RegisterCreatorCacheTrigger()); // Order 15
}
/**
* Register a single trigger
*
* @param \App\Services\WeldLogTriggers\Contracts\WeldLogTriggerInterface $trigger
*/
public function register($trigger)
{
$this->triggers[$trigger->getName()] = $trigger;
}
/**
* Get all triggers sorted by execution order
*
* @return array
*/
public function getTriggersInOrder(): array
{
$triggers = $this->triggers;
usort($triggers, function($a, $b) {
return $a->getOrder() <=> $b->getOrder();
});
return $triggers;
}
/**
* Get a specific trigger by name
*
* @param string $name Trigger name
* @return \App\Services\WeldLogTriggers\Contracts\WeldLogTriggerInterface|null
*/
public function getTrigger(string $name)
{
return $this->triggers[$name] ?? null;
}
/**
* Get all registered trigger names
*
* @return array
*/
public function getTriggerNames(): array
{
return array_keys($this->triggers);
}
/**
* Get total count of registered triggers
*
* @return int
*/
public function count(): int
{
return count($this->triggers);
}
}