Files
kopalnia_2026/app/Http/Controllers/WellOperationController.php
2026-09-03 12:21:17 +02:00

545 lines
22 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Concerns\SortsLists;
use App\Services\PumpService;
use App\Services\SyndisService;
use App\Services\WellAssessmentService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
/**
* Eksploatacja studni (legacy grupa site401). Osobny moduł pod sekcją
* Eksploatacja - NIE myli się z modułem Studnie (wells.*, sekcja Stan).
*
* Etap 1: lista eksploatacyjna (site401.php) + szkielet karty studni
* (site401mark). Dashboard ocen pracy układu, energochłonność, wykresy
* Hp/η, parametry pracy (site401inf) i podstrony (lift/mhis/offhis/demand)
* dokładamy w kolejnych etapach - opierają się na danych SYNDIS
* (rep_his_data_everyday, rep_soap_syndis_quality_data) i próbach pomp.
*/
class WellOperationController extends Controller
{
use SortsLists;
/** Status "Wszystkie" - wartość spoza słownika (jak legacy: 100). */
private const STATUS_ALL = 100;
/** Sortowalne kolumny listy (aliasy z joinów). */
private const SORTABLE = [
'name' => 'w.WLL_NAME',
'bariera' => 'w.WLL_BARIERA',
'pump' => 'p.PUP_NAME',
'pump_no' => 'drp.DRS_SN_NUMBER',
'engine' => 'e.ENG_NAME',
'engine_no' => 'dre.DRS_SN_NUMBER',
'type' => 'w.WLL_SYS_TYPE',
];
public function __construct(
private readonly PumpService $pumps,
private readonly WellAssessmentService $assess,
) {}
public function index(Request $request)
{
$department = $request->session()->get('department');
$search = trim((string) $request->query('q', ''));
$status = (int) $request->query('status', 3); // domyślnie jak legacy
[$sort, $dir] = $this->sortParams($request);
$statusOptions = DB::connection('softspm')->table('sys_dictionaries')
->whereIn('DIC_ID', [3, 4])
->orderBy('DIC_ID')
->pluck('DIC_POL_LANG', 'DIC_ID');
$wells = $this->buildQuery($department, $search, $status)
->when($sort !== null, fn ($q) => $q->orderBy(self::SORTABLE[$sort], $dir))
->orderBy('w.WLL_NAME')
->paginate(20)
->withQueryString();
$this->attachOnOff($wells);
return view('exploitation.wells.index', [
'wells' => $wells,
'search' => $search,
'status' => $status,
'statusOptions' => $statusOptions,
'statusAll' => self::STATUS_ALL,
'department' => $department,
]);
}
/** Karta eksploatacyjna studni (migracja site401mark). */
public function show(int $drs)
{
$well = $this->wellDetails($drs);
abort_if(! $well, 404);
$onoff = $this->lastOnOff($drs);
$m = $this->lastMeasurement($drs); // ostatni pomiar SYNDIS lub null
$lpDr = $this->lpDr($drs); // [Lp, Dr] lub [null, null]
$test = $this->pumpTest($well); // punkty próby pompy (kolekcja)
// Typ studni (WLL_CAT_DIC_ID): 18=BASE, 19=OPT, 20=inne (bez oceny), inaczej FULL.
$cat = (int) ($well->cat_dic_id ?? 0);
$grey = $cat === 20; // "SPM - inne" -> brak ocen (jak legacy)
// Krzywe z próby (skala BAZY = m³/h) - do obliczeń ocen/sprawności.
$hCurve = $test->map(fn ($p) => (object) ['x' => $p->q, 'y' => $p->h])->values();
$etaCurve = $test->map(fn ($p) => (object) ['x' => $p->q, 'y' => $p->n])->values();
$hasTest = $test->count() > 1;
$qn = (float) ($well->pump_qn ?? 0); // m³/h (obliczenia)
$q = $m?->q !== null ? (float) $m->q : null; // m³/h (obliczenia)
$hps = ($hasTest && $q !== null) ? $this->pumps->valueAt($hCurve, $q) : null; // do oceny H=f(Q)
$eta = $grey ? null : $this->assess->efficiency($q, $m?->hd, $m?->p2, $m?->pe);
$assessments = $grey ? null : [
'q' => $this->assess->q($qn, $q),
'hfq' => $this->assess->hfq($hps, $m?->pp, $m?->hh),
'throttle' => $this->assess->throttling($m?->hd, $m?->p1, $m?->p2),
'loss' => $this->assess->lineLoss($m?->p1, $m?->pp, $lpDr[0], $lpDr[1]),
];
// WYŚWIETLANIE: poza katalogami jednostka przepływu = m³/min (= m³/h / 60).
$toMin = fn (?float $v) => $v === null ? null : $v / 60;
$hCurveDisp = $hCurve->map(fn ($p) => (object) ['x' => $p->x / 60, 'y' => $p->y])->values();
$etaCurveDisp = $etaCurve->map(fn ($p) => (object) ['x' => $p->x / 60, 'y' => $p->y])->values();
$opQ = $toMin($q);
// Punkt pracy = wartości ZMIERZONE (nie rzutowane na krzywą próby):
// - wysokość podnoszenia Hp = Pp - Hh (manometryczna, z pomiaru),
// - sprawność = η obliczona (resETA). Odchyłka od krzywej = diagnostyka.
$opH = ($m && $m->pp !== null && $m->hh !== null) ? ((float) $m->pp - (float) $m->hh) : null;
$opEta = $eta;
$ry = $this->assess->redYellowLimit($qn);
$yg = $this->assess->yellowGreenLimit($qn);
return view('exploitation.wells.show', [
'well' => $well,
'onoff' => $onoff,
'measurement' => $m,
'qnMin' => $toMin($qn), // Qₙ w m³/min
'grey' => $grey,
'spmImage' => $this->spmImage($cat),
'diag' => $m?->dgn,
'assessments' => $assessments,
'energy' => $eta === null ? null : [
'eta' => $eta,
'ry' => $ry,
'yg' => $yg,
'zone' => $this->assess->energyZone($eta, $ry, $yg),
],
'hPlot' => $hasTest ? $this->pumps->plot($hCurveDisp) : null,
'etaPlot' => $hasTest ? $this->pumps->plot($etaCurveDisp) : null,
'opQ' => $opQ, // punkt pracy Q w m³/min
'opH' => $opH,
'opEta' => $opEta,
]);
}
/** Parametry pracy układu (migracja site401inf) - pełny zestaw pomiarów + dane techniczne. */
public function params(int $drs)
{
$well = $this->wellDetails($drs);
abort_if(! $well, 404);
$m = $this->lastMeasurement($drs);
// Dodatkowe parametry z detali (port getLastValue).
$extra = (object) [
'pb' => $this->lastDetailValue(225, $drs),
'throttle' => $this->lastDetailValue(730, $drs),
'energy_kwh' => $this->lastDetailValue(265, $drs),
'unit_energy' => $this->lastDetailValue(721, $drs),
];
// Parametry techniczne ze słownika WELL_TECHNICAL_PARAMETERS.
$techParams = DB::connection('softspm')->table('sys_dictionaries')
->where('DIC_TYPE', 'WELL_TECHNICAL_PARAMETERS')
->orderBy('DIC_ID')
->get(['DIC_ID', 'DIC_POL_LANG'])
->map(fn ($d) => (object) [
'label' => $d->DIC_POL_LANG,
'value' => $this->lastDetailValue((int) $d->DIC_ID, $drs),
]);
[$lp, $dr] = $this->lpDr($drs);
// Ostatnie zwierciadło statyczne >0 (jak site401bpow - osobne zapytanie).
$hst = DB::connection('softspm')->table('rep_his_data_everyday')
->where('RHD_DRS_ID', $drs)->where('RHD_HST_VAL', '>', 0)
->orderByDesc('RHD_ID')->value('RHD_HST_VAL');
return view('exploitation.wells.params', [
'well' => $well,
'measurement' => $m,
'extra' => $extra,
'techParams' => $techParams,
'lp' => $lp,
'dr' => $dr,
'mParam' => $this->lastDetailValue(251, $drs),
'category' => DB::connection('softspm')->table('sys_dictionaries')->where('DIC_ID', $well->cat_dic_id)->value('DIC_POL_LANG'),
'availableDate' => DB::connection('softspm')->table('seg_dev_resources')->where('DRS_ID', $drs)->value('DRS_AVAILABLE_DATE'),
'onoff' => $this->lastOnOff($drs),
'power' => $this->powerBalance($m, $lp, $dr, $hst === null ? null : (float) $hst),
'historyParams' => DB::connection('softspm')->table('sys_dictionaries')
->whereIn('DIC_ID', [200, 205, 210, 212, 215, 220, 245, 401, 402, 260, 270, 275, 280, 255])
->orderBy('DIC_POL_LANG')->pluck('DIC_POL_LANG', 'DIC_ID'),
]);
}
/** Ręczny zapis pomiaru (migracja edycji „Modyfikuj" z site401inf). */
public function paramsStore(int $drs, Request $request)
{
$well = $this->wellDetails($drs);
abort_if(! $well, 404);
$request->validate(['date' => ['required', 'date']]);
$num = fn ($k) => $request->filled($k) ? (float) str_replace(',', '.', (string) $request->input($k)) : null;
$dt = $request->input('date').' '.($request->filled('time') ? $request->input('time').':00' : now()->format('H:i:s'));
$user = $request->user()->USR_USERNAME;
$qH = $num('q') !== null ? $num('q') * 60 : null; // wpis w m³/min -> baza m³/h
DB::connection('softspm')->transaction(function () use ($drs, $num, $qH, $dt, $user) {
DB::connection('softspm')->table('rep_his_data_everyday')->insert([
'RHD_DRS_ID' => $drs,
'RHD_DATE' => $dt,
'RHD_Q_VAL' => $qH,
'RHD_HD_VAL' => $num('hd'),
'RHD_HST_VAL' => $num('hst'),
'RHD_HST0_VAL' => $num('hst0'),
'RHD_P1_VAL' => $num('p1'),
'RHD_P2_VAL' => $num('p2'),
'RHD_VM3_VAL' => $num('vm3'),
'RHD_WTEMP_VAL' => $num('wtemp'),
'RHD_PP_VAL' => $num('pp'),
'RHD_HH_VAL' => $num('hh'),
'RHD_PE_VAL' => $num('pe'),
'RHD_I_VAL' => $num('i'),
'RHD_U_VAL' => $num('u'),
'RHD_COS_VAL' => $num('cos'),
'RHD_WORK_TIME_VAL' => $num('work_time'),
'RHD_AUDIT_CD' => now()->toDateTimeString(),
]);
$details = [
200 => $qH, 205 => $num('hd'), 210 => $num('hst'), 212 => $num('hst0'),
215 => $num('p1'), 220 => $num('p2'), 225 => $num('pb'), 230 => $num('vm3'),
245 => $num('wtemp'), 401 => $num('pp'), 402 => $num('hh'), 730 => $num('throttle'),
260 => $num('pe'), 265 => $num('energy_kwh'), 270 => $num('i'), 275 => $num('u'),
280 => $num('cos'), 290 => $num('work_time'), 721 => $num('unit_energy'),
];
foreach ($details as $detail => $value) {
if ($value === null) {
continue;
}
DB::connection('softspm')->table('seg_dev_resources_det')->insert([
'DRD_DRS_ID' => $drs,
'DRD_RESOURCES_DETAIL' => $detail,
'DRD_VALUE' => (string) $value,
'DRD_AUDIT_USR' => $user,
'DRD_AUDIT_DATE' => $dt,
]);
}
});
return redirect()->route('well-operation.params', $drs)->with('status', 'Zapisano ręczny pomiar.');
}
/**
* Bilans mocy układu (port nowszego site401bpow.php). Dane z ostatniego
* pomiaru (rep_his_data_everyday) + Lp/Dr. Skala Q = m³/h (stała 0,002752
* ≈ moc hydrauliczna [kW] = Q·H/363). Zwraca składowe mocy lub null.
*/
private function powerBalance(?object $m, ?float $lp, ?float $dr, ?float $hst): ?array
{
if (! $m || $m->q === null || $m->pe === null) {
return null;
}
$q = (float) $m->q;
$pe = (float) $m->pe;
$pp = (float) ($m->pp ?? 0);
$hh = (float) ($m->hh ?? 0);
$p1 = (float) ($m->p1 ?? 0);
$p2 = (float) ($m->p2 ?? 0);
$hstV = (float) ($hst ?? 0);
$lpV = (float) ($lp ?? 0);
$drV = (float) ($dr ?? 0);
$k = 0.002752;
$vel = $drV > 0 ? $q / (2826 * $drV * $drV) : 0.0; // prędkość w rurociągu
$hct = ($vel * $vel) / 19.62; // wysokość prędkości
$pod = $k * $q * (($pp - $hh) + $hct);
$paz = $pe - $pod;
$psl = $k * $q * ($pp - ($lpV + $p1));
$psa = $k * $q * ($p1 - $p2);
$pp2 = $k * $q * ($p2 + $hct);
$pst = $k * $q * $hstV;
$pgh = $pod - ($pst + $pp2 + $psl + $psa);
return [
'pe' => $pe,
'pod' => $pod,
'paz' => $paz,
'psl' => $psl,
'psa' => $psa,
'pp2' => $pp2,
'pst' => $pst,
'pgh' => $pgh,
'sum' => $paz + $psl + $psa + $pp2 + $pst + $pgh,
];
}
/** Pobranie aktualnych danych z SYNDIS (migracja site401mark&data=refresh + ajax_getdata4well). */
public function fetchData(int $drs, Request $request, SyndisService $syndis)
{
$well = $this->wellDetails($drs);
abort_if(! $well, 404);
try {
$saved = $syndis->refreshWell($drs, $well->well_name, $request->user()->USR_USERNAME);
return redirect()->route('well-operation.show', $drs)->with(
$saved ? 'status' : 'error',
$saved
? 'Pobrano aktualne dane z SYNDIS dla studni „'.$well->well_name.'".'
: 'Studnia wyłączona lub brak aktualnych danych w SYNDIS.'
);
} catch (\Throwable $e) {
return redirect()->route('well-operation.show', $drs)
->with('error', 'Nie udało się pobrać danych z SYNDIS: '.$e->getMessage());
}
}
/**
* Lista eksploatacyjna - jedno zapytanie z joinami (pompa/silnik), stan
* ON/OFF dociągany osobno dla bieżącej strony. Wierne site401.php.
*/
private function buildQuery(?string $department, string $search, int $status)
{
return DB::connection('softspm')
->table('seg_wells as w')
->leftJoin('seg_dev_resources as drw', function ($j) {
$j->on('drw.DRS_DEV_ID', '=', 'w.WLL_ID')->where('drw.DRS_DEV_TYPE', '=', 12);
})
->leftJoin('seg_well_pump_eng_rel as rel', 'rel.RWL_WLL_ID', '=', 'w.WLL_ID')
->leftJoin('seg_dev_resources as drp', function ($j) {
$j->on('drp.DRS_ID', '=', 'rel.RWL_PUP_DRS_ID')->where('drp.DRS_DEV_TYPE', '=', 10);
})
->leftJoin('seg_pumps as p', 'p.PUP_ID', '=', 'drp.DRS_DEV_ID')
->leftJoin('seg_dev_resources as dre', function ($j) {
$j->on('dre.DRS_ID', '=', 'rel.RWL_ENG_DRS_ID')->where('dre.DRS_DEV_TYPE', '=', 11);
})
->leftJoin('seg_engines as e', 'e.ENG_ID', '=', 'dre.DRS_DEV_ID')
->where('drw.DRS_DEPARTMENT', $department)
->when($search !== '', fn ($q) => $q->where('w.WLL_NAME', 'like', '%'.$search.'%'))
->when($status !== self::STATUS_ALL, fn ($q) => $q->where('drw.DRS_STATUS_DIC_ID', $status))
->select([
'drw.DRS_ID as drs_id',
'w.WLL_NAME as well_name',
'w.WLL_BARIERA as bariera',
'p.PUP_NAME as pump_name',
'drp.DRS_SN_NUMBER as pump_no',
'e.ENG_NAME as engine_name',
'dre.DRS_SN_NUMBER as engine_no',
'w.WLL_SYS_TYPE as sys_type',
]);
}
/** Stan ON/OFF (DRD_RESOURCES_DETAIL=507) tylko dla studni z bieżącej strony. */
private function attachOnOff(LengthAwarePaginator $wells): void
{
$ids = collect($wells->items())->pluck('drs_id')->filter()->values()->all();
$values = empty($ids) ? collect() : DB::connection('softspm')
->table('seg_dev_resources_det as d')
->joinSub(
DB::connection('softspm')->table('seg_dev_resources_det')
->select('DRD_DRS_ID', DB::raw('MAX(DRD_ID) as max_id'))
->where('DRD_RESOURCES_DETAIL', 507)
->whereIn('DRD_DRS_ID', $ids)
->groupBy('DRD_DRS_ID'),
'mx', 'mx.max_id', '=', 'd.DRD_ID'
)
->pluck('d.DRD_VALUE', 'd.DRD_DRS_ID');
foreach ($wells->items() as $row) {
$row->onoff = $values[$row->drs_id] ?? null;
}
}
/** Podstawowe dane studni + układu pompowego (część site401mark). */
private function wellDetails(int $drs): ?object
{
return DB::connection('softspm')
->table('seg_dev_resources as drw')
->join('seg_wells as w', 'w.WLL_ID', '=', 'drw.DRS_DEV_ID')
->leftJoin('seg_well_pump_eng_rel as rel', 'rel.RWL_WLL_ID', '=', 'w.WLL_ID')
->leftJoin('seg_dev_resources as drp', function ($j) {
$j->on('drp.DRS_ID', '=', 'rel.RWL_PUP_DRS_ID')->where('drp.DRS_DEV_TYPE', '=', 10);
})
->leftJoin('seg_pumps as p', 'p.PUP_ID', '=', 'drp.DRS_DEV_ID')
->leftJoin('seg_dev_resources as dre', function ($j) {
$j->on('dre.DRS_ID', '=', 'rel.RWL_ENG_DRS_ID')->where('dre.DRS_DEV_TYPE', '=', 11);
})
->leftJoin('seg_engines as e', 'e.ENG_ID', '=', 'dre.DRS_DEV_ID')
->where('drw.DRS_ID', $drs)
->where('drw.DRS_DEV_TYPE', 12)
->select([
'drw.DRS_ID as drs_id',
'w.WLL_ID as well_id',
'w.WLL_NAME as well_name',
'w.WLL_BARIERA as bariera',
'w.WLL_SYS_TYPE as sys_type',
'w.WLL_CAT_DIC_ID as cat_dic_id',
'p.PUP_ID as pump_id',
'p.PUP_NAME as pump_name',
'drp.DRS_SN_NUMBER as pump_no',
'p.PUP_Q_N as pump_qn',
'e.ENG_NAME as engine_name',
'dre.DRS_SN_NUMBER as engine_no',
'e.ENG_VOLTAGE as engine_voltage',
])
->first();
}
/** Ostatni pomiar SYNDIS (rep_his_data_everyday) dla studni lub null. */
private function lastMeasurement(int $drs): ?object
{
$r = DB::connection('softspm')->table('rep_his_data_everyday')
->where('RHD_DRS_ID', $drs)
->orderByDesc('RHD_ID')
->first();
if (! $r) {
return null;
}
return (object) [
'q' => $r->RHD_Q_VAL,
'hd' => $r->RHD_HD_VAL,
'hst' => $r->RHD_HST_VAL,
'hst0' => $r->RHD_HST0_VAL,
'p1' => $r->RHD_P1_VAL,
'p2' => $r->RHD_P2_VAL,
'pp' => $r->RHD_PP_VAL,
'hh' => $r->RHD_HH_VAL,
'pe' => $r->RHD_PE_VAL,
'i' => $r->RHD_I_VAL,
'u' => $r->RHD_U_VAL,
'cos' => $r->RHD_COS_VAL,
'vm3' => $r->RHD_VM3_VAL,
'wtemp' => $r->RHD_WTEMP_VAL,
'work_time' => $r->RHD_WORK_TIME_VAL,
'dgn' => $r->RHD_DGN_VAL,
'date' => $r->RHD_DATE,
];
}
/**
* Lp (głębokość zabudowy pompy, detal 240) i Dr (średnica rurociągu,
* detal 235/1000). Zastępuje funkcję składowaną MySQL getLastValue
* własnym zapytaniem (optymalizacja - bez zależności od procedur w bazie).
*/
private function lpDr(int $drs): array
{
$lp = $this->lastDetailValue(240, $drs);
$dr = $this->lastDetailValue(235, $drs);
return [
$lp !== null ? (float) $lp : null,
$dr !== null ? (float) $dr / 1000 : null,
];
}
/**
* Ostatnia wartość detalu zasobu (port funkcji składowanej getLastValue):
* DRD_VALUE z seg_dev_resources_det dla danego detalu i zasobu, po
* najpóźniejszej dacie audytu (rozstrzyganie po DRD_ID).
*/
private function lastDetailValue(int $detail, int $drs): ?string
{
return DB::connection('softspm')->table('seg_dev_resources_det')
->where('DRD_RESOURCES_DETAIL', $detail)
->where('DRD_DRS_ID', $drs)
->orderByDesc('DRD_AUDIT_DATE')
->orderByDesc('DRD_ID')
->value('DRD_VALUE');
}
/**
* Punkty próby pompy ze stacji prób (tst_tests + tst_test_details):
* q=TSD_VAL_04, h=TSD_VAL_08, p=TSD_VAL_14, n=TSD_VAL_18 (skala m³/h, bez /60).
* Dobór próby: po PUP_ID + nr fabrycznych pompy/silnika (fallback bez silnika).
*/
private function pumpTest(object $well): Collection
{
if (empty($well->pump_id)) {
return collect();
}
$base = DB::connection('softspm')->table('tst_tests')
->where('TST_PUP_ID', $well->pump_id)
->where('TST_PUP_SN_NO', $well->pump_no);
$testId = (clone $base)->where('TST_ENG_SN_NO', $well->engine_no)->max('TST_ID')
?? $base->max('TST_ID');
if (! $testId) {
return collect();
}
return DB::connection('softspm')->table('tst_test_details')
->where('TSD_TST_ID', $testId)
->orderBy('TSD_MSR_STEP')
->get()
->map(fn ($d) => (object) [
'q' => (float) $d->TSD_VAL_04,
'h' => (float) $d->TSD_VAL_08,
'p' => (float) $d->TSD_VAL_14,
'n' => (float) $d->TSD_VAL_18,
])
->filter(fn ($p) => $p->q > 0)
->values();
}
/** Plik schematu SPM wg kategorii studni (18=BASE, 19=OPT, inaczej FULL). */
private function spmImage(int $cat): string
{
return match ($cat) {
18 => 'images/spm_base.png',
19 => 'images/spm_opt.png',
default => 'images/spm_full.png',
};
}
/** Ostatni wpis ON/OFF dla studni (wartość, data, powód wyłączenia). */
private function lastOnOff(int $drs): ?object
{
$row = DB::connection('softspm')->table('seg_dev_resources_det')
->where('DRD_RESOURCES_DETAIL', 507)
->where('DRD_DRS_ID', $drs)
->orderByDesc('DRD_ID')
->first();
if ($row && ! empty($row->DRD_DESCRIPTION)) {
$row->reason = DB::connection('softspm')->table('sys_dictionaries')
->where('DIC_ID', $row->DRD_DESCRIPTION)
->value('DIC_POL_LANG');
}
return $row;
}
}