Initial commit

This commit is contained in:
2026-09-03 12:21:17 +02:00
commit 6d40da0410
155 changed files with 21554 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Services;
use App\Models\EquipmentFile;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Carbon;
/**
* Zapis załączników wyposażenia jako BLOB w seg_equipments_files (legacy site206).
* Na jedno wyposażenie przypada max jeden plik danej roli (pierwszy człon MIME):
* 'application' = dokumentacja katalogowa, 'image' = zdjęcie. Wgranie nowego
* pliku tej samej roli zastępuje poprzedni - jak legacy.
*/
class EquipmentFileService
{
/** Zapisuje (zastępując plik tej samej roli) załącznik wyposażenia. */
public function store(int $equId, UploadedFile $file, string $user): void
{
$mime = $file->getMimeType() ?: ($file->getClientMimeType() ?: 'application/octet-stream');
$role = strtolower(strtok($mime, '/')); // 'application' | 'image' | ...
// Usuń dotychczasowy plik tej roli (legacy trzymał 1 kartę + 1 zdjęcie).
EquipmentFile::on('softspm')
->where('EQF_EQU_ID', $equId)
->whereRaw("lower(substring_index(EQF_FILE_TYPE, '/', 1)) = ?", [$role])
->delete();
EquipmentFile::create([
'EQF_EQU_ID' => $equId,
'EQF_FILE' => $file->getContent(),
'EQF_FILE_TYPE' => $mime,
'EQF_FILE_SIZE' => (string) $file->getSize(),
'EQF_FILE_NAME' => $this->sanitize($file->getClientOriginalName()),
'EQF_STATUS' => '1',
'EQF_AUDIT_CU' => $user,
'EQF_AUDIT_CD' => Carbon::now()->toDateTimeString(),
]);
}
/**
* Czyszczenie nazwy pliku jak legacy delPolishAndBlankCharactersFromFileName:
* polskie znaki -> ASCII, spacje/myślniki -> '_', usunięcie znaków
* specjalnych, małe litery. Zachowuje kropkę (rozszerzenie). Limit 128 zn.
*/
public function sanitize(string $name): string
{
$map = [
'ą'=>'a','ż'=>'z','ź'=>'z','ć'=>'c','ś'=>'s','ę'=>'e','ó'=>'o','ł'=>'l','ń'=>'n',
'Ą'=>'a','Ż'=>'z','Ź'=>'z','Ć'=>'c','Ś'=>'s','Ę'=>'e','Ó'=>'o','Ł'=>'l','Ń'=>'n',
];
$val = strtr($name, $map);
$val = str_replace([' ', '-'], '_', $val);
$val = preg_replace('/[^A-Za-z0-9._]/', '', $val);
return substr(strtolower($val), 0, 128);
}
}
+276
View File
@@ -0,0 +1,276 @@
<?php
namespace App\Services;
use App\Models\Pump;
use App\Models\PumpCurvePoint;
use App\Support\PolynomialRegression;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
/**
* Logika domenowa pomp (legacy site203 / site203inf) - funkcjonalność 1:1.
*
* SKALA: baza UAT trzyma Q/krzywe BEZ ×60 (PUP_Q_N, PEP_X_PARAM w m³/h) -
* nie dzielimy/mnożymy przez 60.
*
* Krzywe Q-H i Q-η: 8 punktów przy stałych ułamkach Q_n (indeks 4 = nominał).
*/
class PumpService
{
private const Q_FACTORS = [0.0, 0.25, 0.55, 0.7, 1.0, 1.22, 1.35, 1.42];
private const NOMINAL_INDEX = 4;
/** Kolory segmentów krzywej (wg algorytmu legacy). */
private const BAND_COLORS = ['red' => '#c62828', 'amber' => '#f9a825', 'green' => '#2e7d32'];
private function db()
{
return DB::connection('softspm');
}
// ===================== LOOKUPY / ZAPIS =====================
/**
* Dodanie pompy. Użytkownik podaje H i η dla każdego z 8 punktów Q
* (Q generowane z Q_n). Wszystko transakcyjnie.
*
* @param array $data name, manufacturer_id, q_n, length, diameter, weight,
* h: float[8], eta: float[8] (w kolejności rosnącego Q)
*/
public function create(array $data): Pump
{
return $this->db()->transaction(function () use ($data) {
$qn = (float) $data['q_n'];
$h = array_values($data['h']);
$eta = array_values($data['eta']);
$pump = Pump::create([
'PUP_MCF_ID' => $data['manufacturer_id'],
'PUP_NAME' => $data['name'],
'PUP_Q_N' => $qn,
'PUP_Q_MIN' => 0.55 * $qn,
'PUP_Q_MAX' => 1.35 * $qn,
'PUP_H_P' => $h[self::NOMINAL_INDEX] ?? null,
'PUP_H_MAX' => $h[2] ?? null,
'PUP_H_MIN' => $h[6] ?? null,
'PUP_ETA_MAX' => $eta[self::NOMINAL_INDEX] ?? null,
'PUP_PUMP_LENGTH' => $data['length'] ?? null,
'PUP_WEIGHT' => $data['weight'] ?? null,
'PUP_DIAMETER' => $data['diameter'] ?? null,
'PUP_STATUS' => '1',
]);
$rows = [];
foreach (self::Q_FACTORS as $i => $f) {
$x = round($f * $qn, 4);
$rows[] = ['PEP_PUP_ID' => $pump->PUP_ID, 'PEP_X_PARAM' => $x, 'PEP_Y_PARAM' => (float) ($h[$i] ?? 0), 'PEP_TYPE' => 'H'];
$rows[] = ['PEP_PUP_ID' => $pump->PUP_ID, 'PEP_X_PARAM' => $x, 'PEP_Y_PARAM' => (float) ($eta[$i] ?? 0), 'PEP_TYPE' => 'ETA'];
}
PumpCurvePoint::insert($rows);
return $pump;
});
}
/**
* Aktualizacja pompy: dane + wartości Y krzywych (po PEP_ID) + pochodne
* (3. punkt H=H_MAX, 5.=H_P, 7.=H_MIN; 5. ETA=ETA_MAX) - jak legacy.
*/
public function update(Pump $pump, array $data): void
{
$this->db()->transaction(function () use ($pump, $data) {
$pump->update([
'PUP_MCF_ID' => $data['manufacturer_id'],
'PUP_NAME' => $data['name'],
'PUP_PUMP_LENGTH' => $data['length'] ?? null,
'PUP_WEIGHT' => $data['weight'] ?? null,
'PUP_DIAMETER' => $data['diameter'] ?? null,
]);
$hPoints = $pump->curvePoints()->where('PEP_TYPE', 'H')->orderBy('PEP_X_PARAM')->get();
foreach ($hPoints->values() as $idx => $point) {
$y = $this->num($data['h'][$point->PEP_ID] ?? $point->PEP_Y_PARAM);
$point->update(['PEP_Y_PARAM' => $y]);
match ($idx + 1) {
3 => $pump->update(['PUP_H_MAX' => $y]),
5 => $pump->update(['PUP_H_P' => $y]),
7 => $pump->update(['PUP_H_MIN' => $y]),
default => null,
};
}
$etaPoints = $pump->curvePoints()->where('PEP_TYPE', 'ETA')->orderBy('PEP_X_PARAM')->get();
foreach ($etaPoints->values() as $idx => $point) {
$y = $this->num($data['eta'][$point->PEP_ID] ?? $point->PEP_Y_PARAM);
$point->update(['PEP_Y_PARAM' => $y]);
if ($idx + 1 === 5) {
$pump->update(['PUP_ETA_MAX' => $y]);
}
}
});
}
/** Usunięcie logiczne (PUP_STATUS='0'). */
public function delete(Pump $pump): void
{
$pump->update(['PUP_STATUS' => '0']);
}
// ===================== ODCZYT KRZYWYCH =====================
/** Punkty krzywej danego typu: kolekcja {id,x,y} (x = PEP_X_PARAM, m³/h). */
public function curve(Pump $pump, string $type): Collection
{
return $pump->curvePoints()
->where('PEP_TYPE', $type)
->orderBy('PEP_X_PARAM')
->get()
->map(fn ($p) => (object) [
'id' => $p->PEP_ID,
'x' => round((float) $p->PEP_X_PARAM, 3),
'y' => (float) $p->PEP_Y_PARAM,
])
->values();
}
/**
* Czy krzywą można narysować - jak legacy:
* H → 0 wartości Y=0,
* ETA → <2 wartości Y=0 (punkt przy Q=0 legalnie =0).
*/
public function curveDrawable(Collection $points, int $allowedZeros): bool
{
if ($points->isEmpty()) {
return false;
}
$zeros = $points->filter(fn ($p) => (float) $p->y == 0.0)->count();
return $zeros <= $allowedZeros;
}
/** Wartość z aproksymacji wielomianowej (4 współcz.) dla zadanego Q. */
public function valueAt(Collection $points, float $q): ?float
{
if ($points->isEmpty()) {
return null;
}
$reg = new PolynomialRegression(4);
foreach ($points as $p) {
$reg->addData((float) $p->x, (float) $p->y);
}
return $reg->interpolate($reg->getCoefficients(), $q);
}
/**
* Model wykresu: aproksymacja wielomianowa próbkowana i pocięta na
* segmenty kolorowane wg stref Q (scope[0..6]) - wiernie wg legacy:
* [0→scope2] red, [scope2→scope3] amber, [scope3→scope5] green,
* [scope5→scope6] amber, [scope6→max] red. Plus surowe punkty (niebieskie).
*
* @return array{segments:array<int,array{color:string,points:array<int,array{x:float,y:float}>}>,raw:Collection,xMax:float,yMax:float}
*/
public function plot(Collection $points): array
{
$scope = $points->pluck('x')->map(fn ($v) => (float) $v)->values()->all();
$xMax = max($points->max('x'), 0.0001);
$reg = new PolynomialRegression(4);
foreach ($points as $p) {
$reg->addData((float) $p->x, (float) $p->y);
}
$coeffs = $reg->getCoefficients();
$band = function (float $x) use ($scope): string {
if (count($scope) < 7) {
return 'green';
}
if ($x < $scope[2]) return 'red';
if ($x < $scope[3]) return 'amber';
if ($x < $scope[5]) return 'green';
if ($x < $scope[6]) return 'amber';
return 'red';
};
$n = 120;
$segments = [];
$current = null;
$yMax = (float) $points->max('y');
$prev = null;
for ($i = 0; $i <= $n; $i++) {
$x = $xMax * $i / $n;
$y = max(0.0, $reg->interpolate($coeffs, $x));
$yMax = max($yMax, $y);
$color = self::BAND_COLORS[$band($x)];
$pt = ['x' => $x, 'y' => $y];
if ($current === null || $current['color'] !== $color) {
if ($current !== null) {
$current['points'][] = $pt; // domknij łączeniem
$segments[] = $current;
}
$current = ['color' => $color, 'points' => $prev !== null ? [$prev, $pt] : [$pt]];
} else {
$current['points'][] = $pt;
}
$prev = $pt;
}
if ($current !== null) {
$segments[] = $current;
}
return [
'segments' => $segments,
'raw' => $points,
'xMax' => $xMax,
'yMax' => max($yMax, 0.0001) * 1.08,
'coeffs' => array_values($coeffs), // [c0,c1,c2,c3] do interaktywnego markera
];
}
private function num(mixed $v): float
{
return (float) str_replace(',', '.', (string) $v);
}
// ===================== DOBÓR AGREGATU (site201) =====================
/**
* Dobór agregatu pompowego (legacy site201): pompy, których zakres pracy
* obejmuje zadane Q (PUP_Q_MIN < Q < PUP_Q_MAX; skala UAT bez ×60). Dla
* każdej liczy H(Q) i η(Q) z aproksymacji wielomianowej (st.4). Gdy podano
* H, filtruje do pomp, dla których zadane H mieści się w tolerancji legacy
* [H(Q)-5, H(Q)+8]. Wynik sortowany malejąco po sprawności η(Q).
*
* @return Collection<int,object> każdy: {pump, h_val, eta_val, plot|null}
*/
public function matchingAggregates(float $q, ?float $h): Collection
{
return Pump::query()->visible()->with('manufacturer')
->where('PUP_Q_MIN', '<', $q)
->where('PUP_Q_MAX', '>', $q)
->orderBy('PUP_NAME')
->get()
->map(function (Pump $pump) use ($q) {
$hCurve = $this->curve($pump, 'H');
$hVal = $this->valueAt($hCurve, $q);
if ($hVal === null) {
return null; // brak krzywej H - pompy nie da się ocenić
}
return (object) [
'pump' => $pump,
'h_val' => $hVal,
'eta_val' => $this->valueAt($this->curve($pump, 'ETA'), $q),
'plot' => $this->curveDrawable($hCurve, 0) ? $this->plot($hCurve) : null,
];
})
->filter()
->when($h !== null, fn (Collection $c) => $c->filter(
fn ($m) => $h >= $m->h_val - 5 && $h <= $m->h_val + 8
))
->sortByDesc('eta_val')
->values();
}
}
+153
View File
@@ -0,0 +1,153 @@
<?php
namespace App\Services;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use SoapClient;
/**
* Integracja z SYNDIS (SOAP) - pobór aktualnego pomiaru studni i zapis do bazy.
* Port legacy soap.php (SyndisConnector) + ajax_getdata4well.php.
* Funkcje składowane MySQL zastąpione własnym kodem ([[port-db-functions-to-php]]).
*
* SKALA: SYNDIS zwraca wydajność w m³/min; do bazy zapisujemy m³/h (×60) -
* baza zawsze m³/h ([[flow-unit-display]]).
*/
class SyndisService
{
/** Detale seg_dev_resources_det aktualizowane przy pobraniu (DRD_RESOURCES_DETAIL => klucz danych SYNDIS / wyliczenie). */
private function db()
{
return DB::connection('softspm');
}
/**
* Pobiera aktualne dane studni z SYNDIS i zapisuje pomiar. Zwraca true gdy
* zapisano (pompa załączona), false gdy pompa wyłączona / brak danych.
*
* @throws \Throwable gdy SOAP/SYNDIS niedostępny (obsługiwane w kontrolerze)
*/
public function refreshWell(int $drs, string $wellName, string $user): bool
{
$data = $this->getWellData($wellName);
if ((int) $this->raw($data, 'pumpOnOff') !== 1) {
return false; // legacy: zapis tylko gdy pompa pracuje
}
$v = fn (string $k) => $this->num($this->raw($data, $k));
// Przeliczenia jak legacy:
$pp = $v('pressureInPistonBody') - (($v('barometricPressuremA') * 10) / 98);
$lp = (float) ($this->lastDetailValue(240, $drs) ?? 0);
$hh = $lp - $v('dynamicWaterTable');
$qH = $v('pumpEfficiency') * 60; // m³/min -> m³/h
$now = Carbon::now()->toDateTimeString();
$meas = [
'RHD_DRS_ID' => $drs,
'RHD_DATE' => $now,
'RHD_HD_VAL' => $v('dynamicWaterTable'),
'RHD_Q_VAL' => $qH,
'RHD_P1_VAL' => $v('pressureBeforeThrottle'),
'RHD_P2_VAL' => $v('pressureAfterThrottle'),
'RHD_I_VAL' => $v('currentL1'),
'RHD_U_VAL' => $v('voltageL12'),
'RHD_PE_VAL' => $v('activePower'),
'RHD_COS_VAL' => $v('cos'),
'RHD_PP_VAL' => $pp,
'RHD_HH_VAL' => $hh,
'RHD_WTEMP_VAL' => $v('waterTemperature'),
'RHD_DGN_VAL' => $v('currentDiagnosticsOperationState'),
'RHD_AUDIT_CD' => $now,
];
// Detale do podmiany w seg_dev_resources_det (jak legacy delete+insert).
$details = [
200 => $qH,
205 => $v('dynamicWaterTable'),
215 => $v('pressureBeforeThrottle'),
220 => $v('pressureAfterThrottle'),
245 => $v('waterTemperature'),
260 => $v('activePower'),
265 => $v('activeEnergy'),
270 => $v('currentL1'),
275 => $v('voltageL12'),
280 => $v('cos'),
401 => $pp,
402 => $hh,
];
$this->db()->transaction(function () use ($drs, $meas, $details, $user, $now) {
$this->db()->table('rep_soap_syndis_quality_data')
->where('SQD_DRS_ID', $drs)->update(['SQD_AUDIT_CD' => $now]);
$this->db()->table('rep_his_data_everyday')->insert($meas);
$this->db()->table('rep_his_data_demand')->insert($meas);
foreach ($details as $detail => $value) {
$this->db()->table('seg_dev_resources_det')
->where('DRD_RESOURCES_DETAIL', $detail)
->where('DRD_DRS_ID', $drs)
->delete();
$this->db()->table('seg_dev_resources_det')->insert([
'DRD_DRS_ID' => $drs,
'DRD_RESOURCES_DETAIL'=> $detail,
'DRD_VALUE' => (string) $value,
'DRD_EXPORT_SEGAP' => '1',
'DRD_AUDIT_USR' => $user,
'DRD_AUDIT_DATE' => $now,
]);
}
});
return true;
}
/** Wywołanie SOAP GetWellData -> tablica (port SyndisConnector::GetWellDataAsArray). */
private function getWellData(string $wellName): array
{
if (! class_exists(SoapClient::class)) {
throw new \RuntimeException('Rozszerzenie PHP soap nie jest dostępne na serwerze.');
}
$cfg = config('softspm.syndis');
$client = new SoapClient($cfg['wsdl'], ['trace' => 1, 'cache_wsdl' => WSDL_CACHE_NONE]);
if (! empty($cfg['location'])) {
$client->__setLocation($cfg['location']);
}
return $this->objectToArray($client->GetWellData(['id' => $wellName]));
}
/** Ostatnia wartość detalu (port getLastValue) - dla Lp przy przeliczeniu Hh. */
private function lastDetailValue(int $detail, int $drs): ?string
{
return $this->db()->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');
}
/** SYNDIS zwraca pola jako {'value' => x}; wyciąga surową wartość. */
private function raw(array $data, string $key): mixed
{
return $data[$key]['value'] ?? null;
}
private function num(mixed $v): float
{
return (float) str_replace(',', '.', (string) ($v ?? 0));
}
private function objectToArray(mixed $d): mixed
{
if (is_object($d)) {
$d = get_object_vars($d);
}
return is_array($d) ? array_map([$this, 'objectToArray'], $d) : $d;
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
namespace App\Services;
/**
* Oceny pracy układu pompowego (port func.php: wellEvaluation4_q / _HfQ_new /
* _Throt / _LineLoss + setRedYellowLimes / setYellowGreenLimes + sprawność).
* Skala m³/h (baza UAT bez ×60 - jak [[use-newest-methods]] / katalog pomp).
*
* Każda ocena zwraca ['zone' => 'green'|'yellow'|'red', 'text' => string]
* albo null gdy nie da się ocenić (brak danych) - jak legacy.
*/
class WellAssessmentService
{
private const T_OPT = 'Praca optymalna';
private const T_Q_YELLOW = 'Praca na granicy przedziału stosowalności';
private const T_Q_RED = 'Praca poza przedziałem stosowalności';
private const T_Q_NODATA = 'Brak danych w systemie';
private const T_HFQ_YELLOW = 'Charakterystyka pompy ulega odkształcaniu, możliwe przecieki na złączach rurociągu';
private const T_HFQ_RED = 'Odkształcona charakterystyka pompy, możliwa utrata szczelności rurociągu';
private const T_THROT_YELLOW = 'Wyraźne dławienie w układzie pompowym';
private const T_THROT_RED = 'Silne dławienie w układzie pompowym';
private const T_LOSS_YELLOW = 'Wyraźne straty liniowe w rurociągu';
private const T_LOSS_RED = 'Wysokie straty liniowe w rurociągu';
/** Przedział Q: porównanie wydajności do nominału. */
public function q(float $qn, ?float $q): ?array
{
if ($q === null || $q <= 0) {
return ['zone' => 'red', 'text' => self::T_Q_NODATA];
}
if (0.7 * $qn <= $q && $q <= 1.22 * $qn) {
return ['zone' => 'green', 'text' => self::T_OPT];
}
if ((0.55 * $qn <= $q && $q < 0.7 * $qn) || (1.22 * $qn < $q && $q <= 1.35 * $qn)) {
return ['zone' => 'yellow', 'text' => self::T_Q_YELLOW];
}
return ['zone' => 'red', 'text' => self::T_Q_RED];
}
/** H=f(Q): odchyłka wysokości podnoszenia ze stacji prób (HPS) od (Pp-Hh). */
public function hfq(?float $hps, ?float $pp, ?float $hh): ?array
{
if (! $pp || $hps === null || $hps == 0.0) {
return null;
}
$dev = ($hps - ($pp - $hh)) / $hps;
if ($dev >= -0.05 && $dev <= 0.05) {
return ['zone' => 'green', 'text' => self::T_OPT];
}
if (($dev < -0.05 && $dev >= -0.07) || ($dev > 0.05 && $dev <= 0.07)) {
return ['zone' => 'yellow', 'text' => self::T_HFQ_YELLOW];
}
return ['zone' => 'red', 'text' => self::T_HFQ_RED];
}
/** Dławienie: spadek P1-P2 na zasuwie dławiącej, progi wg zwierciadła HD. */
public function throttling(?float $hd, ?float $p1, ?float $p2): ?array
{
if ($hd === null || $p1 === null || $p2 === null) {
return null;
}
$hsa = $p1 - $p2;
$green = $this->pick($hd, [[50, 6], [100, 8], [200, 10], [350, 13], [450, 16]]);
$yellow = $this->pick($hd, [[50, 15], [100, 20], [200, 28], [350, 35], [450, 39]]);
if ($green === null) {
return null; // HD poza zakresem - brak oceny (jak legacy)
}
if ($hsa <= $green) {
return ['zone' => 'green', 'text' => self::T_OPT];
}
if ($hsa <= $yellow) {
return ['zone' => 'yellow', 'text' => self::T_THROT_YELLOW];
}
return ['zone' => 'red', 'text' => self::T_THROT_RED];
}
/** Straty liniowe: HSL = Pp - Lp - P1, progi wg średnicy rurociągu DR i Lp. */
public function lineLoss(?float $p1, ?float $pp, ?float $lp, ?float $dr): ?array
{
if (! $pp || $p1 === null || $lp === null || $dr === null) {
return null;
}
$hsl = $pp - $lp - $p1;
// Progi [green, yellow] zależne od DR. Każdy próg to pick() po Lp.
if ($dr > 0.04 && $dr < 0.145) {
$green = $this->pick($lp, [[50, 3.5], [100, 6.4], [200, 10], [300, 18], [450, 25]]);
$yellow = $this->pick($lp, [[50, 4.9], [100, 9.5], [200, 16], [300, 24], [450, 35]]);
// legacy: powyżej "yellow" -> wciąż YELLOW + wymóg weryfikacji p1/p2
$overText = self::T_LOSS_RED.'. Wymagana weryfikacja p1 i p2';
$overZone = 'yellow';
} elseif ($dr > 0.145 && $dr <= 0.160) {
$green = $this->pick($lp, [[50, 2.3], [100, 4.3], [200, 8.6], [300, 12], [450, 21]]);
$yellow = $this->pick($lp, [[50, 4.5], [100, 7.2], [200, 14], [300, 18], [450, 27]]);
$overText = self::T_LOSS_RED;
$overZone = 'red';
} elseif ($dr > 0.195 && $dr <= 0.215) {
$green = $this->pick($lp, [[50, 2], [100, 3], [200, 6.5], [300, 9.5], [450, 12]]);
$yellow = $this->pick($lp, [[50, 3], [100, 5.5], [200, 11], [300, 15], [450, 18]]);
$overText = self::T_LOSS_RED;
$overZone = 'red';
} else {
return null; // DR poza obsługiwanymi zakresami - brak oceny (jak legacy)
}
if ($green === null) {
return null;
}
if ($hsl <= $green) {
return ['zone' => 'green', 'text' => self::T_OPT];
}
if ($hsl <= $yellow) {
return ['zone' => 'yellow', 'text' => self::T_LOSS_YELLOW];
}
return ['zone' => $overZone, 'text' => $overText];
}
/** Granica czerwony/żółty energochłonności w funkcji Q (próg sprawności). */
public function redYellowLimit(float $q): float
{
return match (true) {
$q > 0 && $q <= 25 => 0.00210526315789473 * $q + 0.267368421052632,
$q > 25 && $q <= 40 => 0.00133333333333334 * $q + 0.286666666666667,
$q > 40 && $q <= 60 => 0.000749999999999997 * $q + 0.31,
$q > 60 && $q <= 260 => 0.000225000000000001 * $q + 0.3415,
$q > 260 && $q <= 500 => 4.16666666666662e-5 * $q + 0.389166666666667,
$q > 500 => 1.99999999999997e-5 * $q + 0.4,
default => 0.0,
};
}
/** Granica żółty/zielony energochłonności w funkcji Q. */
public function yellowGreenLimit(float $q): float
{
return match (true) {
$q > 0 && $q <= 8 => 0.00999999999999998 * $q + 0.3,
$q > 8 && $q <= 19 => 0.00545454545454544 * $q + 0.336363636363637,
$q > 19 && $q <= 38 => 0.00210526315789474 * $q + 0.4,
$q > 38 && $q <= 60 => 0.000454545454545451 * $q + 0.462727272727273,
$q > 60 && $q <= 500 => 0.000170454545454545 * $q + 0.479772727272727,
$q > 500 => 9.99999999999959e-6 * $q + 0.560000000000001,
default => 0.0,
};
}
/** Sprawność układu η = Q·(HD+P2) / (367·Pe). Pe<=0 traktujemy jak 1 (jak legacy). */
public function efficiency(?float $q, ?float $hd, ?float $p2, ?float $pe): ?float
{
if ($q === null || $hd === null || $p2 === null) {
return null;
}
$pe = ($pe === null || $pe <= 0) ? 1.0 : $pe;
return ($q * ($hd + $p2)) / (367 * $pe);
}
/** Strefa energochłonności: η>=yg zielona, >=ry żółta, inaczej czerwona. */
public function energyZone(float $eta, float $ry, float $yg): string
{
if ($eta >= $yg) {
return 'green';
}
return $eta >= $ry ? 'yellow' : 'red';
}
/** Wybór progu po przedziałach: [[górna granica wykluczna, wartość], ...]. */
private function pick(float $x, array $bands): ?float
{
foreach ($bands as [$limit, $value]) {
if ($x < $limit) {
return (float) $value;
}
}
return null;
}
}
+276
View File
@@ -0,0 +1,276 @@
<?php
namespace App\Services;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/**
* Logika domenowa studni (migracja site301A/site301E/site301inf).
*
* Zapisy są wielotabelowe i w legacy szły BEZ transakcji (ryzyko danych
* częściowych). Tu każdy zapis jest opakowany w DB::transaction() na
* połączeniu `softspm`.
*
* Lokalizacja słowników: kolumna języka wybierana wg locale (pl→POL/en→ENG/
* de→GER), domyślnie POL.
*/
class WellService
{
/** DRS_DEV_TYPE dla studni. */
private const DEV_TYPE_WELL = 12;
/** Domyślny status zasobu nowej studni (legacy: DRS_STATUS_DIC_ID=3). */
private const DEFAULT_STATUS_DIC_ID = 3;
private function db()
{
return DB::connection('softspm');
}
/** Kolumna języka w sys_dictionaries wg aktualnego locale. */
private function langColumn(): string
{
return match (app()->getLocale()) {
'en' => 'DIC_ENG_LANG',
'de' => 'DIC_GER_LANG',
default => 'DIC_POL_LANG',
};
}
// ===================== LOOKUPY (do formularzy) =====================
/** Ujęcia wody (bariera): SWI_ID => SWI_NAME. */
public function waterIntakes()
{
return $this->db()->table('seg_water_intakes')
->orderBy('SWI_NAME')
->pluck('SWI_NAME', 'SWI_ID');
}
/** Słownik po ID: DIC_ID => opis (dla WELL_CATEGORY, WELL_STATUS_LEVEL_01). */
public function dictionaryById(string $type)
{
$lang = $this->langColumn();
return $this->db()->table('sys_dictionaries')
->where('DIC_TYPE', $type)
->orderBy($lang)
->pluck($lang, 'DIC_ID');
}
/** Słownik po opisie: lista wartości tekstowych (dla WELL_SYSTEM_TYPE,
* WELL_STATUS_LEVEL_03 - legacy zapisuje sam opis, nie DIC_ID). */
public function dictionaryDescriptions(string $type)
{
$lang = $this->langColumn();
return $this->db()->table('sys_dictionaries')
->where('DIC_TYPE', $type)
->orderBy($lang)
->pluck($lang);
}
/** Dane studni do formularza edycji (wg zapytania z site301inf). */
public function findForEdit(int $drsId): ?object
{
$well = $this->db()->table('seg_wells as w')
->join('seg_dev_resources as drw', function ($j) {
$j->on('drw.DRS_DEV_ID', '=', 'w.WLL_ID')->where('drw.DRS_DEV_TYPE', '=', self::DEV_TYPE_WELL);
})
->leftJoin('seg_object_hole_cards as ohc', 'ohc.OHC_DRS_ID', '=', 'drw.DRS_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', $drsId)
->select([
'drw.DRS_ID as drs_id',
'w.WLL_ID as well_id',
'w.WLL_NAME as name',
'w.WLL_SWI_ID as water_intake_id',
'w.WLL_CAT_DIC_ID as category_id',
'w.WLL_PP_SCOPE as pp_scope',
'w.WLL_HH_SCOPE as hh_scope',
'w.WLL_SYS_TYPE as sys_type',
'w.WLL_STAT_DESC as status_desc',
'drw.DRS_RESOURCES_DIC_ID as status_id',
'drw.DRS_AVAILABLE_DATE as available_date',
'ohc.OHC_DL_GEOGR as latitude',
'ohc.OHC_SZER_GEOGR as longitude',
'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',
])
->first();
if (! $well) {
return null;
}
// Ostatnia zabudowa (do edycji daty zabudowy WLE_DATE_FROM).
$exchange = $this->db()->table('seg_wells_exchanged')
->where('WLE_WLL_ID', $well->well_id)
->orderByDesc('WLE_NR_EXCHANGE')
->first();
$well->wle_id = $exchange->WLE_ID ?? null;
$well->wle_date_from = $exchange->WLE_DATE_FROM ?? null;
return $well;
}
/** Czy istnieje studnia o danej nazwie (kontrola unikalności). */
public function nameExists(string $name): bool
{
return $this->db()->table('seg_wells')->where('WLL_NAME', $name)->exists();
}
// ===================== ZAPIS =====================
/**
* Dodanie studni - transakcyjny odpowiednik site301inf (linie 27-66).
* Zapisuje do: seg_wells, seg_dev_resources, seg_dev_resources_det (×3),
* seg_object_hole_cards, seg_object_hole_cards_his, seg_dev_resources_his.
*
* @return int DRS_ID utworzonego zasobu studni.
*/
public function create(array $data, string $department, string $user): int
{
return $this->db()->transaction(function () use ($data, $department, $user) {
$now = Carbon::now();
$intakeName = (string) $this->db()->table('seg_water_intakes')
->where('SWI_ID', $data['water_intake_id'])
->value('SWI_NAME');
$wellId = $this->db()->table('seg_wells')->insertGetId([
'WLL_NAME' => $data['name'],
'WLL_SWI_ID' => $data['water_intake_id'],
'WLL_BARIERA' => $intakeName,
'WLL_CAT_DIC_ID' => $data['category_id'],
'WLL_LATITUDE' => $this->decimal($data['latitude']),
'WLL_LONGITUDE' => $this->decimal($data['longitude']),
], 'WLL_ID');
$drsId = $this->db()->table('seg_dev_resources')->insertGetId([
'DRS_DEV_ID' => $wellId,
'DRS_DEV_TYPE' => self::DEV_TYPE_WELL,
'DRS_AVAILABLE_DATE' => $now->toDateString(),
'DRS_BUY_DATE' => $now->toDateString(),
'DRS_STATUS_DIC_ID' => self::DEFAULT_STATUS_DIC_ID,
'DRS_RESOURCES_DIC_ID' => $data['status_id'],
'DRS_DEPARTMENT' => $department,
'DRS_AUDIT_USR' => $user,
'DRS_AUDIT_DATE' => $now->toDateTimeString(),
], 'DRS_ID');
// Parametry techniczne: średnica (DST), głębokość (LS), zasilanie (LZ).
$this->insertDetail($drsId, 'DST', $this->decimal($data['diameter']), $now, $user);
$this->insertDetail($drsId, 'LS', $this->decimal($data['depth']), $now, $user);
$this->insertDetail($drsId, 'LZ', $this->decimal($data['supply']), $now, $user);
// Karta otworu.
$this->db()->table('seg_object_hole_cards')->insert([
'OHC_DRS_ID' => $drsId,
'OHC_UJ' => $intakeName,
'OHC_NAME' => $data['name'],
'OHC_DATA_ODDANIA' => $data['available_date'] ?? null,
'OHC_GLEB_ODWIER' => $data['depth'],
'OHC_GLEB_OSTAT' => $data['depth'],
'OHC_DL_GEOGR' => $this->decimal($data['latitude']),
'OHC_SZER_GEOGR' => $this->decimal($data['longitude']),
'OHC_PLC' => $department,
]);
$this->db()->table('seg_object_hole_cards_his')->insert([
'OHH_DRS_ID' => $drsId,
'OHH_UJ' => $intakeName,
'OHH_NAME' => $data['name'],
'OHH_ZASYP' => $data['supply'],
'OHH_PLC' => $department,
]);
// Kopia historyczna zasobu (kolumny _his identyczne z seg_dev_resources).
$this->db()->statement(
'INSERT INTO seg_dev_resources_his SELECT * FROM seg_dev_resources WHERE DRS_ID = ?',
[$drsId]
);
return $drsId;
});
}
/**
* Aktualizacja studni - transakcyjny odpowiednik site301inf (linie 138-155).
*/
public function update(int $drsId, array $data, string $user): void
{
$this->db()->transaction(function () use ($drsId, $data) {
$intakeName = (string) $this->db()->table('seg_water_intakes')
->where('SWI_ID', $data['water_intake_id'])
->value('SWI_NAME');
$this->db()->table('seg_wells')->where('WLL_ID', $data['well_id'])->update([
'WLL_NAME' => $data['name'],
'WLL_SWI_ID' => $data['water_intake_id'],
'WLL_BARIERA' => $intakeName,
'WLL_CAT_DIC_ID' => $data['category_id'],
'WLL_PP_SCOPE' => $data['pp_scope'],
'WLL_HH_SCOPE' => $data['hh_scope'],
'WLL_SYS_TYPE' => $data['sys_type'],
'WLL_STAT_DESC' => $data['status_desc'],
]);
$this->db()->table('seg_dev_resources')->where('DRS_ID', $drsId)->update([
'DRS_AVAILABLE_DATE' => $data['available_date'] ?: null,
'DRS_RESOURCES_DIC_ID' => $data['status_id'],
]);
$this->db()->table('seg_object_hole_cards')->where('OHC_DRS_ID', $drsId)->update([
'OHC_NAME' => $data['name'],
'OHC_UJ' => $intakeName,
'OHC_DL_GEOGR' => $data['latitude'],
'OHC_SZER_GEOGR' => $data['longitude'],
]);
// Data zabudowy ostatniego agregatu (tylko gdy istnieje wpis wymiany).
if (! empty($data['wle_id'])) {
$dateFrom = trim(($data['available_date'] ?? '').' '.($data['build_time'] ?? ''));
$this->db()->table('seg_wells_exchanged')->where('WLE_ID', $data['wle_id'])->update([
'WLE_DATE_FROM' => $dateFrom ?: null,
]);
}
});
}
// ===================== POMOCNICZE =====================
/** Wstawia parametr techniczny do seg_dev_resources_det (DIC po nazwie). */
private function insertDetail(int $drsId, string $dicName, string $value, Carbon $now, string $user): void
{
$detailId = $this->db()->table('sys_dictionaries')
->where('DIC_TYPE', 'WELL_TECHNICAL_PARAMETERS')
->where('DIC_NAME', $dicName)
->value('DIC_ID');
$this->db()->table('seg_dev_resources_det')->insert([
'DRD_DRS_ID' => $drsId,
'DRD_RESOURCES_DETAIL' => $detailId,
'DRD_VALUE' => $value,
'DRD_AUDIT_DATE' => $now->toDateTimeString(),
'DRD_AUDIT_USR' => $user,
]);
}
/** Normalizacja liczby (przecinek → kropka), jak getFloatValue w legacy. */
private function decimal(?string $value): string
{
return str_replace(',', '.', trim((string) $value));
}
}