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
@@ -0,0 +1,135 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Concerns\SortsLists;
use App\Models\Engine;
use App\Models\Manufacturer;
use App\Models\Pump;
use App\Services\PumpService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Katalog agregatow (legacy site205 / site205E) - "agregat" = pompa + przypisane
* silniki (seg_pump_engine_rel). Uklad lista -> szczegoly (jak pozostale katalogi).
*/
class AggregateController extends Controller
{
use SortsLists;
private const MCF_ALL = 100;
/** Sortowalne kolumny listy: klucz nagłówka => kolumna/alias SQL. */
private const SORTABLE = [
'name' => 'PUP_NAME',
'flow' => 'PUP_Q_N',
'mcf' => 'm.MCF_NAME',
'engines' => 'engines_count',
];
public function __construct(private readonly PumpService $pumps) {}
public function index(Request $request)
{
$by = $request->query('by', 'name');
$q = trim((string) $request->query('q', ''));
$mcf = (int) $request->query('mcf', self::MCF_ALL);
[$sort, $dir] = $this->sortParams($request);
$aggregates = Pump::query()
->visible()->with('manufacturer')->where('PUP_Q_N', '>', 0)
->select('seg_pumps.*')
->selectSub(
DB::connection('softspm')->table('seg_pump_engine_rel')
->whereColumn('RPE_PUP_ID', 'seg_pumps.PUP_ID')->selectRaw('count(*)'),
'engines_count'
)
->when($q !== '' && $by === 'name', fn ($w) => $w->where('PUP_NAME', 'like', '%'.$q.'%'))
->when($q !== '' && $by === 'flow', function ($w) use ($q) {
$v = (float) str_replace(',', '.', $q);
$w->whereBetween('PUP_Q_N', [$v - $v * 0.1, $v + $v * 0.1]);
})
->when($mcf !== self::MCF_ALL, fn ($w) => $w->where('PUP_MCF_ID', $mcf))
// Sortowanie po producencie wymaga joinu (relacja nie jest w FROM).
->when($sort === 'mcf', fn ($w) => $w
->leftJoin('seg_manufacturers as m', 'm.MCF_ID', '=', 'seg_pumps.PUP_MCF_ID'))
->when($sort !== null, fn ($w) => $w->orderBy(self::SORTABLE[$sort], $dir))
->orderBy('PUP_Q_N')->orderBy('PUP_NAME')
->paginate(20)->withQueryString();
return view('catalog.aggregates.index', [
'aggregates' => $aggregates,
'manufacturers' => Manufacturer::orderBy('MCF_NAME')->pluck('MCF_NAME', 'MCF_ID'),
'by' => $by, 'q' => $q, 'mcf' => $mcf, 'mcfAll' => self::MCF_ALL,
]);
}
public function show(Request $request, Pump $pump)
{
$pump->load('manufacturer');
$h = $this->pumps->curve($pump, 'H');
$eta = $this->pumps->curve($pump, 'ETA');
$hDrawable = $this->pumps->curveDrawable($h, 0);
$etaDrawable = $this->pumps->curveDrawable($eta, 1);
$engines = $this->assignedEngines($pump->PUP_ID);
$engineId = (int) $request->query('engine') ?: (int) ($engines->first()->ENG_ID ?? 0);
$engine = $engineId && $engines->firstWhere('ENG_ID', $engineId)
? Engine::with('manufacturer')->find($engineId)
: null;
return view('catalog.aggregates.show', [
'pump' => $pump,
'engines' => $engines,
'engine' => $engine,
'h' => $h, 'eta' => $eta,
'hDrawable' => $hDrawable, 'etaDrawable' => $etaDrawable,
'hPlot' => $hDrawable ? $this->pumps->plot($h) : null,
'etaPlot' => $etaDrawable ? $this->pumps->plot($eta) : null,
]);
}
public function edit(Pump $pump)
{
$allEngines = Engine::query()->visible()->with('manufacturer')
->orderBy('ENG_POWER')->orderBy('ENG_NAME')->get();
$assigned = DB::connection('softspm')->table('seg_pump_engine_rel')
->where('RPE_PUP_ID', $pump->PUP_ID)->pluck('RPE_ENG_ID')->all();
return view('catalog.aggregates.edit', [
'pump' => $pump->load('manufacturer'),
'allEngines' => $allEngines,
'assigned' => array_map('intval', $assigned),
]);
}
public function update(Request $request, Pump $pump)
{
$engineIds = collect($request->input('engines', []))->map(fn ($v) => (int) $v)->filter()->unique();
DB::connection('softspm')->transaction(function () use ($pump, $engineIds) {
DB::connection('softspm')->table('seg_pump_engine_rel')->where('RPE_PUP_ID', $pump->PUP_ID)->delete();
foreach ($engineIds as $eid) {
DB::connection('softspm')->table('seg_pump_engine_rel')
->insert(['RPE_PUP_ID' => $pump->PUP_ID, 'RPE_ENG_ID' => $eid]);
}
});
return redirect()->route('aggregates.show', $pump)
->with('status', 'Zaktualizowano silniki dla pompy "'.$pump->PUP_NAME.'".');
}
private function assignedEngines(int $pumpId)
{
return DB::connection('softspm')->table('seg_pump_engine_rel as r')
->join('seg_engines as e', 'e.ENG_ID', '=', 'r.RPE_ENG_ID')
->leftJoin('seg_manufacturers as m', 'm.MCF_ID', '=', 'e.ENG_MCF_ID')
->where('r.RPE_PUP_ID', $pumpId)
->select('e.ENG_ID', 'e.ENG_NAME', 'e.ENG_VOLTAGE', 'e.ENG_POWER', 'e.ENG_ETA', 'm.MCF_NAME')
->orderBy('e.ENG_POWER')->orderBy('e.ENG_NAME')->get();
}
}
@@ -0,0 +1,114 @@
<?php
namespace App\Http\Controllers;
use App\Services\PumpService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Wybór agregatu pompowego (legacy site201 "Wysokość podnoszenia dla posiadanych
* typów pomp"). Narzędzie doboru: użytkownik podaje wydajność Q (i opcjonalnie
* wysokość podnoszenia H), system zwraca pompy obejmujące to Q wraz z H(Q)/η(Q),
* listę zestawów pompa+silnik posortowaną malejąco po sprawności oraz wykres
* nałożonych krzywych H. Klik wiersza -> szczegóły zestawu (aggregates.show).
*
* Szczegół zestawu = istniejący ekran agregatu (site201det był jego duplikatem),
* więc nie powielamy - kierujemy do aggregates.show?pump=X&engine=Y.
*/
class AggregateSelectionController extends Controller
{
public function __construct(private readonly PumpService $pumps) {}
public function index(Request $request)
{
$qIn = $request->query('q');
$hIn = $request->query('h');
$q = $this->toFloat($qIn);
$h = $this->toFloat($hIn);
$searched = $q !== null && $q > 0;
$matches = $searched ? $this->pumps->matchingAggregates($q, $h) : collect();
[$rows, $chart] = $matches->isNotEmpty()
? [$this->buildRows($matches), $this->buildChart($matches)]
: [collect(), null];
return view('catalog.aggregate-selection.index', [
'q' => $qIn, // surowe (do ponownego wyświetlenia w polu)
'h' => $hIn,
'qVal' => $q, // liczbowe (do nagłówka Q_sz)
'rows' => $rows,
'chart' => $chart,
'searched' => $searched,
]);
}
/**
* Rozwija dopasowane pompy na wiersze pompa+silnik. Silniki pompy wg mocy
* (ENG_POWER) - jak legacy. leftJoin: pompa bez silnika wciąż daje 1 wiersz.
*/
private function buildRows($matches)
{
$engines = DB::connection('softspm')->table('seg_pump_engine_rel as r')
->leftJoin('seg_engines as e', 'e.ENG_ID', '=', 'r.RPE_ENG_ID')
->whereIn('r.RPE_PUP_ID', $matches->pluck('pump.PUP_ID')->all())
->orderBy('e.ENG_POWER')
->select('r.RPE_PUP_ID', 'e.ENG_ID', 'e.ENG_NAME', 'e.ENG_POWER', 'e.ENG_VOLTAGE')
->get()
->groupBy('RPE_PUP_ID');
$rows = collect();
$lp = 1;
foreach ($matches as $m) {
$pumpEngines = $engines->get($m->pump->PUP_ID) ?: collect([null]);
foreach ($pumpEngines as $e) {
$rows->push((object) [
'lp' => $lp++,
'pump_id' => $m->pump->PUP_ID,
'pump_name' => $m->pump->PUP_NAME,
'engine_id' => $e->ENG_ID ?? null,
'engine_name' => $e->ENG_NAME ?? null,
'q_n' => $m->pump->PUP_Q_N,
'h_val' => $m->h_val,
'eta_val' => $m->eta_val,
'power' => $e->ENG_POWER ?? null,
'voltage' => $e->ENG_VOLTAGE ?? null,
]);
}
}
return $rows;
}
/**
* Dane wykresu nałożonych krzywych H (wspólne osie). xMax = najszersza
* krzywa, yMax = najwyższa (zaokrąglona w górę do 10, jak legacy).
*/
private function buildChart($matches): ?array
{
$curves = $matches->filter(fn ($m) => $m->plot)
->map(fn ($m) => ['name' => $m->pump->PUP_NAME, 'plot' => $m->plot])
->values();
if ($curves->isEmpty()) {
return null;
}
return [
'curves' => $curves,
'xMax' => (float) $curves->max(fn ($c) => $c['plot']['xMax']),
'yMax' => max(10.0, ceil($curves->max(fn ($c) => $c['plot']['yMax']) / 10) * 10),
];
}
/** Pole tekstowe -> float (przecinek dziesiętny), puste -> null. */
private function toFloat($v): ?float
{
if ($v === null || trim((string) $v) === '') {
return null;
}
return (float) str_replace(',', '.', (string) $v);
}
}
@@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
/**
* Logowanie SoftSPM_2 na istniejącej bazie `sys_users` (database-first).
*
* Strategia haseł (przejściowa):
* - Legacy trzyma USR_PASSWORD = md5(hasło).
* - Akceptujemy logowanie po MD5 (kompatybilność) ORAZ po bcrypt.
* - Po udanym logowaniu MD5, jeśli kolumna USR_PASSWORD pomieści bcrypt
* (>=60 znaków) i włączony jest flag SOFTSPM_REHASH - przepisujemy hash
* na bcrypt. Domyślnie WYŁĄCZONE, żeby nie uszkodzić zbyt krótkiej kolumny.
* Decyzja o rozszerzeniu kolumny / osobnej kolumnie do ustalenia.
*/
class LoginController extends Controller
{
/** Pokaż formularz logowania. */
public function show()
{
return view('auth.login');
}
/** Obsłuż próbę logowania. */
public function attempt(Request $request)
{
// Faza testów: oddział może być wymuszony w configu (pole ukryte w logowaniu).
$forcedDepartment = config('softspm.login_department');
$rules = [
'username' => ['required', 'string', 'max:100'],
'password' => ['required', 'string'],
'lang' => ['nullable', 'in:pl,en,de'],
];
if (! $forcedDepartment) {
$rules['department'] = ['required', 'string', Rule::in(config('softspm.departments'))];
}
$credentials = $request->validate($rules);
$department = $forcedDepartment ?: $credentials['department'];
$user = User::query()
->where('USR_USERNAME', $credentials['username'])
->where('USR_STATUS', '1')
->first();
if (! $user || ! $this->passwordMatches($credentials['password'], $user->USR_PASSWORD)) {
throw ValidationException::withMessages([
'username' => 'Nieprawidłowy login lub hasło.',
]);
}
// Opcjonalna migracja MD5 -> bcrypt (patrz docblock klasy).
$this->maybeRehash($user, $credentials['password']);
Auth::login($user);
$request->session()->regenerate();
// Oddział filtrujący listy obiektów (odpowiednik legacy 'syndis_eq_stock').
$request->session()->put('department', $department);
// Zapamiętaj wybrany język UI (i18n w kolejnym etapie).
if (! empty($credentials['lang'])) {
$request->session()->put('locale', $credentials['lang']);
}
return redirect()->intended(route('dashboard'));
}
/** Wyloguj. */
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
/**
* Weryfikacja hasła: bcrypt (nowe) lub md5 (legacy).
*/
private function passwordMatches(string $plain, ?string $stored): bool
{
if (empty($stored)) {
return false;
}
// Hash bcrypt zaczyna się od $2y$ / $2a$ - sprawdź najpierw.
if (str_starts_with($stored, '$2')) {
return Hash::check($plain, $stored);
}
// Legacy: md5 (32 znaki hex). hash_equals chroni przed timing-attack.
return hash_equals(strtolower($stored), md5($plain));
}
/**
* Po logowaniu MD5 przepisz hash na bcrypt - tylko gdy bezpiecznie.
*/
private function maybeRehash(User $user, string $plain): void
{
if (! config('softspm.rehash_passwords', false)) {
return;
}
if (str_starts_with((string) $user->USR_PASSWORD, '$2')) {
return; // już bcrypt
}
$user->USR_PASSWORD = Hash::make($plain);
$user->save();
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Concerns\SortsLists;
use App\Http\Requests\StoreCableRequest;
use App\Http\Requests\UpdateCableRequest;
use App\Models\Equipment;
use App\Models\Manufacturer;
use App\Services\EquipmentFileService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Katalog kabli - pełny CRUD (legacy site206 / site206A / site206E / site206inf).
* Dane w seg_equipments (EQU_TYPE_ID=13) + załączniki BLOB w seg_equipments_files.
* Usuwanie logiczne (EQU_STATUS='0'); blokowane gdy kabel jest na stanie
* (seg_equipments_stock) - jak legacy. Newest methods: Eloquent + Form Requests.
*/
class CableController extends Controller
{
use SortsLists;
/** EQU_TYPE_ID kabli (sys_dictionaries). */
private const TYPE_CABLE = 13;
/** Producent „Wszyscy" (jak legacy: 100). */
private const MCF_ALL = 100;
/** Sortowalne kolumny listy: klucz nagłówka => kolumna SQL. */
private const SORTABLE = [
'name' => 'EQU_NAME',
'mcf' => 'm.MCF_NAME',
];
public function __construct(private readonly EquipmentFileService $files) {}
public function index(Request $request)
{
$q = trim((string) $request->query('q', ''));
$mcf = (int) $request->query('mcf', self::MCF_ALL);
[$sort, $dir] = $this->sortParams($request);
$cables = Equipment::query()
->visible()->ofType(self::TYPE_CABLE)->with('manufacturer')
->when($q !== '', fn ($w) => $w->where('EQU_NAME', 'like', '%'.$q.'%'))
->when($mcf !== self::MCF_ALL, fn ($w) => $w->where('EQU_MCF_ID', $mcf))
->when($sort === 'mcf', fn ($w) => $w
->leftJoin('seg_manufacturers as m', 'm.MCF_ID', '=', 'seg_equipments.EQU_MCF_ID')
->select('seg_equipments.*'))
->when($sort !== null, fn ($w) => $w->orderBy(self::SORTABLE[$sort], $dir))
->orderBy('EQU_NAME')
->paginate(20)
->withQueryString();
return view('catalog.cables.index', [
'cables' => $cables,
'manufacturers' => $this->manufacturers(),
'q' => $q,
'mcf' => $mcf,
'mcfAll' => self::MCF_ALL,
]);
}
public function create()
{
return view('catalog.cables.create', ['manufacturers' => $this->manufacturers()]);
}
public function store(StoreCableRequest $request)
{
$data = $request->validated();
$cable = Equipment::create([
'EQU_NAME' => $data['name'],
'EQU_MCF_ID' => $data['manufacturer_id'],
'EQU_DESCRIPTION' => $data['description'] ?? null,
'EQU_TYPE_ID' => self::TYPE_CABLE,
'EQU_STATUS' => '1',
]);
$this->saveFiles($request, $cable->EQU_ID);
return redirect()->route('cables.show', $cable)
->with('status', 'Dodano typ kabla „'.$cable->EQU_NAME.'".');
}
public function show(Equipment $cable)
{
abort_unless((int) $cable->EQU_TYPE_ID === self::TYPE_CABLE, 404);
$cable->load('manufacturer', 'files');
return view('catalog.cables.show', [
'cable' => $cable,
'document' => $cable->files->first(fn ($f) => ! $f->isImage()),
'image' => $cable->files->first(fn ($f) => $f->isImage()),
'inStock' => $this->inStock($cable->EQU_ID),
]);
}
public function edit(Equipment $cable)
{
abort_unless((int) $cable->EQU_TYPE_ID === self::TYPE_CABLE, 404);
$cable->load('manufacturer', 'files');
return view('catalog.cables.edit', [
'cable' => $cable,
'manufacturers' => $this->manufacturers(),
'document' => $cable->files->first(fn ($f) => ! $f->isImage()),
'image' => $cable->files->first(fn ($f) => $f->isImage()),
]);
}
public function update(UpdateCableRequest $request, Equipment $cable)
{
abort_unless((int) $cable->EQU_TYPE_ID === self::TYPE_CABLE, 404);
$data = $request->validated();
$cable->update([
'EQU_NAME' => $data['name'],
'EQU_MCF_ID' => $data['manufacturer_id'],
'EQU_DESCRIPTION' => $data['description'] ?? null,
]);
$this->saveFiles($request, $cable->EQU_ID);
return redirect()->route('cables.show', $cable)
->with('status', 'Zaktualizowano typ kabla „'.$cable->EQU_NAME.'".');
}
public function destroy(Equipment $cable)
{
abort_unless((int) $cable->EQU_TYPE_ID === self::TYPE_CABLE, 404);
// Legacy: nie usuwaj, gdy kabel jest na stanie (seg_equipments_stock).
if ($this->inStock($cable->EQU_ID)) {
return redirect()->route('cables.show', $cable)
->with('error', 'Nie można usunąć - kabel jest powiązany ze stanem magazynowym.');
}
$cable->update(['EQU_STATUS' => '0']); // usuwanie logiczne
return redirect()->route('cables.index')
->with('status', 'Usunięto typ kabla „'.$cable->EQU_NAME.'".');
}
/** Zapis przesłanych plików (dokumentacja + zdjęcie), jeśli są. */
private function saveFiles(Request $request, int $equId): void
{
$user = $request->user()->USR_USERNAME;
if ($request->hasFile('catalogue_file')) {
$this->files->store($equId, $request->file('catalogue_file'), $user);
}
if ($request->hasFile('image_file')) {
$this->files->store($equId, $request->file('image_file'), $user);
}
}
private function inStock(int $equId): bool
{
return DB::connection('softspm')->table('seg_equipments_stock')
->where('SES_EQU_ID', $equId)->exists();
}
private function manufacturers()
{
return Manufacturer::orderBy('MCF_NAME')->pluck('MCF_NAME', 'MCF_ID');
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Concerns;
use Illuminate\Http\Request;
/**
* Wspólne wyznaczanie parametrów sortowania list (nagłówki <x-sortable-th>).
* Kontroler udostępnia whitelistę kolumn przez stałą self::SORTABLE
* (klucz nagłówka => kolumna SQL). Zapobiega wstrzyknięciu dowolnej kolumny
* do ORDER BY - bierzemy tylko klucze z whitelisty.
*/
trait SortsLists
{
/**
* @return array{0: ?string, 1: string} [klucz sortowania lub null, kierunek 'asc'|'desc']
*/
protected function sortParams(Request $request): array
{
$sort = $request->query('sort');
$dir = $request->query('dir') === 'desc' ? 'desc' : 'asc';
if (! is_string($sort) || ! array_key_exists($sort, static::SORTABLE)) {
return [null, $dir];
}
return [$sort, $dir];
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
@@ -0,0 +1,110 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Pulpit / Mapa Obiektów. Nanosi studnie jako kropki na płaszczyznę wg
* LOKALNYCH współrzędnych geodezyjnych (seg_wells.WLL_LATITUDE/WLL_LONGITUDE -
* siatka kopalniana w metrach, NIE WGS84). Normalizacja min-max do obszaru mapy
* daje dokładny względny układ pola studni (bez internetu - air-gap OK).
*
* Kolor kropki: zielony = pompa pracuje (ON), czerwony = nie pracuje (OFF).
*/
class DashboardController extends Controller
{
/** Margines obszaru mapy (%), żeby kropki nie dotykały krawędzi. */
private const PAD = 6;
public function index(Request $request)
{
$department = $request->session()->get('department');
$rows = DB::connection('softspm')->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', '=', 12);
})
->where('drw.DRS_DEPARTMENT', $department)
->where('drw.DRS_STATUS_DIC_ID', 3) // w eksploatacji
->select('drw.DRS_ID as drs_id', 'w.WLL_NAME as name', 'w.WLL_LATITUDE as lat', 'w.WLL_LONGITUDE as lon')
->get();
// Parsowanie współrzędnych (przecinek lub kropka dziesiętna), odrzucenie błędnych.
$num = function ($v) {
$s = str_replace([',', ' '], ['.', ''], (string) $v);
return is_numeric($s) ? (float) $s : null;
};
$wells = $rows->map(function ($r) use ($num) {
$r->lat = $num($r->lat);
$r->lon = $num($r->lon);
return $r;
})->filter(fn ($r) => $r->lat !== null && $r->lon !== null && $r->lat > 0 && $r->lon > 0)->values();
// Stan ON/OFF dla naniesionych studni (DRD_RESOURCES_DETAIL=507).
$onoff = $this->onOff($wells->pluck('drs_id')->all());
// Normalizacja do obszaru mapy. Zakres liczony z PERCENTYLI (2-98%), żeby
// pojedyncze studnie ze skrajnymi współrzędnymi nie rozciągały skali i nie
// zwijały reszty w jeden punkt. Pozycje przycięte (clamp) do obszaru.
$minLat = $this->percentile($wells->pluck('lat')->all(), 2);
$maxLat = $this->percentile($wells->pluck('lat')->all(), 98);
$minLon = $this->percentile($wells->pluck('lon')->all(), 2);
$maxLon = $this->percentile($wells->pluck('lon')->all(), 98);
$latRange = max(1e-6, $maxLat - $minLat);
$lonRange = max(1e-6, $maxLon - $minLon);
$span = 100 - 2 * self::PAD;
$clamp = fn ($v) => max(self::PAD, min(100 - self::PAD, $v));
$points = $wells->map(function ($r) use ($minLat, $maxLat, $minLon, $lonRange, $latRange, $span, $clamp, $onoff) {
return (object) [
'drs_id' => $r->drs_id,
'name' => $r->name,
// X = długość (wschód), Y = szerokość (północ, odwrócona - północ u góry).
'left' => round($clamp(self::PAD + (($r->lon - $minLon) / $lonRange) * $span), 3),
'top' => round($clamp(self::PAD + (($maxLat - $r->lat) / $latRange) * $span), 3),
'on' => ($onoff[$r->drs_id] ?? null) === 'ON',
];
});
return view('dashboard', [
'points' => $points,
'onCount' => $points->where('on', true)->count(),
'offCount' => $points->where('on', false)->count(),
]);
}
/** Percentyl p (0-100) z tablicy liczb (interpolacja liniowa). */
private function percentile(array $values, float $p): float
{
if (empty($values)) {
return 0.0;
}
sort($values);
$i = ($p / 100) * (count($values) - 1);
$lo = (int) floor($i);
$hi = (int) ceil($i);
return $lo === $hi ? (float) $values[$lo] : $values[$lo] + ($values[$hi] - $values[$lo]) * ($i - $lo);
}
/** Ostatni stan ON/OFF (507) dla zbioru DRS_ID - jednym zapytaniem. */
private function onOff(array $ids): \Illuminate\Support\Collection
{
if (empty($ids)) {
return collect();
}
return 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');
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Concerns\SortsLists;
use App\Http\Requests\StoreEngineRequest;
use App\Http\Requests\UpdateEngineRequest;
use App\Models\Engine;
use App\Models\Manufacturer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Katalog silników - pełny CRUD (legacy site204 / site204A / site204E / site204inf).
* Usuwanie logiczne (ENG_STATUS='0'). Newest methods: Eloquent + Form Requests.
*/
class EngineController extends Controller
{
use SortsLists;
private const MCF_ALL = 100;
/** Sortowalne kolumny listy: klucz nagłówka => kolumna SQL. */
private const SORTABLE = [
'name' => 'ENG_NAME',
'voltage' => 'ENG_VOLTAGE',
'power' => 'ENG_POWER',
'mcf' => 'm.MCF_NAME',
];
public function index(Request $request)
{
$by = $request->query('by', 'name'); // 'name' | 'power'
$q = trim((string) $request->query('q', ''));
$mcf = (int) $request->query('mcf', self::MCF_ALL);
[$sort, $dir] = $this->sortParams($request);
$engines = Engine::query()
->visible()
->with('manufacturer')
->when($q !== '' && $by === 'name', fn ($w) => $w->where('ENG_NAME', 'like', '%'.$q.'%'))
->when($q !== '' && $by === 'power', function ($w) use ($q) {
$v = (float) str_replace(',', '.', $q);
$w->whereBetween('ENG_POWER', [$v - $v * 0.1, $v + $v * 0.1]);
})
->when($mcf !== self::MCF_ALL, fn ($w) => $w->where('ENG_MCF_ID', $mcf))
// Sortowanie po producencie wymaga joinu (relacja nie jest w FROM).
->when($sort === 'mcf', fn ($w) => $w
->leftJoin('seg_manufacturers as m', 'm.MCF_ID', '=', 'seg_engines.ENG_MCF_ID')
->select('seg_engines.*'))
->when($sort !== null, fn ($w) => $w->orderBy(self::SORTABLE[$sort], $dir))
->orderBy('ENG_NAME')
->paginate(20)
->withQueryString();
return view('catalog.engines.index', [
'engines' => $engines,
'manufacturers' => $this->manufacturers(),
'by' => $by,
'q' => $q,
'mcf' => $mcf,
'mcfAll' => self::MCF_ALL,
]);
}
public function create()
{
return view('catalog.engines.create', ['manufacturers' => $this->manufacturers()]);
}
public function store(StoreEngineRequest $request)
{
$engine = Engine::create($this->toColumns($request->validated()) + ['ENG_STATUS' => '1']);
return redirect()->route('engines.show', $engine)
->with('status', 'Dodano silnik „'.$engine->ENG_NAME.'".');
}
public function show(Engine $engine)
{
$engine->load('manufacturer');
$relatedPumps = DB::connection('softspm')->table('seg_pump_engine_rel as r')
->leftJoin('seg_pumps as p', 'p.PUP_ID', '=', 'r.RPE_PUP_ID')
->leftJoin('seg_manufacturers as m', 'm.MCF_ID', '=', 'p.PUP_MCF_ID')
->where('r.RPE_ENG_ID', $engine->ENG_ID)
->select('p.PUP_NAME', DB::raw('p.PUP_Q_N as q_n'), 'p.PUP_H_P', 'm.MCF_NAME')
->get();
return view('catalog.engines.show', ['engine' => $engine, 'relatedPumps' => $relatedPumps]);
}
public function edit(Engine $engine)
{
return view('catalog.engines.edit', [
'engine' => $engine,
'manufacturers' => $this->manufacturers(),
]);
}
public function update(UpdateEngineRequest $request, Engine $engine)
{
$engine->update($this->toColumns($request->validated()));
return redirect()->route('engines.show', $engine)
->with('status', 'Zaktualizowano silnik „'.$engine->ENG_NAME.'".');
}
public function destroy(Engine $engine)
{
$engine->update(['ENG_STATUS' => '0']); // usuwanie logiczne
return redirect()->route('engines.index')
->with('status', 'Usunięto silnik „'.$engine->ENG_NAME.'".');
}
/** Mapowanie pól formularza → kolumny seg_engines. */
private function toColumns(array $data): array
{
return [
'ENG_NAME' => $data['name'],
'ENG_MCF_ID' => $data['manufacturer_id'],
'ENG_POWER' => $data['power'],
'ENG_VOLTAGE' => $data['voltage'],
'ENG_ELECTRICITY' => $data['current'] ?? null,
'ENG_ETA' => $data['eta'] ?? null,
'ENG_COS_FI' => $data['cos_fi'] ?? null,
'ENG_RPM' => $data['rpm'] ?? null,
'ENG_LENGTH' => $data['length'] ?? null,
'ENG_WEIGHT' => $data['weight'] ?? null,
'ENG_DIAMETER' => $data['diameter'] ?? null,
'ENG_WIRES' => $data['wires'] ?? null,
];
}
private function manufacturers()
{
return Manufacturer::orderBy('MCF_NAME')->pluck('MCF_NAME', 'MCF_ID');
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers;
use App\Models\EquipmentFile;
use Symfony\Component\HttpFoundation\Response;
/**
* Strumieniowanie załączników wyposażenia (BLOB z seg_equipments_files) -
* wspólne dla wszystkich katalogów na seg_equipments (kable/orurowanie/
* opomiarowanie). Podgląd/pobranie pliku po EQF_ID.
*/
class EquipmentFileController extends Controller
{
public function file(EquipmentFile $file): Response
{
return response($file->EQF_FILE ?? '', 200, [
'Content-Type' => $file->EQF_FILE_TYPE ?: 'application/octet-stream',
'Content-Disposition' => 'inline; filename="'.($file->EQF_FILE_NAME ?: 'plik').'"',
]);
}
}
+187
View File
@@ -0,0 +1,187 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Concerns\SortsLists;
use App\Http\Requests\StoreMeteringRequest;
use App\Http\Requests\UpdateMeteringRequest;
use App\Models\Equipment;
use App\Models\Manufacturer;
use App\Services\EquipmentFileService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Katalog opomiarowania - pełny CRUD (legacy site208 / A / E / inf).
* seg_equipments (EQU_TYPE_ID=14). Pola jak w kablach (nazwa/producent/opis/
* pliki); przy dodawaniu kategoria EQU_PARAM_02 ustawiana automatycznie na
* słownik WELL_CATEGORY/SPM_FULL (jak legacy normalny formularz site208A).
* Usuwanie logiczne; blokada gdy urządzenie jest użyte jako zasób
* (seg_dev_resources DRS_DEV_TYPE=14) - inaczej niż kable (tam stan magazynowy).
*
* Wariant admina (site208Admin: ręczny wybór kategorii + język) NIE portowany -
* dotyczył wyłącznie usera 'admin'; obecny model nie ma kontroli admin per-ekran.
*/
class MeteringController extends Controller
{
use SortsLists;
/** EQU_TYPE_ID opomiarowania (sys_dictionaries). */
private const TYPE_METERING = 14;
private const MCF_ALL = 100;
/** Sortowalne kolumny listy. */
private const SORTABLE = [
'name' => 'EQU_NAME',
'mcf' => 'm.MCF_NAME',
];
public function __construct(private readonly EquipmentFileService $files) {}
public function index(Request $request)
{
$q = trim((string) $request->query('q', ''));
$mcf = (int) $request->query('mcf', self::MCF_ALL);
[$sort, $dir] = $this->sortParams($request);
$items = Equipment::query()
->visible()->ofType(self::TYPE_METERING)->with('manufacturer')
->when($q !== '', fn ($w) => $w->where('EQU_NAME', 'like', '%'.$q.'%'))
->when($mcf !== self::MCF_ALL, fn ($w) => $w->where('EQU_MCF_ID', $mcf))
->when($sort === 'mcf', fn ($w) => $w
->leftJoin('seg_manufacturers as m', 'm.MCF_ID', '=', 'seg_equipments.EQU_MCF_ID')
->select('seg_equipments.*'))
->when($sort !== null, fn ($w) => $w->orderBy(self::SORTABLE[$sort], $dir))
->orderBy('EQU_NAME')
->paginate(20)
->withQueryString();
return view('catalog.metering.index', [
'items' => $items,
'manufacturers' => $this->manufacturers(),
'q' => $q,
'mcf' => $mcf,
'mcfAll' => self::MCF_ALL,
]);
}
public function create()
{
return view('catalog.metering.create', ['manufacturers' => $this->manufacturers()]);
}
public function store(StoreMeteringRequest $request)
{
$data = $request->validated();
$item = Equipment::create([
'EQU_NAME' => $data['name'],
'EQU_MCF_ID' => $data['manufacturer_id'],
'EQU_DESCRIPTION' => $data['description'] ?? null,
'EQU_PARAM_02' => $this->defaultCategoryId(), // kategoria SPM_FULL (jak legacy)
'EQU_TYPE_ID' => self::TYPE_METERING,
'EQU_STATUS' => '1',
]);
$this->saveFiles($request, $item->EQU_ID);
return redirect()->route('metering.show', $item)
->with('status', 'Dodano urządzenie „'.$item->EQU_NAME.'".');
}
public function show(Equipment $metering)
{
abort_unless((int) $metering->EQU_TYPE_ID === self::TYPE_METERING, 404);
$metering->load('manufacturer', 'files');
return view('catalog.metering.show', [
'item' => $metering,
'document' => $metering->files->first(fn ($f) => ! $f->isImage()),
'image' => $metering->files->first(fn ($f) => $f->isImage()),
'inUse' => $this->inUse($metering->EQU_ID),
]);
}
public function edit(Equipment $metering)
{
abort_unless((int) $metering->EQU_TYPE_ID === self::TYPE_METERING, 404);
$metering->load('manufacturer', 'files');
return view('catalog.metering.edit', [
'item' => $metering,
'manufacturers' => $this->manufacturers(),
'document' => $metering->files->first(fn ($f) => ! $f->isImage()),
'image' => $metering->files->first(fn ($f) => $f->isImage()),
]);
}
public function update(UpdateMeteringRequest $request, Equipment $metering)
{
abort_unless((int) $metering->EQU_TYPE_ID === self::TYPE_METERING, 404);
$data = $request->validated();
// Legacy update aktualizuje tylko nazwę/producenta/opis (kategoria zostaje).
$metering->update([
'EQU_NAME' => $data['name'],
'EQU_MCF_ID' => $data['manufacturer_id'],
'EQU_DESCRIPTION' => $data['description'] ?? null,
]);
$this->saveFiles($request, $metering->EQU_ID);
return redirect()->route('metering.show', $metering)
->with('status', 'Zaktualizowano urządzenie „'.$metering->EQU_NAME.'".');
}
public function destroy(Equipment $metering)
{
abort_unless((int) $metering->EQU_TYPE_ID === self::TYPE_METERING, 404);
// Legacy: blokuj, gdy urządzenie jest użyte jako zasób (seg_dev_resources, typ 14).
if ($this->inUse($metering->EQU_ID)) {
return redirect()->route('metering.show', $metering)
->with('error', 'Nie można usunąć - urządzenie jest użyte w stanie opomiarowania.');
}
$metering->update(['EQU_STATUS' => '0']); // usuwanie logiczne
return redirect()->route('metering.index')
->with('status', 'Usunięto urządzenie „'.$metering->EQU_NAME.'".');
}
private function saveFiles(Request $request, int $equId): void
{
$user = $request->user()->USR_USERNAME;
if ($request->hasFile('catalogue_file')) {
$this->files->store($equId, $request->file('catalogue_file'), $user);
}
if ($request->hasFile('image_file')) {
$this->files->store($equId, $request->file('image_file'), $user);
}
}
/** Czy urządzenie jest użyte jako zasób opomiarowania (seg_dev_resources, typ 14). */
private function inUse(int $equId): bool
{
return DB::connection('softspm')->table('seg_dev_resources')
->where('DRS_DEV_TYPE', self::TYPE_METERING)
->where('DRS_DEV_ID', $equId)
->exists();
}
/** DIC_ID słownikowej kategorii WELL_CATEGORY/SPM_FULL (jak hidden w site208A). */
private function defaultCategoryId(): ?int
{
return DB::connection('softspm')->table('sys_dictionaries')
->where('DIC_TYPE', 'WELL_CATEGORY')
->where('DIC_NAME', 'SPM_FULL')
->value('DIC_ID');
}
private function manufacturers()
{
return Manufacturer::orderBy('MCF_NAME')->pluck('MCF_NAME', 'MCF_ID');
}
}
+174
View File
@@ -0,0 +1,174 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Concerns\SortsLists;
use App\Http\Requests\StorePipingRequest;
use App\Http\Requests\UpdatePipingRequest;
use App\Models\Equipment;
use App\Models\Manufacturer;
use App\Services\EquipmentFileService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Katalog orurowania i armatury - pełny CRUD (legacy site207 / A / E / inf).
* seg_equipments (EQU_TYPE_ID=15) + dwa parametry: EQU_PARAM_01=średnica [mm],
* EQU_PARAM_02=długość [m] / ciśnienie [bar]. Załączniki BLOB jak w kablach.
* Usuwanie logiczne; blokada gdy na stanie (seg_equipments_stock). Pliki przez
* wspólny endpoint equipment.file.
*/
class PipingController extends Controller
{
use SortsLists;
/** EQU_TYPE_ID orurowania/armatury (sys_dictionaries). */
private const TYPE_PIPING = 15;
private const MCF_ALL = 100;
/** Sortowalne kolumny listy. */
private const SORTABLE = [
'name' => 'EQU_NAME',
'diameter' => 'EQU_PARAM_01',
'length' => 'EQU_PARAM_02',
'mcf' => 'm.MCF_NAME',
];
public function __construct(private readonly EquipmentFileService $files) {}
public function index(Request $request)
{
$q = trim((string) $request->query('q', ''));
$mcf = (int) $request->query('mcf', self::MCF_ALL);
[$sort, $dir] = $this->sortParams($request);
$items = Equipment::query()
->visible()->ofType(self::TYPE_PIPING)->with('manufacturer')
->when($q !== '', fn ($w) => $w->where('EQU_NAME', 'like', '%'.$q.'%'))
->when($mcf !== self::MCF_ALL, fn ($w) => $w->where('EQU_MCF_ID', $mcf))
->when($sort === 'mcf', fn ($w) => $w
->leftJoin('seg_manufacturers as m', 'm.MCF_ID', '=', 'seg_equipments.EQU_MCF_ID')
->select('seg_equipments.*'))
->when($sort !== null, fn ($w) => $w->orderBy(self::SORTABLE[$sort], $dir))
->orderBy('EQU_NAME')->orderBy('EQU_PARAM_01')
->paginate(20)
->withQueryString();
return view('catalog.piping.index', [
'items' => $items,
'manufacturers' => $this->manufacturers(),
'q' => $q,
'mcf' => $mcf,
'mcfAll' => self::MCF_ALL,
]);
}
public function create()
{
return view('catalog.piping.create', ['manufacturers' => $this->manufacturers()]);
}
public function store(StorePipingRequest $request)
{
$data = $request->validated();
$item = Equipment::create([
'EQU_NAME' => $data['name'],
'EQU_MCF_ID' => $data['manufacturer_id'],
'EQU_PARAM_01' => $data['diameter'],
'EQU_PARAM_02' => $data['length'] ?? null,
'EQU_DESCRIPTION' => $data['description'] ?? null,
'EQU_TYPE_ID' => self::TYPE_PIPING,
'EQU_STATUS' => '1',
]);
$this->saveFiles($request, $item->EQU_ID);
return redirect()->route('piping.show', $item)
->with('status', 'Dodano pozycję „'.$item->EQU_NAME.'".');
}
public function show(Equipment $piping)
{
abort_unless((int) $piping->EQU_TYPE_ID === self::TYPE_PIPING, 404);
$piping->load('manufacturer', 'files');
return view('catalog.piping.show', [
'item' => $piping,
'document' => $piping->files->first(fn ($f) => ! $f->isImage()),
'image' => $piping->files->first(fn ($f) => $f->isImage()),
'inStock' => $this->inStock($piping->EQU_ID),
]);
}
public function edit(Equipment $piping)
{
abort_unless((int) $piping->EQU_TYPE_ID === self::TYPE_PIPING, 404);
$piping->load('manufacturer', 'files');
return view('catalog.piping.edit', [
'item' => $piping,
'manufacturers' => $this->manufacturers(),
'document' => $piping->files->first(fn ($f) => ! $f->isImage()),
'image' => $piping->files->first(fn ($f) => $f->isImage()),
]);
}
public function update(UpdatePipingRequest $request, Equipment $piping)
{
abort_unless((int) $piping->EQU_TYPE_ID === self::TYPE_PIPING, 404);
$data = $request->validated();
$piping->update([
'EQU_NAME' => $data['name'],
'EQU_MCF_ID' => $data['manufacturer_id'],
'EQU_PARAM_01' => $data['diameter'],
'EQU_PARAM_02' => $data['length'] ?? null,
'EQU_DESCRIPTION' => $data['description'] ?? null,
]);
$this->saveFiles($request, $piping->EQU_ID);
return redirect()->route('piping.show', $piping)
->with('status', 'Zaktualizowano pozycję „'.$piping->EQU_NAME.'".');
}
public function destroy(Equipment $piping)
{
abort_unless((int) $piping->EQU_TYPE_ID === self::TYPE_PIPING, 404);
if ($this->inStock($piping->EQU_ID)) {
return redirect()->route('piping.show', $piping)
->with('error', 'Nie można usunąć - pozycja jest powiązana ze stanem magazynowym.');
}
$piping->update(['EQU_STATUS' => '0']); // usuwanie logiczne
return redirect()->route('piping.index')
->with('status', 'Usunięto pozycję „'.$piping->EQU_NAME.'".');
}
private function saveFiles(Request $request, int $equId): void
{
$user = $request->user()->USR_USERNAME;
if ($request->hasFile('catalogue_file')) {
$this->files->store($equId, $request->file('catalogue_file'), $user);
}
if ($request->hasFile('image_file')) {
$this->files->store($equId, $request->file('image_file'), $user);
}
}
private function inStock(int $equId): bool
{
return DB::connection('softspm')->table('seg_equipments_stock')
->where('SES_EQU_ID', $equId)->exists();
}
private function manufacturers()
{
return Manufacturer::orderBy('MCF_NAME')->pluck('MCF_NAME', 'MCF_ID');
}
}
+165
View File
@@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Concerns\SortsLists;
use App\Http\Requests\StorePumpRequest;
use App\Http\Requests\UpdatePumpRequest;
use App\Models\Manufacturer;
use App\Models\Pump;
use App\Services\PumpService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Katalog pomp - pełny CRUD (legacy site203 / site203A / site203E / site203inf).
* Newest methods: Eloquent, resource controller, Form Requests, serwis domenowy.
*/
class PumpController extends Controller
{
use SortsLists;
/** Producent „Wszyscy" (jak legacy: 100). */
private const MCF_ALL = 100;
/** Sortowalne kolumny listy: klucz nagłówka => kolumna SQL. */
private const SORTABLE = [
'name' => 'PUP_NAME',
'flow' => 'PUP_Q_N',
'hp' => 'PUP_H_P',
'mcf' => 'm.MCF_NAME',
];
public function __construct(private readonly PumpService $pumps) {}
public function index(Request $request)
{
$by = $request->query('by', 'name'); // 'name' | 'flow'
$q = trim((string) $request->query('q', ''));
$mcf = (int) $request->query('mcf', self::MCF_ALL);
[$sort, $dir] = $this->sortParams($request);
$pumps = Pump::query()
->visible()
->with('manufacturer')
->where('PUP_Q_N', '>', 0)
->when($q !== '' && $by === 'name', fn ($w) => $w->where('PUP_NAME', 'like', '%'.$q.'%'))
->when($q !== '' && $by === 'flow', function ($w) use ($q) {
$v = (float) str_replace(',', '.', $q);
$w->whereBetween('PUP_Q_N', [$v - $v * 0.1, $v + $v * 0.1]); // skala UAT bez ×60
})
->when($mcf !== self::MCF_ALL, fn ($w) => $w->where('PUP_MCF_ID', $mcf))
// Sortowanie po producencie wymaga joinu (relacja nie jest w FROM).
->when($sort === 'mcf', fn ($w) => $w
->leftJoin('seg_manufacturers as m', 'm.MCF_ID', '=', 'seg_pumps.PUP_MCF_ID')
->select('seg_pumps.*'))
->when($sort !== null, fn ($w) => $w->orderBy(self::SORTABLE[$sort], $dir))
->orderBy('PUP_Q_N')
->orderBy('PUP_NAME')
->paginate(20)
->withQueryString();
return view('catalog.pumps.index', [
'pumps' => $pumps,
'manufacturers' => $this->manufacturers(),
'by' => $by,
'q' => $q,
'mcf' => $mcf,
'mcfAll' => self::MCF_ALL,
]);
}
public function create()
{
return view('catalog.pumps.create', ['manufacturers' => $this->manufacturers()]);
}
public function store(StorePumpRequest $request)
{
$pump = $this->pumps->create($request->validated());
return redirect()
->route('pumps.show', $pump)
->with('status', 'Dodano pompę „'.$pump->PUP_NAME.'".');
}
public function show(Request $request, Pump $pump)
{
$pump->load('manufacturer');
$h = $this->pumps->curve($pump, 'H'); // x = m³/h (bez /60)
$eta = $this->pumps->curve($pump, 'ETA');
// Warunki rysowania jak legacy: H bez zer, η dopuszcza 1 zero (punkt Q=0).
$hDrawable = $this->pumps->curveDrawable($h, 0);
$etaDrawable = $this->pumps->curveDrawable($eta, 1);
// Przelicz dla zadanego Q [m³/h]. Domyślnie Q_n; clamp do zakresu.
$maxX = (float) ($h->max('x') ?? 0);
$qn = round((float) $pump->PUP_Q_N, 3);
$qIn = $request->query('q');
$q = $qIn === null ? $qn : (float) str_replace(',', '.', (string) $qIn);
$outOfRange = $q < 0 || $q > $maxX;
$q = max(0, min($q, $maxX));
$relatedEngines = DB::connection('softspm')->table('seg_pump_engine_rel as r')
->leftJoin('seg_engines as e', 'e.ENG_ID', '=', 'r.RPE_ENG_ID')
->leftJoin('seg_manufacturers as m', 'm.MCF_ID', '=', 'e.ENG_MCF_ID')
->where('r.RPE_PUP_ID', $pump->PUP_ID)
->select('e.ENG_NAME', 'e.ENG_VOLTAGE', 'e.ENG_POWER', 'm.MCF_NAME')
->get();
return view('catalog.pumps.show', [
'pump' => $pump,
'h' => $h,
'eta' => $eta,
'hDrawable' => $hDrawable,
'etaDrawable' => $etaDrawable,
'hPlot' => $hDrawable ? $this->pumps->plot($h) : null,
'etaPlot' => $etaDrawable ? $this->pumps->plot($eta) : null,
'relatedEngines' => $relatedEngines,
'q' => $q,
'hAtQ' => $hDrawable ? $this->pumps->valueAt($h, $q) : null,
'etaAtQ' => $etaDrawable ? $this->pumps->valueAt($eta, $q) : null,
'outOfRange' => $outOfRange,
]);
}
public function edit(Pump $pump)
{
$pump->load('manufacturer');
return view('catalog.pumps.edit', [
'pump' => $pump,
'manufacturers' => $this->manufacturers(),
'h' => $this->pumps->curve($pump, 'H'),
'eta' => $this->pumps->curve($pump, 'ETA'),
'qn' => round((float) $pump->PUP_Q_N, 3),
]);
}
public function update(UpdatePumpRequest $request, Pump $pump)
{
$this->pumps->update($pump, $request->validated());
return redirect()
->route('pumps.show', $pump)
->with('status', 'Zaktualizowano pompę „'.$pump->PUP_NAME.'".');
}
public function destroy(Pump $pump)
{
$this->pumps->delete($pump);
return redirect()
->route('pumps.index')
->with('status', 'Usunięto pompę „'.$pump->PUP_NAME.'".');
}
/** MCF_ID => MCF_NAME do selectów. */
private function manufacturers()
{
return Manufacturer::orderBy('MCF_NAME')->pluck('MCF_NAME', 'MCF_ID');
}
}
+250
View File
@@ -0,0 +1,250 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Concerns\SortsLists;
use App\Services\WellService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
/**
* Studnie - migracja sites/site401.php (część read-only: lista + wyszukiwanie
* + paginacja). Database-first na bazie `softspm`.
*
* Wydajność: legacy robił N+1 (osobne podzapytania o pompę, silnik i stan
* ON/OFF dla KAŻDEGO wiersza). Tu wszystko w JEDNYM zapytaniu z JOIN-ami
* (+ podzapytanie po ostatni ON/OFF). Parytet wyników do weryfikacji na
* serwerze testowym - patrz README_SOFTSPM.md.
*/
class WellController extends Controller
{
use SortsLists;
/** Status "Wszystkie" - wartość spoza słownika (jak legacy: 100). */
private const STATUS_ALL = 100;
/** Sortowalne kolumny listy: klucz nagłówka => kolumna SQL (z aliasów 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 index(Request $request)
{
$department = $request->session()->get('department');
$search = trim((string) $request->query('q', ''));
// Domyślny status jak w legacy: 3.
$status = (int) $request->query('status', 3);
[$sort, $dir] = $this->sortParams($request);
// Słownik statusów do filtra (legacy: DIC_ID in (3,4) + "Wszystkie").
$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();
// Stan ON/OFF dociągany tylko dla 20 widocznych studni (wydajność).
$this->attachOnOff($wells);
return view('wells.index', [
'wells' => $wells,
'search' => $search,
'status' => $status,
'statusOptions' => $statusOptions,
'statusAll' => self::STATUS_ALL,
'department' => $department,
]);
}
/** Formularz dodania studni (migracja site301A). */
public function create(WellService $wells)
{
return view('wells.create', $this->formLookups($wells));
}
/** Zapis nowej studni (migracja site301inf, dodawanie). */
public function store(Request $request, WellService $wells)
{
$data = $this->validateWell($request, $wells, isUpdate: false);
$drsId = $wells->create(
$data,
$request->session()->get('department'),
$request->user()->USR_USERNAME,
);
return redirect()
->route('wells.index')
->with('status', 'Dodano studnię „'.$data['name'].'".');
}
/** Formularz edycji studni (migracja site301E). */
public function edit(int $drs, WellService $wells)
{
$well = $wells->findForEdit($drs);
abort_if(! $well, 404);
return view('wells.edit', $this->formLookups($wells) + ['well' => $well]);
}
/** Aktualizacja studni (migracja site301inf, subWellUpdate). */
public function update(int $drs, Request $request, WellService $wells)
{
$data = $this->validateWell($request, $wells, isUpdate: true);
$wells->update($drs, $data, $request->user()->USR_USERNAME);
return redirect()
->route('wells.index')
->with('status', 'Zaktualizowano studnię „'.$data['name'].'".');
}
/** Wspólne słowniki do formularzy dodania/edycji. */
private function formLookups(WellService $wells): array
{
return [
'waterIntakes' => $wells->waterIntakes(),
'categories' => $wells->dictionaryById('WELL_CATEGORY'),
'statuses' => $wells->dictionaryById('WELL_STATUS_LEVEL_01'),
'sysTypes' => $wells->dictionaryDescriptions('WELL_SYSTEM_TYPE'),
'statusDescs' => $wells->dictionaryDescriptions('WELL_STATUS_LEVEL_03'),
];
}
/** Walidacja + normalizacja pól formularza studni. */
private function validateWell(Request $request, WellService $wells, bool $isUpdate): array
{
$rules = [
'name' => ['required', 'string', 'max:255'],
'water_intake_id' => ['required', 'integer', 'min:1'],
'category_id' => ['required', 'integer'],
'status_id' => ['required', 'integer'],
'latitude' => ['nullable', 'string', 'max:128'],
'longitude' => ['nullable', 'string', 'max:128'],
];
if ($isUpdate) {
$rules += [
'well_id' => ['required', 'integer'],
'pp_scope' => ['nullable', 'string', 'max:16'],
'hh_scope' => ['nullable', 'string', 'max:16'],
'sys_type' => ['nullable', 'string', 'max:128'],
'status_desc' => ['nullable', 'string', 'max:255'],
'available_date' => ['nullable', 'date'],
'build_time' => ['nullable', 'string', 'max:8'],
'wle_id' => ['nullable', 'integer'],
];
} else {
$rules += [
'diameter' => ['nullable', 'string', 'max:32'],
'depth' => ['nullable', 'string', 'max:32'],
'supply' => ['nullable', 'string', 'max:32'],
'available_date' => ['nullable', 'date'],
];
}
$data = $request->validate($rules);
// Unikalność nazwy tylko przy dodawaniu (legacy sprawdzał przed INSERT).
if (! $isUpdate && $wells->nameExists($data['name'])) {
throw ValidationException::withMessages(['name' => 'Studnia o tej nazwie już istnieje.']);
}
return $data;
}
/**
* Jedno zapytanie zastępujące listę + N+1 z legacy.
*
* Mapowanie tabel legacy:
* seg_wells (w) - studnia
* seg_dev_resources (drw,12) - zasób studni: DRS_ID, dział, status
* seg_well_pump_eng_rel (rel) - powiązanie studnia↔pompa/silnik
* seg_dev_resources (drp,10) - zasób pompy (nr seryjny)
* seg_pumps (p) - pompa (nazwa)
* seg_dev_resources (dre,11) - zasób silnika (nr seryjny)
* seg_engines (e) - silnik (nazwa)
* seg_dev_resources_det (507) - ostatni stan ON/OFF
*/
private function buildQuery(?string $department, string $search, int $status)
{
// UWAGA WYDAJNOŚĆ: NIE łączymy tu ostatniego ON/OFF z seg_dev_resources_det
// (tabela ma miliardy wierszy) - agregacja po całości była wąskim gardłem
// paginacji. Stan ON/OFF dociągamy osobno tylko dla bieżącej strony
// w attachOnOff().
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')
// Pompa
->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')
// Silnik
->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',
]);
}
/**
* Dociąga ostatni stan ON/OFF (DRD_RESOURCES_DETAIL=507) TYLKO dla studni
* z bieżącej strony - po ich DRS_ID (indeks INX_DRD_DRS_DETAIL_DATE),
* zamiast agregować całą tabelę seg_dev_resources_det. Każdy wiersz
* dostaje właściwość ->onoff.
*/
private function attachOnOff(\Illuminate\Contracts\Pagination\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;
}
}
}
@@ -0,0 +1,544 @@
<?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;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Ustawia język aplikacji na podstawie cookie 'locale' (PL/EN/DE) - wybór
* z menu użytkownika. Tłumaczenia stringów to osobny krok (i18n legacy
* language/*.php -> lang/); tu wyłącznie ustawienie locale + atrybut lang.
*/
class SetLocale
{
private const SUPPORTED = ['pl', 'en', 'de'];
public function handle(Request $request, Closure $next): Response
{
$locale = $request->cookie('locale');
if (is_string($locale) && in_array($locale, self::SUPPORTED, true)) {
app()->setLocale($locale);
}
return $next($request);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
/**
* Walidacja dodania/edycji typu kabla (site206A/E). Pliki opcjonalne:
* dokumentacja katalogowa (PDF/Office) oraz zdjęcie. Limit 8 MB (mediumblob 16 MB).
*/
class StoreCableRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'description' => ['nullable', 'string', 'max:255'],
'catalogue_file' => ['nullable', 'file', 'mimes:pdf,doc,docx,xls,xlsx,ppt,pptx', 'max:8192'],
'image_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,gif,bmp', 'max:8192'],
];
}
public function attributes(): array
{
return [
'name' => 'typ kabla',
'manufacturer_id' => 'producent',
'description' => 'opis',
'catalogue_file' => 'dokumentacja katalogowa',
'image_file' => 'zdjęcie',
];
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreEngineRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
foreach (['power', 'voltage', 'current', 'eta', 'cos_fi', 'rpm', 'length', 'weight', 'diameter'] as $f) {
$this->merge([$f => $this->dec($this->$f)]);
}
}
public function rules(): array
{
return [
// Unikalność: nazwa + napięcie (jak legacy).
'name' => ['required', 'string', 'max:128', Rule::unique('seg_engines', 'ENG_NAME')->where(fn ($q) => $q->where('ENG_VOLTAGE', $this->voltage))],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'power' => ['required', 'numeric'],
'voltage' => ['required', 'numeric'],
'current' => ['nullable', 'numeric'],
'eta' => ['nullable', 'numeric'],
'cos_fi' => ['nullable', 'numeric'],
'rpm' => ['nullable', 'numeric'],
'length' => ['nullable', 'numeric'],
'weight' => ['nullable', 'numeric'],
'diameter' => ['nullable', 'numeric'],
'wires' => ['nullable', 'string', 'max:32'],
];
}
private function dec(mixed $v): ?string
{
return $v === null || $v === '' ? null : str_replace(',', '.', (string) $v);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
/**
* Walidacja dodania/edycji urządzenia opomiarowania (site208A/E). Pola jak
* w kablach (nazwa, producent, opis, pliki). Kategoria (EQU_PARAM_02=SPM_FULL)
* ustawiana automatycznie w serwisie/kontrolerze - jak legacy normalny dodawania.
*/
class StoreMeteringRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'description' => ['nullable', 'string', 'max:255'],
'catalogue_file' => ['nullable', 'file', 'mimes:pdf,doc,docx,xls,xlsx,ppt,pptx', 'max:8192'],
'image_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,gif,bmp', 'max:8192'],
];
}
public function attributes(): array
{
return [
'name' => 'nazwa',
'manufacturer_id' => 'producent',
'description' => 'opis',
'catalogue_file' => 'dokumentacja katalogowa',
'image_file' => 'zdjęcie',
];
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
/**
* Walidacja dodania/edycji orurowania i armatury (site207A/E). Średnica
* wymagana (legacy float_req), długość/ciśnienie opcjonalne. Pliki jak w kablach.
*/
class StorePipingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'diameter' => ['required', 'string', 'max:128'],
'length' => ['nullable', 'string', 'max:128'],
'description' => ['nullable', 'string', 'max:255'],
'catalogue_file' => ['nullable', 'file', 'mimes:pdf,doc,docx,xls,xlsx,ppt,pptx', 'max:8192'],
'image_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,gif,bmp', 'max:8192'],
];
}
public function attributes(): array
{
return [
'name' => 'nazwa',
'manufacturer_id' => 'producent',
'diameter' => 'średnica',
'length' => 'długość / ciśnienie',
'description' => 'opis',
'catalogue_file' => 'dokumentacja katalogowa',
'image_file' => 'zdjęcie',
];
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePumpRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'q_n' => $this->dec($this->q_n),
'length' => $this->dec($this->length),
'diameter' => $this->dec($this->diameter),
'weight' => $this->dec($this->weight),
'h' => collect($this->h ?? [])->map(fn ($v) => $this->dec($v))->all(),
'eta' => collect($this->eta ?? [])->map(fn ($v) => $this->dec($v))->all(),
]);
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:128', 'unique:seg_pumps,PUP_NAME'],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'q_n' => ['required', 'numeric', 'gt:0'],
// 8 punktów krzywych - H wymagane (>0), η wymagane (0 dozwolone przy Q=0).
'h' => ['required', 'array', 'size:8'],
'h.*' => ['required', 'numeric'],
'eta' => ['required', 'array', 'size:8'],
'eta.*' => ['required', 'numeric'],
'length' => ['nullable', 'numeric'],
'diameter' => ['nullable', 'numeric'],
'weight' => ['nullable', 'numeric'],
];
}
public function messages(): array
{
return [
'h.*.required' => 'Podaj wartość H dla każdego punktu Q.',
'eta.*.required' => 'Podaj wartość η dla każdego punktu Q.',
];
}
private function dec(mixed $v): ?string
{
return $v === null || $v === '' ? null : str_replace(',', '.', (string) $v);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
/**
* Walidacja edycji typu kabla (site206E). Reguły jak przy dodawaniu - wgranie
* nowego pliku zastępuje dotychczasowy tej samej roli; brak pliku = bez zmian.
*/
class UpdateCableRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'description' => ['nullable', 'string', 'max:255'],
'catalogue_file' => ['nullable', 'file', 'mimes:pdf,doc,docx,xls,xlsx,ppt,pptx', 'max:8192'],
'image_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,gif,bmp', 'max:8192'],
];
}
public function attributes(): array
{
return [
'name' => 'typ kabla',
'manufacturer_id' => 'producent',
'description' => 'opis',
'catalogue_file' => 'dokumentacja katalogowa',
'image_file' => 'zdjęcie',
];
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateEngineRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
foreach (['power', 'voltage', 'current', 'eta', 'cos_fi', 'rpm', 'length', 'weight', 'diameter'] as $f) {
$this->merge([$f => $this->dec($this->$f)]);
}
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:128', Rule::unique('seg_engines', 'ENG_NAME')->where(fn ($q) => $q->where('ENG_VOLTAGE', $this->voltage))->ignore($this->route('engine'))],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'power' => ['required', 'numeric'],
'voltage' => ['required', 'numeric'],
'current' => ['nullable', 'numeric'],
'eta' => ['nullable', 'numeric'],
'cos_fi' => ['nullable', 'numeric'],
'rpm' => ['nullable', 'numeric'],
'length' => ['nullable', 'numeric'],
'weight' => ['nullable', 'numeric'],
'diameter' => ['nullable', 'numeric'],
'wires' => ['nullable', 'string', 'max:32'],
];
}
private function dec(mixed $v): ?string
{
return $v === null || $v === '' ? null : str_replace(',', '.', (string) $v);
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
/**
* Walidacja edycji urządzenia opomiarowania (site208E). Reguły jak przy
* dodawaniu; wgranie nowego pliku zastępuje dotychczasowy tej samej roli.
*/
class UpdateMeteringRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'description' => ['nullable', 'string', 'max:255'],
'catalogue_file' => ['nullable', 'file', 'mimes:pdf,doc,docx,xls,xlsx,ppt,pptx', 'max:8192'],
'image_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,gif,bmp', 'max:8192'],
];
}
public function attributes(): array
{
return [
'name' => 'nazwa',
'manufacturer_id' => 'producent',
'description' => 'opis',
'catalogue_file' => 'dokumentacja katalogowa',
'image_file' => 'zdjęcie',
];
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
/**
* Walidacja edycji orurowania i armatury (site207E). Reguły jak przy dodawaniu;
* wgranie nowego pliku zastępuje dotychczasowy tej samej roli.
*/
class UpdatePipingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'diameter' => ['required', 'string', 'max:128'],
'length' => ['nullable', 'string', 'max:128'],
'description' => ['nullable', 'string', 'max:255'],
'catalogue_file' => ['nullable', 'file', 'mimes:pdf,doc,docx,xls,xlsx,ppt,pptx', 'max:8192'],
'image_file' => ['nullable', 'image', 'mimes:jpg,jpeg,png,gif,bmp', 'max:8192'],
];
}
public function attributes(): array
{
return [
'name' => 'nazwa',
'manufacturer_id' => 'producent',
'diameter' => 'średnica',
'length' => 'długość / ciśnienie',
'description' => 'opis',
'catalogue_file' => 'dokumentacja katalogowa',
'image_file' => 'zdjęcie',
];
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdatePumpRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'length' => $this->dec($this->length),
'diameter' => $this->dec($this->diameter),
'weight' => $this->dec($this->weight),
'h' => collect($this->h ?? [])->map(fn ($v) => $this->dec($v))->all(),
'eta' => collect($this->eta ?? [])->map(fn ($v) => $this->dec($v))->all(),
]);
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:128', Rule::unique('seg_pumps', 'PUP_NAME')->ignore($this->route('pump'))],
'manufacturer_id' => ['required', 'integer', 'exists:seg_manufacturers,MCF_ID'],
'length' => ['nullable', 'numeric'],
'diameter' => ['nullable', 'numeric'],
'weight' => ['nullable', 'numeric'],
'h' => ['nullable', 'array'],
'h.*' => ['nullable', 'numeric'],
'eta' => ['nullable', 'array'],
'eta.*' => ['nullable', 'numeric'],
];
}
private function dec(mixed $v): ?string
{
return $v === null || $v === '' ? null : str_replace(',', '.', (string) $v);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** Silnik - katalog (legacy site204). Tabela seg_engines (database-first). */
class Engine extends Model
{
protected $connection = 'softspm';
protected $table = 'seg_engines';
protected $primaryKey = 'ENG_ID';
public $timestamps = false;
protected $guarded = [];
public function manufacturer(): BelongsTo
{
return $this->belongsTo(Manufacturer::class, 'ENG_MCF_ID', 'MCF_ID');
}
/** Tylko widoczne (ENG_STATUS='1'). */
public function scopeVisible(Builder $q): Builder
{
return $q->where('ENG_STATUS', '1');
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Wyposażenie z katalogów (legacy seg_equipments). Typ rozróżniany przez
* EQU_TYPE_ID (sys_dictionaries): 13=kable (site206); 207/208 dołożą kolejne.
* Database-first na bazie `softspm`.
*/
class Equipment extends Model
{
protected $connection = 'softspm';
protected $table = 'seg_equipments';
protected $primaryKey = 'EQU_ID';
public $timestamps = false;
protected $guarded = [];
public function manufacturer(): BelongsTo
{
return $this->belongsTo(Manufacturer::class, 'EQU_MCF_ID', 'MCF_ID');
}
public function files(): HasMany
{
return $this->hasMany(EquipmentFile::class, 'EQF_EQU_ID', 'EQU_ID');
}
/** Tylko widoczne (EQU_STATUS='1', nieusunięte logicznie). */
public function scopeVisible(Builder $q): Builder
{
return $q->where('EQU_STATUS', '1');
}
/** Filtr po rodzaju wyposażenia (EQU_TYPE_ID). */
public function scopeOfType(Builder $q, int $typeId): Builder
{
return $q->where('EQU_TYPE_ID', $typeId);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* Załącznik wyposażenia (legacy seg_equipments_files) - plik trzymany jako BLOB
* w bazie (EQF_FILE mediumblob). Rola pliku wynika z prefiksu MIME:
* 'image/*' = zdjęcie, pozostałe (np. 'application/pdf') = dokumentacja.
*/
class EquipmentFile extends Model
{
protected $connection = 'softspm';
protected $table = 'seg_equipments_files';
protected $primaryKey = 'EQF_ID';
public $timestamps = false;
protected $guarded = [];
/** Czy to plik graficzny (zdjęcie). */
public function isImage(): bool
{
return str_starts_with(strtolower((string) $this->EQF_FILE_TYPE), 'image/');
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/** Producent - tabela seg_manufacturers. */
class Manufacturer extends Model
{
protected $connection = 'softspm';
protected $table = 'seg_manufacturers';
protected $primaryKey = 'MCF_ID';
public $timestamps = false;
protected $guarded = [];
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Pompa - katalog (legacy site203). Tabela seg_pumps (database-first).
* Uwaga: PUP_Q_N i krzywe PEP_X_PARAM trzymane ×60; w UI dzielimy /60 (m³/min).
*/
class Pump extends Model
{
protected $connection = 'softspm';
protected $table = 'seg_pumps';
protected $primaryKey = 'PUP_ID';
public $timestamps = false;
protected $guarded = [];
public function manufacturer(): BelongsTo
{
return $this->belongsTo(Manufacturer::class, 'PUP_MCF_ID', 'MCF_ID');
}
public function curvePoints(): HasMany
{
return $this->hasMany(PumpCurvePoint::class, 'PEP_PUP_ID', 'PUP_ID');
}
/** Tylko widoczne (PUP_STATUS='1', nieusunięte logicznie). */
public function scopeVisible(Builder $q): Builder
{
return $q->where('PUP_STATUS', '1');
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* Punkt krzywej pompy (Q-H lub Q-ETA) - tabela seg_pump_eng_param.
* PEP_TYPE: 'H' | 'ETA'. PEP_X_PARAM trzymane ×60 (m³/min po /60).
*/
class PumpCurvePoint extends Model
{
protected $connection = 'softspm';
protected $table = 'seg_pump_eng_param';
protected $primaryKey = 'PEP_ID';
public $timestamps = false;
protected $guarded = [];
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
/**
* Model użytkownika - DATABASE-FIRST na istniejącej tabeli `sys_users`
* (baza operacyjna `softspm`). NIE zmieniamy schematu.
*
* Kolumny legacy (z func.php / index.php):
* USR_ID - PK
* USR_USERNAME - login
* USR_PASSWORD - hash hasła (legacy: MD5, docelowo bcrypt - patrz LoginController)
* USR_STATUS - '1' = aktywny
* USR_PRT_ID - uczestnik/system (sys_participants)
*/
class User extends Authenticatable implements AuthenticatableContract
{
use Notifiable;
protected $connection = 'softspm';
protected $table = 'sys_users';
protected $primaryKey = 'USR_ID';
/** Tabela legacy nie ma created_at/updated_at. */
public $timestamps = false;
/** Tabela legacy nie ma kolumny remember_token. */
protected $rememberTokenName = '';
protected $hidden = [
'USR_PASSWORD',
];
protected $guarded = [];
/**
* Laravel Auth: która kolumna trzyma hash hasła.
*/
public function getAuthPassword()
{
return $this->USR_PASSWORD;
}
/**
* Wyłączamy mechanizm "remember me" - brak kolumny w schemacie legacy.
*/
public function getRememberToken()
{
return null;
}
public function setRememberToken($value)
{
// no-op
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
+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));
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace App\Support;
/**
* Regresja wielomianowa - wierny port legacy polynomial/PolynomialRegression.php
* (Andrew Que, GPL). Ten sam algorytm (równania normalne + eliminacja
* wierszowa), arytmetyka natywna (float) zamiast bcmath - wynik identyczny dla
* zakresów danych pomp, bez zależności od ext-bcmath.
*
* Użycie jak w legacy: new PolynomialRegression(4) → 4 współczynniki (wielomian
* stopnia 3); addData(x,y) ×N; getCoefficients(); interpolate(coeffs, x).
*/
class PolynomialRegression
{
/** @var float[] */
private array $xPowers = [];
/** @var float[] */
private array $xyPowers = [];
public function __construct(private int $degree = 3)
{
$this->reset();
}
public function reset(): void
{
$squares = ($this->degree - 1) * 2;
for ($i = 0; $i <= $squares; $i++) {
$this->xPowers[$i] = 0.0;
$this->xyPowers[$i] = 0.0;
}
}
public function addData(float $x, float $y): void
{
$squares = ($this->degree - 1) * 2;
for ($i = 0; $i <= $squares; $i++) {
$this->xPowers[$i] += $x ** $i;
$this->xyPowers[$i] += $y * ($x ** $i);
}
}
/**
* Współczynniki [c0, c1, ...] (c0 = wyraz wolny).
*
* @return float[]
*/
public function getCoefficients(?int $degree = null): array
{
$degree ??= $this->degree;
$eps = 1e-12;
// Macierz równań normalnych + kolumna augmentacji.
$matrix = [];
for ($row = 0; $row < $degree; $row++) {
for ($col = 0; $col < $degree; $col++) {
$matrix[$row][$col] = $this->xPowers[$row + $col];
}
$matrix[$row][$degree] = $this->xyPowers[$row];
}
$rows = count($matrix);
$isDone = array_fill(0, $rows, false);
$order = [];
// Eliminacja wierszowa do macierzy górnotrójkątnej z 1 na przekątnej.
for ($column = 0; $column < $rows; $column++) {
$activeRow = 0;
while ($activeRow < $rows
&& (abs($matrix[$activeRow][$column]) < $eps || $isDone[$activeRow])) {
$activeRow++;
}
if ($activeRow < $rows) {
$order[$column] = $activeRow;
$firstTerm = $matrix[$activeRow][$column];
for ($sub = $column; $sub <= $rows; $sub++) {
$matrix[$activeRow][$sub] /= $firstTerm;
}
$isDone[$activeRow] = true;
for ($row = 0; $row < $rows; $row++) {
if (! $isDone[$row] && abs($matrix[$row][$column]) >= $eps) {
$factor = $matrix[$row][$column];
for ($sub = $column; $sub <= $rows; $sub++) {
$matrix[$row][$sub] -= $factor * $matrix[$activeRow][$sub];
}
}
}
}
}
// Podstawianie wsteczne.
$isDone = array_fill(0, $rows, false);
$coefficients = [];
for ($column = $rows - 1; $column >= 0; $column--) {
$activeRow = $order[$column];
$isDone[$activeRow] = true;
for ($row = 0; $row < $rows; $row++) {
if (! $isDone[$row]) {
$factor = $matrix[$row][$column];
for ($sub = $column; $sub <= $rows; $sub++) {
$matrix[$row][$sub] -= $factor * $matrix[$activeRow][$sub];
}
}
}
$coefficients[$column] = $matrix[$activeRow][$rows];
}
ksort($coefficients);
return $coefficients;
}
/** @param float[] $coefficients */
public function interpolate(array $coefficients, float $x): float
{
$y = 0.0;
foreach ($coefficients as $i => $c) {
$y += $c * ($x ** $i);
}
return $y;
}
}