Initial commit
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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').'"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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/');
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user