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);
}
}