Initial commit
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[compose.yaml]
|
||||
indent_size = 4
|
||||
@@ -0,0 +1,40 @@
|
||||
APP_NAME=SoftSPM
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost:8000
|
||||
APP_LOCALE=pl
|
||||
APP_FALLBACK_LOCALE=pl
|
||||
APP_TIMEZONE=Europe/Warsaw
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Database-first: nie tworzymy tabel pomocniczych w bazach legacy,
|
||||
# dlatego sesje/cache/kolejki lecą na pliki (NIE uruchamiamy migracji).
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
DB_CONNECTION=softspm
|
||||
|
||||
# Baza operacyjna (domyślna)
|
||||
DB_HOST=10.42.1.210
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=softspm
|
||||
DB_USERNAME=przemek
|
||||
DB_PASSWORD=
|
||||
|
||||
# Bazy raportowa i historyczna (ten sam host/user, inna nazwa bazy)
|
||||
DB_REP_DATABASE=softspm_rep
|
||||
DB_HIS_DATABASE=softspm_his
|
||||
|
||||
# Migracja haseł MD5->bcrypt - dopiero po zgodzie klienta.
|
||||
SOFTSPM_REHASH=false
|
||||
|
||||
# Integracja SYNDIS (SOAP) - IP w LAN produkcyjnym.
|
||||
SYNDIS_WSDL=http://172.21.11.101/SyndisSpm.wsdl
|
||||
SYNDIS_LOCATION=http://172.21.11.101:3615/
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
|
||||
# SoftSPM: fonty i obrazy są pobierane/kopiowane przy starcie środowiska
|
||||
# (patrz README_SOFTSPM.md → "Środowisko lokalne (Docker)"). Trzymamy je
|
||||
# w repo świadomie - produkcja jest air-gap i nie pobierze ich sama.
|
||||
!/public/fonts
|
||||
!/public/images
|
||||
@@ -0,0 +1,233 @@
|
||||
# SoftSPM_2 - aplikacja Laravel (nakładka + szkielet)
|
||||
|
||||
Historycznie ten katalog trzymał **tylko pliki specyficzne dla SoftSPM** (modele,
|
||||
kontrolery, widoki, konfiguracja, trasy), a czysty szkielet Laravela stał osobno
|
||||
na UAT jako `softspm_pge`. Od 2026-09-03 katalog jest **pełnym, uruchamialnym
|
||||
projektem Laravel** - szkielet został domieszany lokalnie, żeby dało się pracować
|
||||
i testować na stacji roboczej.
|
||||
|
||||
Lista plików nakładki (to, co nakładamy na szkielet na UAT/produkcji) jest
|
||||
zapisana w [`docker/overlay-files.txt`](docker/overlay-files.txt).
|
||||
|
||||
> Migracja legacy PHP 5.4/5.6 → Laravel.
|
||||
> **Źródło migracji: `C:\Programy\xampp_old\htdocs\trzeciCloude_v1\`** - NIE `newPGE`
|
||||
> ani inne kopie w `htdocs/`. Audyt legacy i mapa `siteNNN` → trasy: [docs/LEGACY.md](docs/LEGACY.md).
|
||||
> Kontekst i decyzje: patrz pamięć
|
||||
> projektu Claude. Charakterystyka: 3 bazy MariaDB, **produkcja bez internetu
|
||||
> (air-gap, LAN)**.
|
||||
|
||||
> **Wersja frameworka: Laravel 12** (nie 11). Laravel 11 jest EOL - Composer
|
||||
> odmawia instalacji każdego wydania 11.31+ z powodu niezałatanych advisory
|
||||
> bezpieczeństwa, a nowszych 11.x już nie ma. **UAT/produkcję trzeba podnieść
|
||||
> do 12.** Kod nakładki nie wymagał żadnych zmian przy tym przejściu.
|
||||
|
||||
> **Tailwind v4**, konfigurowany w CSS (`@theme` w `resources/css/app.css`).
|
||||
> Pliku `tailwind.config.js` **nie ma i nie powinno być** - starsze fragmenty
|
||||
> tego README, które go wymieniają, są nieaktualne.
|
||||
|
||||
### Środowiska
|
||||
|
||||
| Środowisko | OS | Ścieżka | Internet |
|
||||
|---|---|---|---|
|
||||
| **UAT / testowe** | Debian | `/var/www/html/softspm_pge` | jest (build assetów tutaj) |
|
||||
| **Produkcja** | Windows Server | (web root → `public/`) | **BRAK (air-gap, LAN)** |
|
||||
|
||||
> **Debian = case-sensitive!** Nazwy plików/ścieżek (komponenty Blade, fonty,
|
||||
> obrazki) muszą zgadzać się co do wielkości liter. Trzymamy wszystko lowercase.
|
||||
|
||||
## Środowisko lokalne (Docker)
|
||||
|
||||
Stacja robocza ma tylko PHP 5.6 i 7.4 (XAMPP), a Laravel 12 wymaga PHP ≥ 8.2 -
|
||||
dlatego dev stoi w kontenerze. **Baza NIE jest kontenerem**: łączymy się do
|
||||
istniejącej MariaDB `10.42.1.210` (podejście database-first).
|
||||
|
||||
Kontener: `docker/Dockerfile` (PHP 8.3 + Apache + Node 20, rozszerzenia
|
||||
`pdo_mysql mysqli soap zip gd intl opcache`; `soap` jest wymagany przez
|
||||
`SyndisService`). DocumentRoot → `public/`.
|
||||
|
||||
```bash
|
||||
docker compose up -d app # aplikacja na http://localhost:8000
|
||||
docker compose exec app php artisan ... # artisan
|
||||
docker compose exec app composer ... # composer
|
||||
docker compose exec app npm run build # produkcyjny build assetów
|
||||
docker compose --profile dev up vite # opcjonalny hot-reload (port 5173)
|
||||
```
|
||||
|
||||
Pierwsze uruchomienie na czystej maszynie:
|
||||
|
||||
```bash
|
||||
cp .env.example .env # uzupełnić DB_PASSWORD
|
||||
docker compose build app
|
||||
docker compose up -d app
|
||||
docker compose exec app composer install
|
||||
docker compose exec app npm install
|
||||
docker compose exec app npm run build
|
||||
docker compose exec app php artisan key:generate
|
||||
docker compose exec app chmod -R 777 storage bootstrap/cache
|
||||
docker compose exec app php docker/check/db-check.php # smoke-test 3 połączeń
|
||||
```
|
||||
|
||||
`php artisan migrate` **nie jest uruchamiane** - sesje i cache lecą na pliki,
|
||||
schematu legacy nie dotykamy.
|
||||
|
||||
### Zasoby binarne (air-gap)
|
||||
|
||||
Nie da się ich pobrać na produkcji, więc leżą w repo (`.gitignore` ma na nie wyjątki):
|
||||
|
||||
| Plik | Skąd |
|
||||
|---|---|
|
||||
| `public/images/logo.png`, `dashboard_01.png`, `dashboard_02.png` | makiety `newPGE/uj_dashboard/` (jedyna rzecz, po jaką sięgamy do `newPGE` - kodu legacy szukamy w `trzeciCloude_v1`) |
|
||||
| `public/fonts/inter/Inter-Variable.woff2` | fontsource (Inter VF, latin) |
|
||||
| `public/fonts/jetbrains-mono/JetBrainsMono-Variable.woff2` | fontsource (JetBrains Mono VF, latin) |
|
||||
| `public/fonts/material-symbols/MaterialSymbolsOutlined.woff2` | fontsource (Material Symbols Outlined VF) |
|
||||
|
||||
Vite przy buildzie ostrzega, że `/fonts/...` nie rozwiązuje się w czasie budowania -
|
||||
to jest **oczekiwane**: ścieżki są absolutne i obsługuje je serwer z `public/`.
|
||||
|
||||
## Jak nałożyć na `softspm_pge` (UAT, Debian)
|
||||
|
||||
1. Skopiować pliki nakładki (lista: `docker/overlay-files.txt`) do
|
||||
`/var/www/html/softspm_pge/`, nadpisując m.in. `config/database.php`,
|
||||
`config/navigation.php`, `config/softspm.php`, `routes/web.php`,
|
||||
`resources/`, `package.json`, `vite.config.js`. `vendor/` i `node_modules/`
|
||||
zostają; z `public/` kopiujemy tylko `images/` i `fonts/`.
|
||||
2. Uprawnienia (Debian): `chown -R www-data:www-data storage bootstrap/cache`.
|
||||
3. Front-end (Tailwind v4 przez `@tailwindcss/vite`, konfiguracja w
|
||||
`resources/css/app.css`; NIE ma `tailwind.config.js` ani `postcss.config.js`):
|
||||
```
|
||||
npm install
|
||||
npm run build # produkcyjny build do public/build
|
||||
```
|
||||
4. `.env` (sekcja niżej) + `php artisan config:cache`. **NIE** uruchamiać
|
||||
`php artisan migrate` - sesje/cache trzymamy na plikach (patrz `.env`), więc
|
||||
nie tworzymy żadnych tabel; legacy schematu nie dotykamy.
|
||||
|
||||
## Zmienne `.env`
|
||||
|
||||
```dotenv
|
||||
APP_NAME=SoftSPM
|
||||
APP_LOCALE=pl
|
||||
|
||||
# Database-first + brak potrzeby tabel pomocniczych → sesje/cache na plikach.
|
||||
# Dzięki temu NIE uruchamiamy migracji i nie dorzucamy tabel do baz legacy.
|
||||
SESSION_DRIVER=file
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
DB_CONNECTION=softspm
|
||||
|
||||
# Baza operacyjna (domyślna)
|
||||
DB_HOST=10.42.1.210
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=softspm
|
||||
DB_USERNAME=przemek
|
||||
DB_PASSWORD=visia2209
|
||||
|
||||
# Bazy raportowa i historyczna (ten sam host/user, inna nazwa bazy)
|
||||
DB_REP_DATABASE=softspm_rep
|
||||
DB_HIS_DATABASE=softspm_his
|
||||
|
||||
# Migracja haseł MD5->bcrypt - włączyć DOPIERO po rozszerzeniu
|
||||
# kolumny sys_users.USR_PASSWORD do VARCHAR(255).
|
||||
SOFTSPM_REHASH=false
|
||||
```
|
||||
|
||||
## Air-gap (produkcja bez internetu) - zasady
|
||||
|
||||
- **Zero CDN w runtime.** Tailwind kompilujemy lokalnie (Vite). W makietach
|
||||
(`uj_dashboard/`) Tailwind/fonty szły z CDN - to tylko podgląd.
|
||||
- **Fonty self-hosted.** Pobrać i umieścić w `public/fonts/` (na maszynie z
|
||||
netem), zgodnie z `@font-face` w `resources/css/app.css`:
|
||||
- `public/fonts/inter/Inter-Variable.woff2`
|
||||
- `public/fonts/jetbrains-mono/JetBrainsMono-Variable.woff2`
|
||||
- `public/fonts/material-symbols/MaterialSymbolsOutlined.woff2`
|
||||
- **Tło logowania:** `public/images/login-bg.jpg` (zamiast obrazka z Google
|
||||
w makiecie - można użyć `trzeciCloude_v1/images/tlo.jpg`).
|
||||
- **Build na UAT (Debian, ma internet), wdrożenie na Windows (air-gap):**
|
||||
assety i zależności budujemy na UAT (`npm run build`, `composer install`),
|
||||
a na produkcję Windows kopiujemy **gotowy artefakt** - pełny `vendor/` +
|
||||
skompilowane `public/build/` + self-hostowane fonty/obrazki. Na produkcji
|
||||
**bez** `composer install` / `npm install` (brak internetu i często Node).
|
||||
- **Web root produkcji (Windows):** serwer (IIS/Apache) musi wskazywać podkatalog
|
||||
`public/`, nie korzeń projektu.
|
||||
|
||||
## Co już jest (pierwszy pionowy slice)
|
||||
|
||||
| Plik | Rola |
|
||||
|---|---|
|
||||
| `config/database.php` | 3 połączenia MariaDB (database-first, bez zmiany schematu) |
|
||||
| `config/softspm.php` | flaga migracji haseł MD5→bcrypt |
|
||||
| `resources/css/app.css` | tokeny „Industrial Precision" (Tailwind v4, `@theme`) + fonty self-hosted |
|
||||
| `resources/views/components/guest-layout.blade.php` | layout logowania |
|
||||
| `resources/views/components/app-layout.blade.php` | layout aplikacji (topbar + sidebar) |
|
||||
| `resources/views/auth/login.blade.php` | ekran logowania (wg makiety) |
|
||||
| `resources/views/dashboard.blade.php` | pulpit (placeholder mapy) |
|
||||
| `app/Models/User.php` | model na `sys_users` |
|
||||
| `app/Http/Controllers/Auth/LoginController.php` | logowanie MD5/bcrypt + wybór oddziału |
|
||||
| `app/Http/Controllers/WellController.php` | Studnie: lista (site401) + dodawanie/edycja (site301A/E/inf) |
|
||||
| `app/Services/WellService.php` | transakcyjny zapis studni (lookupy + create + update) |
|
||||
| `resources/views/wells/index.blade.php` | lista studni (wg makiety "Well Inventory") |
|
||||
| `resources/views/wells/create.blade.php`, `edit.blade.php` | formularze dodania/edycji |
|
||||
| `resources/views/components/well-field.blade.php`, `well-select.blade.php` | pola formularza |
|
||||
| `routes/web.php` | trasy: login / logout / dashboard / wells (CRUD) |
|
||||
|
||||
## Weryfikacja parytetu: Studnie (site401 → /wells)
|
||||
|
||||
Nowa lista zastępuje **N+1** z legacy (podzapytania pompa/silnik/ON-OFF na każdy
|
||||
wiersz) **jednym** zapytaniem z JOIN-ami. Przed zatwierdzeniem sprawdzić na
|
||||
serwerze testowym, że zwraca ten sam zestaw studni co legacy, dla tego samego
|
||||
oddziału i statusu. Szybki test liczności (MariaDB, podstaw `<DZIAL>`, `<STATUS>`):
|
||||
|
||||
```sql
|
||||
-- Legacy: bazowa lista (przed N+1) - site401.php, zapytanie $query
|
||||
SELECT COUNT(*) FROM seg_wells
|
||||
LEFT JOIN seg_dev_resources ON DRS_DEV_ID=WLL_ID AND DRS_DEV_TYPE=12
|
||||
LEFT JOIN seg_well_pump_eng_rel ON RWL_WLL_ID=WLL_ID
|
||||
WHERE DRS_DEPARTMENT='<DZIAL>' AND DRS_STATUS_DIC_ID=<STATUS>;
|
||||
```
|
||||
|
||||
Porównać z `->total()` na `/wells?status=<STATUS>` (ten sam oddział z sesji).
|
||||
Dodatkowo wyrywkowo sprawdzić kilka studni: nazwa pompy/silnika, nr seryjny i
|
||||
ostatni stan ON/OFF (DRD_RESOURCES_DETAIL=507, najnowszy DRD_ID) muszą się zgadzać.
|
||||
|
||||
### Świadome odstępstwa od legacy (do akceptacji)
|
||||
- **Paginacja serwerowa** (`paginate(20)`) zamiast cache w sesji + limit 100.
|
||||
Usuwa to limit 100 i checkbox „Pokaż wszystkie" (zbędny przy realnej paginacji).
|
||||
- **Filtry przez GET** (zamiast POST+sesja) - czyste linki, komponują się z paginacją.
|
||||
- **Stan ON/OFF jako chip** (ON/OFF) zamiast ikon power_on/off.png.
|
||||
|
||||
## Dodawanie / edycja studni (site301A/E/inf → /wells/create, /wells/{drs}/edit)
|
||||
|
||||
Zapis jest **wielotabelowy**. Dodanie studni wstawia do: `seg_wells`,
|
||||
`seg_dev_resources` (typ 12), `seg_dev_resources_det` ×3 (średnica `DST`,
|
||||
głębokość `LS`, zasilanie `LZ`), `seg_object_hole_cards`,
|
||||
`seg_object_hole_cards_his`, `seg_dev_resources_his`. Edycja aktualizuje
|
||||
`seg_wells`, `seg_dev_resources`, `seg_object_hole_cards` oraz (gdy istnieje)
|
||||
datę zabudowy w `seg_wells_exchanged`. Cała logika w `WellService`.
|
||||
|
||||
### Ulepszenia względem legacy
|
||||
- **Transakcja** (`DB::transaction`) - legacy robił te INSERT-y bez transakcji
|
||||
(ryzyko danych częściowych przy błędzie). Teraz zapis jest atomowy.
|
||||
- **Walidacja** wejścia + kontrola unikalności nazwy.
|
||||
|
||||
### Świadome odstępstwa / pominięcia (do akceptacji)
|
||||
- **NIE przebudowujemy `rep_object_departments`** po każdym dodaniu (legacy robił
|
||||
`truncate`+`insert` całej tabeli przez ajax `refresh_departments`). To wąskie
|
||||
gardło wydajności; nowa lista `/wells` go nie używa. Do rewizji przy migracji
|
||||
raportów, jeśli inne ekrany na nim polegają.
|
||||
- **Słowniki w języku PL** (`DIC_POL_LANG`) - pełne i18n PL/EN/DE w osobnym etapie.
|
||||
- **`build_time`** jako jedno pole `time` (HH:MM) zamiast dwóch selectów godz./min.
|
||||
|
||||
### Weryfikacja (na serwerze testowym)
|
||||
Dodać testową studnię w wybranym oddziale → sprawdzić, że pojawia się na `/wells`
|
||||
(status 3, ten oddział) oraz że powstały spójne wpisy w 6 tabelach. Edytować →
|
||||
sprawdzić aktualizację `seg_wells`/`seg_dev_resources`/`seg_object_hole_cards`.
|
||||
|
||||
## Otwarte do ustalenia
|
||||
|
||||
- **Hasła MD5 → bcrypt** - `USR_PASSWORD` to `varchar(64)`, więc bcrypt się mieści
|
||||
(rehash bezpieczny bez zmiany schematu). Czeka na zgodę klienta (`SOFTSPM_REHASH=true`).
|
||||
- **Mapa offline** - strategia kafelków bez internetu (lokalny tile-server /
|
||||
kafelki statyczne / własny podkład kopalni).
|
||||
- **`rep_object_departments`** - czy odtworzyć przebudowę po zapisie studni
|
||||
(na razie pominięta - patrz wyżej).
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "softspm/softspm2",
|
||||
"type": "project",
|
||||
"description": "SoftSPM 2 - migracja legacy PHP 5.6 (newPGE) do Laravela, database-first na MariaDB.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"ext-pdo": "*",
|
||||
"ext-soap": "*",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.50"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
Generated
+8402
File diff suppressed because it is too large
Load Diff
+126
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| SoftSPM_2 - konfiguracja baz danych (nakładka na szkielet Laravela)
|
||||
|--------------------------------------------------------------------------
|
||||
| Podejście DATABASE-FIRST: pracujemy na istniejącym schemacie MariaDB
|
||||
| (NIE zmieniamy struktury). Trzy osobne bazy:
|
||||
| - softspm → dane operacyjne (tabele seg_/sys_/tst_) [domyślna]
|
||||
| - softspm_rep → tabele raportowe (rep_)
|
||||
| - softspm_his → historia (tabele _his)
|
||||
|
|
||||
| Wartości pobierane z .env (ustawiane na serwerze testowym). Sterownik
|
||||
| 'mariadb' wymaga Laravel 11+. Wszystkie 3 bazy stoją na tym samym hoście,
|
||||
| ale trzymamy je jako odrębne połączenia - modele wskazują swoje przez
|
||||
| właściwość $connection.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
'default' => env('DB_CONNECTION', 'softspm'),
|
||||
|
||||
'connections' => [
|
||||
|
||||
// === Baza operacyjna (domyślna) ===
|
||||
'softspm' => [
|
||||
'driver' => 'mariadb',
|
||||
'host' => env('DB_HOST', '10.42.1.210'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'softspm'),
|
||||
'username' => env('DB_USERNAME', 'spm_local'),
|
||||
'password' => env('DB_PASSWORD', 'ff8efe59e7265fe9680b0c325e44cb53'),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => 'utf8',
|
||||
'collation' => 'utf8_general_ci', // zgodnie z SET NAMES utf8 w legacy
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => false, // legacy schema - wyłączamy ONLY_FULL_GROUP_BY itd.
|
||||
'engine' => null,
|
||||
],
|
||||
|
||||
// === Baza raportowa ===
|
||||
'softspm_rep' => [
|
||||
'driver' => 'mariadb',
|
||||
'host' => env('DB_REP_HOST', env('DB_HOST', '10.42.1.210')),
|
||||
'port' => env('DB_REP_PORT', env('DB_PORT', '3306')),
|
||||
'database' => env('DB_REP_DATABASE', 'softspm_rep'),
|
||||
'username' => env('DB_REP_USERNAME', env('DB_USERNAME', 'spm_local')),
|
||||
'password' => env('DB_REP_PASSWORD', env('DB_PASSWORD', 'ff8efe59e7265fe9680b0c325e44cb53')),
|
||||
'charset' => 'utf8',
|
||||
'collation' => 'utf8_general_ci',
|
||||
'prefix' => '',
|
||||
'strict' => false,
|
||||
'engine' => null,
|
||||
],
|
||||
|
||||
// === Baza historyczna ===
|
||||
'softspm_his' => [
|
||||
'driver' => 'mariadb',
|
||||
'host' => env('DB_HIS_HOST', env('DB_HOST', '10.42.1.210')),
|
||||
'port' => env('DB_HIS_PORT', env('DB_PORT', '3306')),
|
||||
'database' => env('DB_HIS_DATABASE', 'softspm_his'),
|
||||
'username' => env('DB_HIS_USERNAME', env('DB_USERNAME', 'spm_local')),
|
||||
'password' => env('DB_HIS_PASSWORD', env('DB_PASSWORD', 'ff8efe59e7265fe9680b0c325e44cb53')),
|
||||
'charset' => 'utf8',
|
||||
'collation' => 'utf8_general_ci',
|
||||
'prefix' => '',
|
||||
'strict' => false,
|
||||
'engine' => null,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
| Migracje Laravela trzymamy w bazie operacyjnej. UWAGA: w podejściu
|
||||
| database-first migracji raczej nie uruchamiamy na istniejących tabelach
|
||||
| - służy głównie tabelom pomocniczym (sessions, cache, jobs).
|
||||
*/
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'softspm'), '_').'_database_'),
|
||||
],
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
],
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| SoftSPM 2 - struktura nawigacji (jedno źródło dla całego systemu)
|
||||
|--------------------------------------------------------------------------
|
||||
| Wzorzec jak w oryginale:
|
||||
| - górne menu = SEKCJE (zakres),
|
||||
| - po kliknięciu sekcji w LEWYM menu pojawiają się jej pozycje,
|
||||
| - kliknięcie pozycji otwiera funkcję.
|
||||
|
|
||||
| Struktura ODTWORZONA 1:1 z legacy index.php (sekcje + kolejność pozycji
|
||||
| + etykiety z language/polish.php). 'route' dokładamy w miarę migracji;
|
||||
| null = placeholder „w przygotowaniu".
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
[
|
||||
'key' => 'start', 'label' => 'Start', 'icon' => 'home',
|
||||
'route' => 'dashboard',
|
||||
'items' => [
|
||||
['label' => 'Mapa obiektów', 'icon' => 'map', 'route' => 'dashboard'],
|
||||
],
|
||||
],
|
||||
|
||||
// ===== Katalogi (200) - kolejność wg index.php: 203,204,205,201,206,207,208 =====
|
||||
[
|
||||
'key' => 'katalogi', 'label' => 'Katalogi', 'icon' => 'inventory_2',
|
||||
'items' => [
|
||||
['label' => 'Katalog pomp', 'icon' => 'water_drop', 'route' => 'pumps.index'],
|
||||
['label' => 'Katalog silników', 'icon' => 'bolt', 'route' => 'engines.index'],
|
||||
['label' => 'Katalog agregatów', 'icon' => 'precision_manufacturing', 'route' => 'aggregates.index'],
|
||||
['label' => 'Wybór agregatu pompowego', 'icon' => 'checklist', 'route' => 'aggregate-selection.index'],
|
||||
['label' => 'Katalog kabli', 'icon' => 'cable', 'route' => 'cables.index'],
|
||||
['label' => 'Katalog orurowania i armatury', 'icon' => 'plumbing', 'route' => 'piping.index'],
|
||||
['label' => 'Katalog opomiarowania', 'icon' => 'speed', 'route' => 'metering.index'],
|
||||
],
|
||||
],
|
||||
|
||||
// ===== Stan (300) - kolejność wg index.php: 303,304,301,302,309,306,307,308 =====
|
||||
[
|
||||
'key' => 'stan', 'label' => 'Stan', 'icon' => 'monitoring',
|
||||
'items' => [
|
||||
['label' => 'Stan pomp', 'icon' => 'water_drop', 'route' => null], // site303
|
||||
['label' => 'Stan silników', 'icon' => 'bolt', 'route' => null], // site304
|
||||
['label' => 'Stan studni', 'icon' => 'water_drop', 'route' => null], // site301
|
||||
['label' => 'Stan zbiorników', 'icon' => 'inventory_2', 'route' => null], // site302
|
||||
['label' => 'Stan piezometrów', 'icon' => 'biotech', 'route' => null], // site309
|
||||
['label' => 'Stan kabli', 'icon' => 'cable', 'route' => null], // site306
|
||||
['label' => 'Stan orurowania i armatury', 'icon' => 'plumbing', 'route' => null], // site307
|
||||
['label' => 'Stan opomiarowania', 'icon' => 'speed', 'route' => null], // site308
|
||||
],
|
||||
],
|
||||
|
||||
// ===== Eksploatacja (400) - kolejność wg index.php: 401,402,404,405,406,403,407 =====
|
||||
[
|
||||
'key' => 'eksploatacja', 'label' => 'Eksploatacja', 'icon' => 'engineering',
|
||||
'items' => [
|
||||
['label' => 'Eksploatacja studni', 'icon' => 'water_drop', 'route' => 'well-operation.index'], // site401
|
||||
['label' => 'Eksploatacja zbiorników', 'icon' => 'inventory_2', 'route' => null], // site402
|
||||
['label' => 'Eksploatacja piezometrów', 'icon' => 'biotech', 'route' => null], // site404
|
||||
['label' => 'Eksploatacja ujęć', 'icon' => 'map', 'route' => null], // site405
|
||||
['label' => 'Kreator doboru pompy', 'icon' => 'checklist', 'route' => null], // site406
|
||||
['label' => 'Parametry stałe', 'icon' => 'settings', 'route' => null], // site403
|
||||
['label' => 'Zrzut wody - rejestr', 'icon' => 'water_drop', 'route' => null, 'append_department' => true], // site407
|
||||
],
|
||||
],
|
||||
|
||||
// ===== Stacja prób (500) - 501,502,503,504 =====
|
||||
[
|
||||
'key' => 'stacja_prob', 'label' => 'Stacja prób', 'icon' => 'biotech',
|
||||
'items' => [
|
||||
['label' => 'Pompy', 'icon' => 'water_drop', 'route' => null], // site501
|
||||
['label' => 'Silniki', 'icon' => 'bolt', 'route' => null], // site502
|
||||
['label' => 'Agregaty', 'icon' => 'precision_manufacturing', 'route' => null], // site503
|
||||
['label' => 'Kable', 'icon' => 'cable', 'route' => null], // site504
|
||||
],
|
||||
],
|
||||
|
||||
// ===== Remonty (600) - 601,602,603 =====
|
||||
[
|
||||
'key' => 'remonty', 'label' => 'Remonty', 'icon' => 'build',
|
||||
'items' => [
|
||||
['label' => 'Demontaż', 'icon' => 'build', 'route' => null], // site601
|
||||
['label' => 'Weryfikacja', 'icon' => 'checklist', 'route' => null], // site602
|
||||
['label' => 'Jakość remontów', 'icon' => 'monitoring', 'route' => null], // site603
|
||||
],
|
||||
],
|
||||
|
||||
// ===== Gospodarka (700) - 701,702,703,704 =====
|
||||
[
|
||||
'key' => 'gospodarka', 'label' => 'Gospodarka', 'icon' => 'inventory_2',
|
||||
'items' => [
|
||||
['label' => 'Statystyki rezerw studni', 'icon' => 'monitoring', 'route' => null], // site701
|
||||
['label' => 'Rezerwy części nowych', 'icon' => 'inventory_2', 'route' => null], // site702
|
||||
['label' => 'Rezerwy części regenerowanych', 'icon' => 'inventory_2', 'route' => null], // site703
|
||||
['label' => 'Rezerwy drutów i kabli', 'icon' => 'cable', 'route' => null], // site704
|
||||
],
|
||||
],
|
||||
|
||||
// ===== Administracja (800) - kolejność wg index.php: 801,802,805,803,806,807,809,810,804 =====
|
||||
[
|
||||
'key' => 'administracja', 'label' => 'Administracja', 'icon' => 'settings',
|
||||
'items' => [
|
||||
['label' => 'Zarządzanie użytkownikami', 'icon' => 'settings', 'route' => null], // site801
|
||||
['label' => 'Zarządzanie profilami', 'icon' => 'settings', 'route' => null], // site802
|
||||
['label' => 'Zarządzanie bazą wiedzy', 'icon' => 'menu_book', 'route' => null], // site805
|
||||
['label' => 'Producenci', 'icon' => 'precision_manufacturing', 'route' => null], // site803
|
||||
['label' => 'Słowniki', 'icon' => 'menu_book', 'route' => null], // site806
|
||||
['label' => 'Parametryzacja', 'icon' => 'settings', 'route' => null], // site807
|
||||
['label' => 'Raporty', 'icon' => 'monitoring', 'route' => null], // site809
|
||||
['label' => 'Zlecenia', 'icon' => 'build', 'route' => null], // site810
|
||||
['label' => 'Zmiana hasła', 'icon' => 'settings', 'route' => null], // site804
|
||||
],
|
||||
],
|
||||
|
||||
// ===== Baza wiedzy (900) - w oryginale bezpośrednia strona (site900) =====
|
||||
[
|
||||
'key' => 'baza_wiedzy', 'label' => 'Baza wiedzy', 'icon' => 'menu_book',
|
||||
'items' => [],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain without subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| SoftSPM_2 - ustawienia aplikacyjne (nakładka)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
| Migracja haseł MD5 -> bcrypt przy logowaniu.
|
||||
| Kolumna sys_users.USR_PASSWORD to VARCHAR(64) - bcrypt (60 zn.) SIĘ MIEŚCI,
|
||||
| więc rehash jest bezpieczny bez zmiany schematu. WYŁĄCZONE wg decyzji klienta
|
||||
| (na razie zostajemy przy MD5); włączyć przez SOFTSPM_REHASH=true po jego zgodzie.
|
||||
*/
|
||||
'rehash_passwords' => env('SOFTSPM_REHASH', false),
|
||||
|
||||
/*
|
||||
| Oddziały (DRS_DEPARTMENT). Wybierane przy logowaniu, filtrują listy
|
||||
| obiektów (jak legacy $_SESSION['syndis_eq_stock']). Wartości muszą
|
||||
| dokładnie odpowiadać DRS_DEPARTMENT w bazie.
|
||||
*/
|
||||
'departments' => [
|
||||
'P/Szczerców',
|
||||
'P/Bełchatów',
|
||||
'Dębina',
|
||||
],
|
||||
|
||||
/*
|
||||
| FAZA TESTÓW: wymuszony oddział logowania. Gdy ustawione (np. 'P/Szczerców'),
|
||||
| pole „Oddział" na ekranie logowania jest UKRYTE, a logowanie używa tej wartości.
|
||||
| ODWRACALNE: ustaw na null, aby przywrócić wybór oddziału przez użytkownika.
|
||||
*/
|
||||
'login_department' => 'P/Szczerców',
|
||||
|
||||
/*
|
||||
| Integracja SYNDIS (SOAP) - pobór aktualnych pomiarów studni.
|
||||
| WSDL + lokalizacja usługi zależne od środowiska (na prod LAN-owe IP).
|
||||
| Gdy puste/niedostępne - „Pobierz aktualne dane" zgłosi błąd (graceful).
|
||||
*/
|
||||
'syndis' => [
|
||||
'wsdl' => env('SYNDIS_WSDL', 'http://172.21.11.101/SyndisSpm.wsdl'),
|
||||
'location' => env('SYNDIS_LOCATION', 'http://172.21.11.101:3615/'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# SoftSPM_2 - lokalne środowisko deweloperskie.
|
||||
# Baza danych NIE jest kontenerem - łączymy się do istniejącej MariaDB
|
||||
# w sieci LAN (10.42.1.210), zgodnie z podejściem database-first.
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
container_name: softspm2-app
|
||||
ports:
|
||||
- "8000:80"
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
environment:
|
||||
APACHE_DOCUMENT_ROOT: /var/www/html/public
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
|
||||
# Vite dev server (hot reload). Uruchamiać opcjonalnie:
|
||||
# docker compose --profile dev up vite
|
||||
vite:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
container_name: softspm2-vite
|
||||
profiles: ["dev"]
|
||||
ports:
|
||||
- "5173:5173"
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
command: npm run dev -- --host 0.0.0.0
|
||||
@@ -0,0 +1,32 @@
|
||||
# SoftSPM_2 - obraz deweloperski (PHP 8.3 + Apache + Node 20)
|
||||
# Produkcja: Windows Server / IIS+Apache z zainstalowanym PHP - ten obraz
|
||||
# służy wyłącznie do pracy lokalnej. DocumentRoot wskazuje na public/.
|
||||
FROM php:8.3-apache
|
||||
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git unzip zip curl ca-certificates \
|
||||
libzip-dev libpng-dev libjpeg62-turbo-dev libfreetype6-dev \
|
||||
libxml2-dev libicu-dev libonig-dev \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j"$(nproc)" \
|
||||
pdo_mysql mysqli soap zip gd intl opcache \
|
||||
&& a2enmod rewrite headers \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Node 20 - build assetów (Vite + Tailwind v4)
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
|
||||
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
|
||||
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
|
||||
|
||||
COPY docker/php.ini /usr/local/etc/php/conf.d/zz-softspm.ini
|
||||
|
||||
WORKDIR /var/www/html
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// Smoke-test połączeń DB (uruchamiać: php artisan tinker < docker/check/db-check.php
|
||||
// albo: docker compose exec app php docker/check/db-check.php)
|
||||
require __DIR__.'/../../vendor/autoload.php';
|
||||
$app = require __DIR__.'/../../bootstrap/app.php';
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
foreach (['softspm' => 'sys_users', 'softspm_rep' => null, 'softspm_his' => null] as $conn => $probe) {
|
||||
try {
|
||||
$row = DB::connection($conn)->select('SELECT DATABASE() AS d, VERSION() AS v')[0];
|
||||
$extra = '';
|
||||
if ($probe) {
|
||||
$extra = ', ' . $probe . '=' . DB::connection($conn)->table($probe)->count() . ' rek.';
|
||||
}
|
||||
echo "OK {$conn} -> {$row->d} (MariaDB {$row->v}{$extra})\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "FAIL {$conn} -> " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Kluczowe tabele legacy używane przez nakładkę
|
||||
$tables = ['sys_users', 'seg_wells', 'seg_dev_resources', 'seg_dev_resources_det',
|
||||
'seg_equipments', 'seg_well_pump_eng_rel', 'seg_object_hole_cards', 'sys_dictionaries',
|
||||
'seg_pumps', 'seg_engines', 'seg_manufacturers', 'rep_his_data_everyday', 'tst_tests'];
|
||||
foreach ($tables as $t) {
|
||||
try {
|
||||
$n = DB::connection('softspm')->table($t)->count();
|
||||
echo "OK softspm.{$t} = {$n}\n";
|
||||
} catch (\Throwable $e) {
|
||||
echo "FAIL softspm.{$t} -> " . substr($e->getMessage(), 0, 120) . "\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
# SoftSPM_2 - pliki NAKŁADKI (kopiowane na szkielet Laravela: UAT /var/www/html/softspm_pge).
|
||||
# Wygenerowane 2026-09-03, PRZED domieszaniem szkieletu Laravela do tego katalogu.
|
||||
# Reszta drzewa (artisan, bootstrap/, vendor/, public/index.php, pozostałe config/*.php) to szkielet.
|
||||
|
||||
app/Http/Controllers/AggregateController.php
|
||||
app/Http/Controllers/AggregateSelectionController.php
|
||||
app/Http/Controllers/Auth/LoginController.php
|
||||
app/Http/Controllers/CableController.php
|
||||
app/Http/Controllers/Concerns/SortsLists.php
|
||||
app/Http/Controllers/DashboardController.php
|
||||
app/Http/Controllers/EngineController.php
|
||||
app/Http/Controllers/EquipmentFileController.php
|
||||
app/Http/Controllers/MeteringController.php
|
||||
app/Http/Controllers/PipingController.php
|
||||
app/Http/Controllers/PumpController.php
|
||||
app/Http/Controllers/WellController.php
|
||||
app/Http/Controllers/WellOperationController.php
|
||||
app/Http/Middleware/SetLocale.php
|
||||
app/Http/Requests/StoreCableRequest.php
|
||||
app/Http/Requests/StoreEngineRequest.php
|
||||
app/Http/Requests/StoreMeteringRequest.php
|
||||
app/Http/Requests/StorePipingRequest.php
|
||||
app/Http/Requests/StorePumpRequest.php
|
||||
app/Http/Requests/UpdateCableRequest.php
|
||||
app/Http/Requests/UpdateEngineRequest.php
|
||||
app/Http/Requests/UpdateMeteringRequest.php
|
||||
app/Http/Requests/UpdatePipingRequest.php
|
||||
app/Http/Requests/UpdatePumpRequest.php
|
||||
app/Models/Engine.php
|
||||
app/Models/Equipment.php
|
||||
app/Models/EquipmentFile.php
|
||||
app/Models/Manufacturer.php
|
||||
app/Models/Pump.php
|
||||
app/Models/PumpCurvePoint.php
|
||||
app/Models/User.php
|
||||
app/Services/EquipmentFileService.php
|
||||
app/Services/PumpService.php
|
||||
app/Services/SyndisService.php
|
||||
app/Services/WellAssessmentService.php
|
||||
app/Services/WellService.php
|
||||
app/Support/PolynomialRegression.php
|
||||
config/database.php
|
||||
config/navigation.php
|
||||
config/softspm.php
|
||||
resources/css/app.css
|
||||
resources/js/app.js
|
||||
resources/views/auth/login.blade.php
|
||||
resources/views/catalog/aggregate-selection/index.blade.php
|
||||
resources/views/catalog/aggregates/edit.blade.php
|
||||
resources/views/catalog/aggregates/index.blade.php
|
||||
resources/views/catalog/aggregates/show.blade.php
|
||||
resources/views/catalog/cables/create.blade.php
|
||||
resources/views/catalog/cables/edit.blade.php
|
||||
resources/views/catalog/cables/index.blade.php
|
||||
resources/views/catalog/cables/show.blade.php
|
||||
resources/views/catalog/engines/create.blade.php
|
||||
resources/views/catalog/engines/edit.blade.php
|
||||
resources/views/catalog/engines/index.blade.php
|
||||
resources/views/catalog/engines/show.blade.php
|
||||
resources/views/catalog/metering/create.blade.php
|
||||
resources/views/catalog/metering/edit.blade.php
|
||||
resources/views/catalog/metering/index.blade.php
|
||||
resources/views/catalog/metering/show.blade.php
|
||||
resources/views/catalog/piping/create.blade.php
|
||||
resources/views/catalog/piping/edit.blade.php
|
||||
resources/views/catalog/piping/index.blade.php
|
||||
resources/views/catalog/piping/show.blade.php
|
||||
resources/views/catalog/pumps/create.blade.php
|
||||
resources/views/catalog/pumps/edit.blade.php
|
||||
resources/views/catalog/pumps/index.blade.php
|
||||
resources/views/catalog/pumps/show.blade.php
|
||||
resources/views/components/app-layout.blade.php
|
||||
resources/views/components/btn.blade.php
|
||||
resources/views/components/confirm-delete.blade.php
|
||||
resources/views/components/curve-chart.blade.php
|
||||
resources/views/components/energy-gauge.blade.php
|
||||
resources/views/components/guest-layout.blade.php
|
||||
resources/views/components/icon.blade.php
|
||||
resources/views/components/power-schematic.blade.php
|
||||
resources/views/components/pump-curve.blade.php
|
||||
resources/views/components/selection-chart.blade.php
|
||||
resources/views/components/sortable-th.blade.php
|
||||
resources/views/components/well-field.blade.php
|
||||
resources/views/components/well-schematic.blade.php
|
||||
resources/views/components/well-select.blade.php
|
||||
resources/views/dashboard.blade.php
|
||||
resources/views/exploitation/wells/index.blade.php
|
||||
resources/views/exploitation/wells/params.blade.php
|
||||
resources/views/exploitation/wells/show.blade.php
|
||||
resources/views/section.blade.php
|
||||
resources/views/wells/create.blade.php
|
||||
resources/views/wells/edit.blade.php
|
||||
resources/views/wells/index.blade.php
|
||||
routes/web.php
|
||||
README_SOFTSPM.md
|
||||
package.json
|
||||
vite.config.js
|
||||
docker-compose.yml
|
||||
docker/Dockerfile
|
||||
docker/php.ini
|
||||
public/images/logo.png
|
||||
public/images/dashboard_01.png
|
||||
public/images/dashboard_02.png
|
||||
public/fonts/inter/Inter-Variable.woff2
|
||||
public/fonts/jetbrains-mono/JetBrainsMono-Variable.woff2
|
||||
public/fonts/material-symbols/MaterialSymbolsOutlined.woff2
|
||||
docker/check/db-check.php
|
||||
docker/overlay-files.txt
|
||||
.env.example
|
||||
.gitignore # (uwaga: wyjatki na public/fonts i public/images dopisane do wersji ze szkieletu)
|
||||
@@ -0,0 +1,15 @@
|
||||
; SoftSPM_2 - ustawienia PHP (dev)
|
||||
memory_limit = 512M
|
||||
upload_max_filesize = 64M
|
||||
post_max_size = 64M
|
||||
max_execution_time = 300
|
||||
date.timezone = Europe/Warsaw
|
||||
|
||||
; Załączniki wyposażenia trzymane są jako BLOB - podnosimy limity pakietu
|
||||
mysqli.allow_local_infile = Off
|
||||
|
||||
; SOAP (integracja SYNDIS) - bez cache WSDL w dev
|
||||
soap.wsdl_cache_enabled = 0
|
||||
|
||||
display_errors = On
|
||||
error_reporting = E_ALL
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
# Legacy SoftSPM - źródło migracji
|
||||
|
||||
**Katalog źródłowy: `C:\Programy\xampp_old\htdocs\trzeciCloude_v1\`**
|
||||
|
||||
To jest jedyne źródło, do którego się odwołujemy przy przepisywaniu na Laravela.
|
||||
W `htdocs/` leży kilka równoległych kopii tej samej aplikacji (`newPGE`,
|
||||
`trzeciTestowy`, `pierwszyPodstawowy`, `wellsctrl*`) - **ignorujemy je**.
|
||||
`trzeciCloude_v1` jako jedyna ma repozytorium git (`Initial commit`) oraz własną
|
||||
dokumentację.
|
||||
|
||||
## Nie powielamy istniejącej dokumentacji
|
||||
|
||||
W legacy leżą dwa opracowania - **czytać je zamiast wyprowadzać wnioski od nowa**:
|
||||
|
||||
| Plik | Co zawiera |
|
||||
|---|---|
|
||||
| `trzeciCloude_v1/CLAUDE.md` (181 linii, EN) | architektura, konwencje nazewnicze bazy, routing shardów historii, martwy kod |
|
||||
| `trzeciCloude_v1/AUDYT.md` (125 linii, PL) | audyt techniczny 2026-06-09 + plan P0/P1/P2 (SQLi, LFI, MD5, SSO, sekrety) |
|
||||
|
||||
Schemat baz: `trzeciCloude_v1/db_structure/*.sql` - **zrzuty struktury bez danych**
|
||||
dla wszystkich trzech baz. Grepować je zamiast zgadywać kolumny.
|
||||
|
||||
## Skala (zmierzone 2026-09-03)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| plików PHP | 983 (1933 pliki łącznie) |
|
||||
| fragmentów stron `sites/*.php` | 198, w 50 numerach `siteNNN` |
|
||||
| raportów `reports/*.php` | 73 |
|
||||
| eksportów `export/*.php` | 45 |
|
||||
| `func.php` | **4156 linii, 111 funkcji** |
|
||||
| `index.php` (front controller + menu + auth) | 435 linii |
|
||||
| `ajax_func.php` / `ajax_getdata4well.php` | 315 / 208 linii |
|
||||
|
||||
> Rozbieżność: `CLAUDE.md` legacy podaje „func.php ~3400 linii, ~140 funkcji",
|
||||
> `AUDYT.md` „~4000 linii, ~140 funkcji". Faktycznie **4156 / 111**.
|
||||
> `CLAUDE.md` twierdzi też „Not a git repository" - dla `trzeciCloude_v1`
|
||||
> to już nieprawda.
|
||||
|
||||
## Architektura w skrócie
|
||||
|
||||
- **Front controller**: `index.php`, dispatch po `?pg=siteNNN` → `include sites/siteNNN.php`
|
||||
(domyślnie `site401`). Menu, bramka auth i połączenie z bazą też są w `index.php`.
|
||||
- **Warianty stron po sufiksie**: `A`=add, `E`=edit, `inf`=szczegóły, `his`/`ahis`/`mhis`/`offhis`=historie,
|
||||
`charts`, `det`/`pdet`=detale, `mark`=widok mapowy. Przykłady:
|
||||
`site301` ma 18 wariantów, `site401` ma 19.
|
||||
- **Uprawnienia per strona**: `sys_user_profiles → sys_profile_site → sys_site_access`
|
||||
spłaszczone do `$_SESSION['siteNNN']` / `$_SESSION['siteNNN_edit']`.
|
||||
**Router ich nie sprawdza** - każda strona i handler AJAX sprawdza sam.
|
||||
- **SOAP w dwóch rolach**: klient SYNDIS (`soap.php` + `soap_*.php`, pobieranie SCADA)
|
||||
oraz serwer SOAP (`webservice/`, wystawianie danych do SYNDIS).
|
||||
- **Zadania cykliczne**: Windows `.bat` + php-cli (`soap_update5min.php` co 5 min,
|
||||
`reports/rep_003_hours.bat` per oddział B/S/D).
|
||||
|
||||
## Bazy i sharding historii
|
||||
|
||||
Połączenie otwiera `softspm`, ale zapytania sięgają dwóch pozostałych przez
|
||||
**twardy prefiks w SQL** (~240 wystąpień `softspm_rep.`, ~90 `softspm_his.`).
|
||||
Wszystkie trzy muszą stać na jednym serwerze.
|
||||
|
||||
- `softspm` - dane operacyjne (w tym tabele `rep_*` bieżące).
|
||||
- `softspm_rep` - tabele raportowe (`rep_002_*` per miesiąc `styc…grud`, `rep_003_data`, `rep_015_*`).
|
||||
- `softspm_his` - archiwum: **shardy kwartalne** `rep_his_data_everyday_YYYY_qN`
|
||||
i `seg_dev_resources_det_YYYY_qN`, piezometry per rok.
|
||||
|
||||
Routing shardów: `getTableNameFrom_everyday_his()` / `getTableNameFrom_resources_det()`
|
||||
(`func.php`). Reguła: bieżący rok → tabela bazowa, ale po **180 dniach** przełącza się
|
||||
na wariant `_his`; starsze → shard `YYYY_qN`; data w **pierwszych 3 dniach miesiąca**
|
||||
jest cofana do poprzedniego miesiąca (więc początek stycznia trafia w Q4 poprzedniego
|
||||
roku); wszystko sprzed 2014 → `2014_q1`.
|
||||
|
||||
> W SoftSPM_2 tej logiki **jeszcze nie ma** - `WellOperationController` czyta tylko
|
||||
> bieżące `rep_his_data_everyday` z bazy `softspm`. Połączenia `softspm_rep`
|
||||
> i `softspm_his` są skonfigurowane, ale nieużywane. Przy migracji historii
|
||||
> trzeba odtworzyć reguły shardowania.
|
||||
|
||||
## Mapa sekcji legacy → SoftSPM_2
|
||||
|
||||
Górne menu legacy = sekcje; `config/navigation.php` odtwarza je 1:1.
|
||||
Status: ✅ = przepisane, ⬜ = `route => null` (placeholder w menu).
|
||||
|
||||
| Sekcja | site | Nazwa | Status w SoftSPM_2 |
|
||||
|---|---|---|---|
|
||||
| **Start (100)** | 100/101/102 | Start, Parametry studni 01/02 | ✅ `dashboard` (mapa = placeholder) |
|
||||
| **Katalogi (200)** | 203 | Katalog pomp | ✅ `pumps.*` (pełny CRUD) |
|
||||
| | 204 | Katalog silników | ✅ `engines.*` |
|
||||
| | 205 | Katalog agregatów | ✅ `aggregates.*` (lista/szczegóły/edycja) |
|
||||
| | 201 | Wybór agregatu pompowego | ✅ `aggregate-selection.index` |
|
||||
| | 206 | Katalog kabli | ✅ `cables.*` |
|
||||
| | 207 | Katalog orurowania i armatury | ✅ `piping.*` |
|
||||
| | 208 | Katalog opomiarowania | ✅ `metering.*` |
|
||||
| | 202 | Katalog piezometrów | ⬜ **brak w `navigation.php`** |
|
||||
| **Stan (300)** | 303,304,301,302,309,306,307,308 | Stan pomp/silników/studni/zbiorników/piezometrów/kabli/orurowania/opomiarowania | ⬜ wszystkie |
|
||||
| | 310 | (bez etykiety w menu; 1204 linie - największy plik `sites/`) | ⬜ |
|
||||
| **Eksploatacja (400)** | 401 | Eksploatacja studni | ✅ `well-operation.*` (lista + karta, etap 1) |
|
||||
| | 402,404,405,406,403,407 | zbiorniki, piezometry, ujęcia, kreator doboru pompy, parametry stałe, zrzut wody | ⬜ |
|
||||
| **Stacja prób (500)** | 501-504 | Pompy, Silniki, Agregaty, Kable | ⬜ |
|
||||
| **Remonty (600)** | 601-603 | Demontaż, Weryfikacja, Jakość remontów | ⬜ |
|
||||
| **Gospodarka (700)** | 701-704 | Statystyki rezerw studni, rezerwy części nowych/regenerowanych, drutów i kabli | ⬜ |
|
||||
| **Administracja (800)** | 801-807, 809, 810 | użytkownicy, profile, baza wiedzy, producenci, słowniki, parametryzacja, raporty, zlecenia, zmiana hasła | ⬜ |
|
||||
| **Baza wiedzy (900)** | 900 | Baza wiedzy | ⬜ (sekcja bez pozycji) |
|
||||
|
||||
Ciężar pracy wg liczby plików `sites/`: **Stan (65)**, Administracja (42),
|
||||
Eksploatacja (39), Katalogi (27) - reszta po 1-4.
|
||||
|
||||
### Moduł „Studnie" (`wells.*`) - odklejony od menu
|
||||
|
||||
`routes/web.php` ma pełny CRUD studni (`wells.index/create/store/edit/update`),
|
||||
widoki istnieją, ale **`config/navigation.php` nigdzie go nie wskazuje** -
|
||||
pozycja „Stan studni" (site301) ma `route => null`. Moduł jest osiągalny wyłącznie
|
||||
przez wpisanie URL-a. Do decyzji: podpiąć pod „Stan studni" czy pod inną pozycję.
|
||||
|
||||
Pochodzenie: lista z `site401.php` (przepisana bez N+1), formularze z `site301A/E/inf`.
|
||||
`WellOperationController` bierze się z tej samej grupy `site401` - te dwa moduły
|
||||
łatwo pomylić.
|
||||
|
||||
## Pułapki przy czytaniu legacy
|
||||
|
||||
- **Duża część drzewa to martwy kod.** Przed edycją potwierdzić, że plik jest żywy:
|
||||
`sites/backup/` (23 pliki), kopie z datą w nazwie (`20161228_soap.php`,
|
||||
`20170130_soap.php`, `20210926_soap.php`), skrypty jednorazowe
|
||||
(`naprawa_20210312.php`, `repair_20210313_data.php`, `rollback.php`, `test*.php`),
|
||||
pięć wariantów tego samego importera `soap_of_while{,_back,_last,_manual,_new}.php`,
|
||||
klony raportów per miesiąc (`rep_002_wrze_print.php`).
|
||||
- **Ścieżki twardo wpisane w `.bat`** potrafią wskazywać na *inną kopię* aplikacji
|
||||
(`reports/rep_003_hours.bat` kieruje na `htdocs\trzeciTestowy\`) - sprawdzić,
|
||||
zanim się założy, że dany skrypt to ten działający.
|
||||
- **Etykiety UI**: `language/{polish,english,german}.php`, zmienne `$lab_*`.
|
||||
`polish.php` (45 kB) jest źródłem prawdy; `english.php`/`german.php` (po ~19 kB)
|
||||
mają braki. SoftSPM_2 ma `SetLocale`, ale **zero wywołań `__()`** - tłumaczenia
|
||||
to osobny, jeszcze nierozpoczęty etap.
|
||||
- **Środowisko legacy**: PHP 5.4/5.6 + usunięte `ext/mysql` (`mysql_connect`),
|
||||
dlatego uruchamia się je tylko starym XAMPP-em (`C:\Programy\xampp_old\php\php.exe`).
|
||||
Facet Eclipse w `.settings/` kłamie, że to PHP 8.2.
|
||||
Generated
+2180
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"axios": "^1.7.4",
|
||||
"laravel-vite-plugin": "^1.0.5",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_URL" value=""/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 3.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,245 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../**/*.blade.php';
|
||||
@source '../../app/**/*.php';
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
|
||||
/*
|
||||
| SoftSPM 2 - system "SoftSPM Industrial" (tokeny z uj_dashboard/DESIGN.md).
|
||||
| Corporate Modern + Glassmorphism, karty "Bento", przyciski-pigułki.
|
||||
| Tailwind v4: konfiguracja w CSS przez @theme.
|
||||
*/
|
||||
@theme {
|
||||
/* Fonty (rodziny) */
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
--font-headline-lg: 'Inter', system-ui, sans-serif;
|
||||
--font-headline-md: 'Inter', system-ui, sans-serif;
|
||||
--font-title-sm: 'Inter', system-ui, sans-serif;
|
||||
--font-body-md: 'Inter', system-ui, sans-serif;
|
||||
--font-body-sm: 'Inter', system-ui, sans-serif;
|
||||
--font-data-label: 'JetBrains Mono', monospace;
|
||||
|
||||
/* Typografia (rozmiary) */
|
||||
--text-headline-lg: 24px;
|
||||
--text-headline-lg--line-height: 32px;
|
||||
--text-headline-lg--font-weight: 700;
|
||||
--text-headline-md: 20px;
|
||||
--text-headline-md--line-height: 28px;
|
||||
--text-headline-md--font-weight: 600;
|
||||
--text-title-sm: 16px;
|
||||
--text-title-sm--line-height: 24px;
|
||||
--text-title-sm--font-weight: 600;
|
||||
--text-body-md: 14px;
|
||||
--text-body-md--line-height: 20px;
|
||||
--text-body-sm: 12px;
|
||||
--text-body-sm--line-height: 18px;
|
||||
--text-data-label: 12px;
|
||||
--text-data-label--line-height: 16px;
|
||||
--text-data-label--font-weight: 500;
|
||||
--text-data-label--letter-spacing: 0.02em;
|
||||
|
||||
/* Odstępy nazwane */
|
||||
--spacing-gutter: 1rem;
|
||||
--spacing-stack-gap: 0.75rem;
|
||||
--spacing-container-padding: 1rem;
|
||||
--spacing-card-inner-padding: 1rem;
|
||||
--spacing-touch-target-min: 44px;
|
||||
|
||||
/* Cień karty "Bento" */
|
||||
--shadow-card: 0 4px 12px rgb(0 0 0 / 0.08);
|
||||
|
||||
/* Kolory - Surface / neutralne */
|
||||
--color-surface: #f7fafb;
|
||||
--color-surface-bright: #f7fafb;
|
||||
--color-surface-dim: #d7dadc;
|
||||
--color-surface-container-lowest: #ffffff;
|
||||
--color-surface-container-low: #f1f4f5;
|
||||
--color-surface-container: #ebeeef;
|
||||
--color-surface-container-high: #e5e9ea;
|
||||
--color-surface-container-highest: #e0e3e4;
|
||||
--color-surface-variant: #e0e3e4;
|
||||
--color-surface-tint: #b72221;
|
||||
--color-on-surface: #181c1d;
|
||||
--color-on-surface-variant: #5b403d;
|
||||
--color-inverse-surface: #2d3132;
|
||||
--color-inverse-on-surface: #eef1f2;
|
||||
--color-outline: #8f706c;
|
||||
--color-outline-variant: #e3beba;
|
||||
--color-background: #f7fafb;
|
||||
--color-on-background: #181c1d;
|
||||
|
||||
/* Primary - ciemna czerwień przemysłowa */
|
||||
--color-primary: #7f0009;
|
||||
--color-on-primary: #ffffff;
|
||||
--color-primary-container: #a61317;
|
||||
--color-on-primary-container: #ffb4ac;
|
||||
--color-inverse-primary: #ffb4ab;
|
||||
--color-primary-fixed: #ffdad6;
|
||||
--color-primary-fixed-dim: #ffb4ab;
|
||||
--color-on-primary-fixed: #410002;
|
||||
--color-on-primary-fixed-variant: #93000d;
|
||||
|
||||
/* Secondary - teal */
|
||||
--color-secondary: #006876;
|
||||
--color-on-secondary: #ffffff;
|
||||
--color-secondary-container: #9fecfc;
|
||||
--color-on-secondary-container: #0d6d7b;
|
||||
--color-secondary-fixed: #a2efff;
|
||||
--color-secondary-fixed-dim: #85d2e2;
|
||||
--color-on-secondary-fixed: #001f25;
|
||||
--color-on-secondary-fixed-variant: #004e59;
|
||||
|
||||
/* Tertiary */
|
||||
--color-tertiary: #2f3e45;
|
||||
--color-on-tertiary: #ffffff;
|
||||
--color-tertiary-container: #46555d;
|
||||
--color-on-tertiary-container: #b9c9d3;
|
||||
--color-tertiary-fixed: #d5e5ef;
|
||||
--color-tertiary-fixed-dim: #b9c9d2;
|
||||
--color-on-tertiary-fixed: #0e1d24;
|
||||
--color-on-tertiary-fixed-variant: #3a4951;
|
||||
|
||||
/* Error / statusy */
|
||||
--color-error: #ba1a1a;
|
||||
--color-on-error: #ffffff;
|
||||
--color-error-container: #ffdad6;
|
||||
--color-on-error-container: #93000a;
|
||||
--color-success-active: #006876;
|
||||
--color-error-alert: #ba1a1a;
|
||||
}
|
||||
|
||||
/* Wariant 'dark' sterowany klasą na <html> (strategia class, nie media). */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* Kolory neutralne wykresów SVG (adaptują się do motywu). */
|
||||
:root {
|
||||
--chart-grid: #eceff0;
|
||||
--chart-axis: #9aa6a8;
|
||||
--chart-tick: #888888;
|
||||
--chart-label: #555555;
|
||||
--chart-raw: #1565c0;
|
||||
}
|
||||
|
||||
/*
|
||||
| ===== MOTYW CIEMNY =====
|
||||
| Nadpisujemy tylko WARTOŚCI tokenów - wszystkie utility (bg-surface,
|
||||
| text-on-surface, border-outline-variant...) używają var(--color-*), więc
|
||||
| cały interfejs przełącza się automatycznie. Paleta wg Material 3 (dark)
|
||||
| spójna z jasną (czerwień przemysłowa + teal). Tokeny *-fixed są wspólne
|
||||
| dla obu motywów - nie nadpisujemy ich.
|
||||
*/
|
||||
html.dark {
|
||||
/* Surface / neutralne */
|
||||
--color-surface: #0f1314;
|
||||
--color-surface-bright: #353a3b;
|
||||
--color-surface-dim: #0f1314;
|
||||
--color-surface-container-lowest: #090d0e;
|
||||
--color-surface-container-low: #181c1d;
|
||||
--color-surface-container: #1c2021;
|
||||
--color-surface-container-high: #262a2b;
|
||||
--color-surface-container-highest: #313536;
|
||||
--color-surface-variant: #414b4c;
|
||||
--color-surface-tint: #ffb4ab;
|
||||
--color-on-surface: #e0e3e4;
|
||||
--color-on-surface-variant: #c4c7c8;
|
||||
--color-inverse-surface: #e0e3e4;
|
||||
--color-inverse-on-surface: #2d3132;
|
||||
--color-outline: #899293;
|
||||
--color-outline-variant: #3f4849;
|
||||
--color-background: #0f1314;
|
||||
--color-on-background: #e0e3e4;
|
||||
|
||||
/* Primary - jaśniejsza czerwień na ciemnym tle */
|
||||
--color-primary: #ffb4ab;
|
||||
--color-on-primary: #690005;
|
||||
--color-primary-container: #93000d;
|
||||
--color-on-primary-container: #ffdad6;
|
||||
--color-inverse-primary: #a61317;
|
||||
|
||||
/* Secondary - jaśniejszy teal */
|
||||
--color-secondary: #85d2e2;
|
||||
--color-on-secondary: #00363f;
|
||||
--color-secondary-container: #004e59;
|
||||
--color-on-secondary-container: #a2efff;
|
||||
|
||||
/* Tertiary */
|
||||
--color-tertiary: #b9c9d3;
|
||||
--color-on-tertiary: #243139;
|
||||
--color-tertiary-container: #3a4951;
|
||||
--color-on-tertiary-container: #d5e5ef;
|
||||
|
||||
/* Error / statusy */
|
||||
--color-error: #ffb4ab;
|
||||
--color-on-error: #690005;
|
||||
--color-error-container: #93000a;
|
||||
--color-on-error-container: #ffdad6;
|
||||
--color-success-active: #85d2e2;
|
||||
--color-error-alert: #ffb4ab;
|
||||
|
||||
/* Wykresy */
|
||||
--chart-grid: #2a2f30;
|
||||
--chart-axis: #566061;
|
||||
--chart-tick: #8a9293;
|
||||
--chart-label: #9aa3a4;
|
||||
--chart-raw: #7fb3ff;
|
||||
}
|
||||
|
||||
/* ===== Fonty self-hosted (air-gap: pliki w public/fonts/) ===== */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('/fonts/inter/Inter-Variable.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/jetbrains-mono/JetBrainsMono-Variable.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-style: normal;
|
||||
font-weight: 100 700;
|
||||
font-display: block;
|
||||
src: url('/fonts/material-symbols/MaterialSymbolsOutlined.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* ===== Komponenty ===== */
|
||||
.material-symbols-outlined {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
|
||||
/* Glassmorfizm - szklane panele/paski (mapa, wyszukiwarka) */
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(227, 190, 186, 0.3);
|
||||
}
|
||||
html.dark .glass-panel {
|
||||
background: rgba(24, 28, 29, 0.7);
|
||||
border-color: rgba(137, 146, 147, 0.25);
|
||||
}
|
||||
|
||||
/* Pill-chipy statusów (OK / Warning / Alert) */
|
||||
.status-chip {
|
||||
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded-full font-data-label text-data-label;
|
||||
}
|
||||
.status-chip-ok { @apply bg-green-100 text-green-800; }
|
||||
.status-chip-warn { @apply bg-amber-100 text-amber-800; }
|
||||
.status-chip-alert { @apply bg-error-container text-on-error-container; }
|
||||
html.dark .status-chip-ok { @apply bg-green-900/40 text-green-300; }
|
||||
html.dark .status-chip-warn { @apply bg-amber-900/40 text-amber-300; }
|
||||
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* SoftSPM_2 - punkt wejścia JS.
|
||||
*
|
||||
* Aplikacja jest server-side (Blade); interakcje ekranowe siedzą w inline
|
||||
* <script> w widokach. Zasada air-gap: ZERO CDN - wszystko przez Vite.
|
||||
*/
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
@@ -0,0 +1,419 @@
|
||||
<x-guest-layout title="SoftSPM 2 - Logowanie">
|
||||
|
||||
<style>
|
||||
.login-screen { min-height: 100vh; display: flex; }
|
||||
|
||||
/* ===== Lewa kolumna - panel wizualny ===== */
|
||||
.login-visual {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
/* Gradient turkus -> czerwień (jak hero na stronie głównej SoftSPM) */
|
||||
background:
|
||||
radial-gradient(circle at 86% 10%, rgba(255,255,255,.10), transparent 38%),
|
||||
radial-gradient(circle at 8% 92%, rgba(255,255,255,.07), transparent 44%),
|
||||
radial-gradient(120% 95% at 102% 102%, rgba(140,22,38,.50), transparent 58%),
|
||||
linear-gradient(118deg, #0a5366 0%, #094a5a 33%, #33263a 64%, #5e1f2c 88%, #6e1a26 100%);
|
||||
}
|
||||
.login-dots {
|
||||
position: absolute; inset: 0; z-index: 0;
|
||||
background-image: radial-gradient(rgba(255,255,255,.06) 1px, transparent 1.4px);
|
||||
background-size: 24px 24px;
|
||||
-webkit-mask-image: linear-gradient(to bottom, #000 28%, transparent 92%);
|
||||
mask-image: linear-gradient(to bottom, #000 28%, transparent 92%);
|
||||
}
|
||||
/* Okręgi w tle - bardziej wyraziste */
|
||||
.login-ring, .login-ring2 { position: absolute; z-index: 0; border-radius: 50%; }
|
||||
.login-ring { top: -180px; right: -150px; width: 470px; height: 470px; border: 1.5px solid rgba(255,255,255,.16); }
|
||||
.login-ring2 { top: -90px; right: -60px; width: 310px; height: 310px; border: 1px solid rgba(255,255,255,.11); }
|
||||
.login-ring3 { position: absolute; z-index: 0; bottom: -150px; left: -120px; width: 360px; height: 360px; border-radius: 50%; border: 1.5px solid rgba(255,255,255,.10); }
|
||||
.login-mesh { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
|
||||
.login-brand { position: relative; z-index: 2; padding: 2.75rem 3rem; max-width: min(1200px, 90%); }
|
||||
.login-brand .over {
|
||||
color: #4fc3e8; font-family: 'JetBrains Mono', monospace; font-size: 12px;
|
||||
letter-spacing: .18em; text-transform: uppercase; font-weight: 600;
|
||||
}
|
||||
.login-brand h1 {
|
||||
color: #fff; font-weight: 700; line-height: 1.18; margin-top: .9rem;
|
||||
font-size: clamp(26px, 3.2vw, 44px);
|
||||
text-shadow: 0 2px 24px rgba(0,0,0,.25);
|
||||
}
|
||||
|
||||
/* ===== Prawa kolumna - formularz ===== */
|
||||
.login-form-panel {
|
||||
width: 50%; display: flex; align-items: center; justify-content: center;
|
||||
padding: 2rem; background: #f7fafb;
|
||||
}
|
||||
|
||||
/* ===== Responsywnie: < 860px pionowo ===== */
|
||||
@media (max-width: 860px) {
|
||||
.login-screen { flex-direction: column; }
|
||||
.login-visual { width: 100%; height: 200px; }
|
||||
.login-brand { padding: 1.25rem 1.5rem; }
|
||||
.login-form-panel { width: 100%; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.login-mesh { opacity: .9; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="login-screen">
|
||||
|
||||
{{-- ===== LEWA: wizual + animacja ===== --}}
|
||||
<div class="login-visual">
|
||||
<div class="login-dots"></div>
|
||||
<div class="login-ring"></div>
|
||||
<div class="login-ring2"></div>
|
||||
<div class="login-ring3"></div>
|
||||
<canvas id="login-mesh" class="login-mesh" aria-hidden="true" data-mode="text" data-text="SoftSPM"></canvas>
|
||||
|
||||
<div class="login-brand">
|
||||
<p class="over">Submersible Pumps Management</p>
|
||||
<h1>Inteligentny Nadzór Eksploatacji Pomp Głębinowych i Raportowanie</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ===== PRAWA: formularz ===== --}}
|
||||
<div class="login-form-panel">
|
||||
<div class="w-full" style="max-width: 360px;">
|
||||
|
||||
{{-- Logo - link do softspm.com (ta sama zakładka) --}}
|
||||
<div class="mb-7">
|
||||
<a href="https://softspm.com" class="inline-block">
|
||||
<img src="{{ asset('images/logo.png') }}" alt="SoftSPM" style="width: 220px; max-width: 100%;" class="h-auto">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 class="font-headline-md text-headline-md text-on-surface">Logowanie</h2>
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1 mb-6">Zaloguj się do systemu SoftSPM</p>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="status-chip-alert px-3 py-2 rounded-lg font-body-sm text-body-sm mb-4">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('login.attempt') }}" class="space-y-4" style="padding: 0;">
|
||||
@csrf
|
||||
|
||||
{{-- Nazwa użytkownika --}}
|
||||
<div class="space-y-1">
|
||||
<label for="username" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Nazwa użytkownika</label>
|
||||
<div class="relative">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-on-surface-variant/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
<input id="username" name="username" type="text" required autofocus autocomplete="username"
|
||||
value="{{ old('username') }}" placeholder="Podaj nazwę użytkownika"
|
||||
class="w-full bg-white border border-outline-variant pl-10 pr-3 py-3 rounded-lg focus:ring-1 focus:ring-secondary focus:border-secondary font-body-md text-on-surface transition-all">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Hasło --}}
|
||||
<div class="space-y-1">
|
||||
<label for="password" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Hasło</label>
|
||||
<div class="relative">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-on-surface-variant/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
<input id="password" name="password" type="password" required autocomplete="current-password"
|
||||
placeholder="••••••••"
|
||||
class="w-full bg-white border border-outline-variant pl-10 pr-3 py-3 rounded-lg focus:ring-1 focus:ring-secondary focus:border-secondary font-body-md text-on-surface transition-all">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Oddział - ukryty na czas testów (config softspm.login_department); odwracalne (ustaw null) --}}
|
||||
@unless (config('softspm.login_department'))
|
||||
<div class="space-y-1">
|
||||
<label for="department" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Oddział</label>
|
||||
<div class="relative">
|
||||
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-on-surface-variant/60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 21h18"/><path d="M5 21V7l8-4v18"/><path d="M19 21V11l-6-4"/><path d="M9 9v.01M9 12v.01M9 15v.01M9 18v.01"/>
|
||||
</svg>
|
||||
<select id="department" name="department" required
|
||||
class="w-full bg-white border border-outline-variant pl-10 pr-3 py-3 rounded-lg focus:ring-1 focus:ring-secondary focus:border-secondary font-body-md text-on-surface transition-all">
|
||||
@foreach (config('softspm.departments') as $dep)
|
||||
<option value="{{ $dep }}" {{ old('department') === $dep ? 'selected' : '' }}>{{ $dep }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@endunless
|
||||
|
||||
<x-btn type="submit" variant="primary" class="w-full mt-2">Zaloguj</x-btn>
|
||||
</form>
|
||||
|
||||
<p class="mt-8 text-center font-body-sm text-body-sm text-on-surface-variant/70">
|
||||
© SoftSPM {{ date('Y') }} · v.2.0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ===== Animacja "flowing wave mesh" (Canvas 2D, bez bibliotek) ===== --}}
|
||||
<script>
|
||||
(function () {
|
||||
const canvas = document.getElementById('login-mesh');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d', { alpha: true });
|
||||
|
||||
// ===== CONFIG (wartości referencyjne; data-mode/data-text nadpisują) =====
|
||||
const CONFIG = {
|
||||
mode: canvas.dataset.mode || 'text',
|
||||
text: canvas.dataset.text || 'SoftSPM',
|
||||
grid: 7,
|
||||
alphaThreshold: 120,
|
||||
fontWeight: 700,
|
||||
textHeightRatio: 0.17,
|
||||
textAnchor: { x: 0.07, y: 0.90 },
|
||||
amplitude: 1.7,
|
||||
speed: 0.0016,
|
||||
color: { r: 41, g: 171, b: 226 },
|
||||
opacity: 0.85,
|
||||
glow: 8,
|
||||
lineWidth: 1,
|
||||
nodeRadius: 1.15,
|
||||
drawNodes: true,
|
||||
edgeFalloff: 0.55,
|
||||
buckets: 6,
|
||||
// tryb wave
|
||||
bandCenter: 0.72,
|
||||
bandWidth: 0.40,
|
||||
waveCols: 90,
|
||||
waveRows: 13,
|
||||
connectorEvery: 3,
|
||||
waves: [
|
||||
{ amp: 14, freq: 1.1, speed: 0.55, phase: 0.0 },
|
||||
{ amp: 8, freq: 2.3, speed: -0.9, phase: 1.7 },
|
||||
{ amp: 5, freq: 3.7, speed: 1.35, phase: 3.1 },
|
||||
],
|
||||
};
|
||||
|
||||
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const rgba = (a) => 'rgba(' + CONFIG.color.r + ',' + CONFIG.color.g + ',' + CONFIG.color.b + ',' + a + ')';
|
||||
|
||||
let dpr = 1, cssW = 0, cssH = 0;
|
||||
let nodes = [], edges = [], spotX = 0;
|
||||
let particles = [];
|
||||
let rafId = null, resizeTimer = null, start = 0;
|
||||
|
||||
function setSize() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
cssW = Math.max(1, Math.round(rect.width));
|
||||
cssH = Math.max(1, Math.round(rect.height));
|
||||
dpr = Math.min(window.devicePixelRatio || 1, 2); // ostre linie na HiDPI
|
||||
canvas.width = Math.round(cssW * dpr);
|
||||
canvas.height = Math.round(cssH * dpr);
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
|
||||
// ===== Konstelacja tła (dryfujące punkty + linie - jak hero strony głównej) =====
|
||||
function makeParticles() {
|
||||
const count = Math.round(cssW * cssH / 15000);
|
||||
particles = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
particles.push({
|
||||
x: Math.random() * cssW, y: Math.random() * cssH,
|
||||
vx: (Math.random() - 0.5) * 0.22, vy: (Math.random() - 0.5) * 0.22,
|
||||
});
|
||||
}
|
||||
}
|
||||
function updateParticles() {
|
||||
for (const p of particles) {
|
||||
p.x += p.vx; p.y += p.vy;
|
||||
if (p.x < 0 || p.x > cssW) p.vx = -p.vx;
|
||||
if (p.y < 0 || p.y > cssH) p.vy = -p.vy;
|
||||
}
|
||||
}
|
||||
function drawConstellation() {
|
||||
const maxD = 155, maxD2 = maxD * maxD;
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
const a = particles[i];
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const b = particles[j];
|
||||
const dx = a.x - b.x, dy = a.y - b.y, d2 = dx * dx + dy * dy;
|
||||
if (d2 < maxD2) {
|
||||
ctx.strokeStyle = 'rgba(180,214,228,' + ((1 - Math.sqrt(d2) / maxD) * 0.16) + ')';
|
||||
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.fillStyle = 'rgba(205,230,240,.32)';
|
||||
ctx.beginPath();
|
||||
for (const p of particles) { ctx.moveTo(p.x + 1.3, p.y); ctx.arc(p.x, p.y, 1.3, 0, Math.PI * 2); }
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// ===== Budowa modelu z napisu =====
|
||||
function buildText() {
|
||||
const off = document.createElement('canvas');
|
||||
off.width = cssW; off.height = cssH;
|
||||
const o = off.getContext('2d');
|
||||
const fontSize = Math.max(12, Math.round(cssH * CONFIG.textHeightRatio));
|
||||
o.clearRect(0, 0, cssW, cssH);
|
||||
o.fillStyle = '#fff';
|
||||
o.textBaseline = 'alphabetic';
|
||||
o.textAlign = 'left';
|
||||
o.font = CONFIG.fontWeight + ' ' + fontSize + "px 'Inter', system-ui, sans-serif";
|
||||
o.fillText(CONFIG.text, cssW * CONFIG.textAnchor.x, cssH * CONFIG.textAnchor.y);
|
||||
const data = o.getImageData(0, 0, cssW, cssH).data;
|
||||
|
||||
nodes = []; edges = [];
|
||||
const map = new Map();
|
||||
const g = CONFIG.grid;
|
||||
let minX = 1e9, maxX = -1e9;
|
||||
for (let y = 0; y < cssH; y += g) {
|
||||
const row = (y / g) | 0;
|
||||
for (let x = 0; x < cssW; x += g) {
|
||||
if (data[(y * cssW + x) * 4 + 3] >= CONFIG.alphaThreshold) {
|
||||
const col = (x / g) | 0;
|
||||
map.set(col + '_' + row, nodes.length);
|
||||
nodes.push({ bx: x, by: y, x: x, y: y, col: col, row: row, ph: col * 0.7 + row * 0.9 });
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
buildEdges(map, true);
|
||||
spotX = nodes.length ? (minX + maxX) / 2 : cssW * 0.3;
|
||||
}
|
||||
|
||||
// ===== Budowa modelu pasma fal =====
|
||||
function buildWave() {
|
||||
nodes = []; edges = [];
|
||||
const map = new Map();
|
||||
const cols = CONFIG.waveCols, rows = CONFIG.waveRows;
|
||||
const bandTop = cssH * (CONFIG.bandCenter - CONFIG.bandWidth / 2);
|
||||
const bandH = cssH * CONFIG.bandWidth;
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const x = cols > 1 ? (c / (cols - 1)) * cssW : 0;
|
||||
const y = rows > 1 ? bandTop + (r / (rows - 1)) * bandH : bandTop;
|
||||
map.set(c + '_' + r, nodes.length);
|
||||
nodes.push({ bx: x, by: y, x: x, y: y, col: c, row: r, ph: c * 0.3 + r * 0.5 });
|
||||
}
|
||||
}
|
||||
buildEdges(map, false);
|
||||
spotX = cssW * 0.5;
|
||||
}
|
||||
|
||||
function buildEdges(map, triangular) {
|
||||
for (const n of nodes) {
|
||||
const i = map.get(n.col + '_' + n.row);
|
||||
const right = map.get((n.col + 1) + '_' + n.row);
|
||||
if (right !== undefined) edges.push([i, right]);
|
||||
if (triangular) {
|
||||
const down = map.get(n.col + '_' + (n.row + 1));
|
||||
const diag = map.get((n.col + 1) + '_' + (n.row + 1));
|
||||
if (down !== undefined) edges.push([i, down]);
|
||||
if (diag !== undefined) edges.push([i, diag]);
|
||||
} else if (n.col % CONFIG.connectorEvery === 0) {
|
||||
const down = map.get(n.col + '_' + (n.row + 1));
|
||||
if (down !== undefined) edges.push([i, down]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function build() {
|
||||
if (CONFIG.mode === 'wave') buildWave(); else buildText();
|
||||
}
|
||||
|
||||
// ===== Ruch węzłów =====
|
||||
function updateNodes(t) {
|
||||
if (CONFIG.mode === 'wave') {
|
||||
for (const n of nodes) {
|
||||
let dy = 0;
|
||||
for (const w of CONFIG.waves) {
|
||||
dy += w.amp * Math.sin((n.col / CONFIG.waveCols) * Math.PI * 2 * w.freq + t * w.speed + w.phase + n.row * 0.1);
|
||||
}
|
||||
n.x = n.bx;
|
||||
n.y = n.by + dy;
|
||||
}
|
||||
} else {
|
||||
const A = CONFIG.amplitude;
|
||||
for (const n of nodes) {
|
||||
n.x = n.bx + A * Math.sin(t + n.ph);
|
||||
n.y = n.by + A * Math.cos(t * 1.1 + n.ph * 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Rysowanie (kwantyzacja jasności do kubełków - 1 stroke/kubełek) =====
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, cssW, cssH);
|
||||
ctx.shadowBlur = 0;
|
||||
drawConstellation(); // tło konstelacji (bez poświaty)
|
||||
const B = CONFIG.buckets;
|
||||
const bins = [];
|
||||
for (let i = 0; i < B; i++) bins.push([]);
|
||||
|
||||
for (const e of edges) {
|
||||
const a = nodes[e[0]], b = nodes[e[1]];
|
||||
const nx = Math.abs((a.x + b.x) / 2 - spotX) / cssW; // odległość poziomą od reflektora
|
||||
let f = 1 - CONFIG.edgeFalloff * (2 * nx) * (2 * nx); // reflektor
|
||||
if (f < 0.05) f = 0.05; else if (f > 1) f = 1;
|
||||
bins[Math.min(B - 1, Math.floor((1 - f) * B))].push(e);
|
||||
}
|
||||
|
||||
ctx.lineWidth = CONFIG.lineWidth;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.shadowColor = rgba(CONFIG.opacity);
|
||||
ctx.shadowBlur = CONFIG.glow;
|
||||
|
||||
for (let bi = 0; bi < B; bi++) {
|
||||
const list = bins[bi];
|
||||
if (!list.length) continue;
|
||||
ctx.strokeStyle = rgba(CONFIG.opacity * (1 - (bi + 0.5) / B));
|
||||
ctx.beginPath();
|
||||
for (const e of list) {
|
||||
const a = nodes[e[0]], b = nodes[e[1]];
|
||||
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (CONFIG.drawNodes) {
|
||||
ctx.shadowBlur = CONFIG.glow * 0.6;
|
||||
ctx.fillStyle = rgba(CONFIG.opacity);
|
||||
ctx.beginPath();
|
||||
const r = CONFIG.nodeRadius;
|
||||
for (const n of nodes) {
|
||||
ctx.moveTo(n.x + r, n.y);
|
||||
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
||||
}
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
function loop(now) {
|
||||
updateNodes((now - start) * CONFIG.speed);
|
||||
updateParticles();
|
||||
draw();
|
||||
rafId = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
|
||||
setSize();
|
||||
makeParticles();
|
||||
build();
|
||||
if (reduce) { updateNodes(0); draw(); return; } // statyczny render
|
||||
start = performance.now();
|
||||
rafId = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(init, 150);
|
||||
}
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
// Czekaj na fonty (czytelny napis), potem buduj model.
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready.then(init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</x-guest-layout>
|
||||
@@ -0,0 +1,90 @@
|
||||
<x-app-layout :active="'aggregate-selection.index'" title="SoftSPM 2 - Wybór agregatu pompowego">
|
||||
@php $fmt = fn ($v, $d = 2) => $v === null || $v === '' ? '-' : number_format(round((float) $v, $d), $d, ',', ' '); @endphp
|
||||
|
||||
<div class="space-y-stack-gap">
|
||||
|
||||
<div>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface">Wybór agregatu pompowego</h1>
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1">Wysokość podnoszenia dla posiadanych typów pomp</p>
|
||||
</div>
|
||||
|
||||
{{-- Parametry doboru --}}
|
||||
<form method="GET" action="{{ route('aggregate-selection.index') }}"
|
||||
class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding">
|
||||
<p class="font-data-label text-data-label text-on-surface-variant tracking-wide mb-3">Wprowadź parametry poszukiwanej pompy</p>
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div class="space-y-1">
|
||||
<label for="q" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Zadana wydajność Q<sub>sz</sub> [m³/h]</label>
|
||||
<input id="q" name="q" type="text" inputmode="decimal" required value="{{ $q }}" placeholder="np. 120"
|
||||
class="bg-surface-container-low border border-outline-variant px-3 py-2 rounded font-data-label text-on-surface focus:ring-1 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="h" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Zadana wysokość podnoszenia H [m] <span class="normal-case text-on-surface-variant/60">(opcjonalnie)</span></label>
|
||||
<input id="h" name="h" type="text" inputmode="decimal" value="{{ $h }}" placeholder="np. 80"
|
||||
class="bg-surface-container-low border border-outline-variant px-3 py-2 rounded font-data-label text-on-surface focus:ring-1 focus:ring-primary focus:border-primary">
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="bg-secondary text-on-secondary px-5 py-2.5 rounded-full font-title-sm text-title-sm hover:opacity-90 transition-all active:scale-95">
|
||||
Szukaj
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if ($searched)
|
||||
{{-- Wykres nałożonych krzywych H --}}
|
||||
@if ($chart)
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding">
|
||||
<h2 class="font-title-sm text-title-sm text-tertiary mb-2">Wysokość podnoszenia dla posiadanych typów pomp</h2>
|
||||
<x-selection-chart :chart="$chart" :q="$qVal" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Lista pomp dla zadanego Q (ranking po sprawności) --}}
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card overflow-hidden">
|
||||
<div class="px-card-inner-padding pt-card-inner-padding">
|
||||
<h2 class="font-title-sm text-title-sm text-tertiary">
|
||||
Lista pomp dla zadanego Q
|
||||
<span class="font-body-sm text-body-sm text-on-surface-variant">(malejąco wg sprawności)</span>
|
||||
</h2>
|
||||
</div>
|
||||
<table class="w-full text-left mt-2">
|
||||
<thead class="bg-secondary/10 border-y border-outline-variant/40">
|
||||
<tr class="font-data-label text-data-label text-on-surface-variant tracking-wide">
|
||||
<th class="px-4 py-3 w-12">Lp.</th>
|
||||
<th class="px-4 py-3">Typ pompy</th>
|
||||
<th class="px-4 py-3">Typ silnika</th>
|
||||
<th class="px-4 py-3 text-right">Q<sub>n</sub> [m³/h]</th>
|
||||
<th class="px-4 py-3 text-right">Q<sub>sz</sub> [m³/h]</th>
|
||||
<th class="px-4 py-3 text-right">H(Q<sub>sz</sub>) [m]</th>
|
||||
<th class="px-4 py-3 text-right">η(Q<sub>sz</sub>) [-]</th>
|
||||
<th class="px-4 py-3 text-right">P<sub>s</sub> [kW]</th>
|
||||
<th class="px-4 py-3 text-right">Napięcie [V]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline-variant/30 font-body-md text-body-md">
|
||||
@forelse ($rows as $row)
|
||||
<tr class="hover:bg-surface-container transition-colors cursor-pointer"
|
||||
onclick="location.href='{{ route('aggregates.show', ['pump' => $row->pump_id] + ($row->engine_id ? ['engine' => $row->engine_id] : [])) }}'">
|
||||
<td class="px-4 py-3 font-data-label text-on-surface-variant">{{ $row->lp }}.</td>
|
||||
<td class="px-4 py-3 font-semibold text-on-surface">{{ $row->pump_name }}</td>
|
||||
<td class="px-4 py-3">{{ $row->engine_name ?? '-' }}</td>
|
||||
<td class="px-4 py-3 text-right font-data-label">{{ $fmt($row->q_n, 3) }}</td>
|
||||
<td class="px-4 py-3 text-right font-data-label">{{ $fmt($qVal, 3) }}</td>
|
||||
<td class="px-4 py-3 text-right font-data-label">{{ $fmt($row->h_val, 2) }}</td>
|
||||
<td class="px-4 py-3 text-right font-data-label">{{ $fmt($row->eta_val, 2) }}</td>
|
||||
<td class="px-4 py-3 text-right font-data-label">{{ $fmt($row->power, 2) }}</td>
|
||||
<td class="px-4 py-3 text-right font-data-label">{{ $row->voltage ?? '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="px-4 py-8 text-center font-body-md text-body-md text-on-surface-variant">
|
||||
Brak pomp spełniających wprowadzone parametry.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,46 @@
|
||||
<x-app-layout :active="'aggregates.index'" title="SoftSPM 2 - Przypisanie silników">
|
||||
<div class="max-w-3xl space-y-stack-gap">
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('aggregates.show', $pump) }}" class="font-body-sm text-body-sm text-on-surface-variant hover:text-primary">← Wróć</a>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface">Silniki dla pompy: {{ $pump->PUP_NAME }}</h1>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('aggregates.update', $pump) }}"
|
||||
class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card overflow-hidden">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
<table class="w-full text-left">
|
||||
<thead class="bg-secondary/10 border-b border-outline-variant/40">
|
||||
<tr class="font-data-label text-data-label text-on-surface-variant tracking-wide">
|
||||
<th class="px-4 py-2 w-12"></th>
|
||||
<th class="px-4 py-2">Typ silnika</th>
|
||||
<th class="px-4 py-2 text-right">Napięcie [V]</th>
|
||||
<th class="px-4 py-2 text-right">Moc [kW]</th>
|
||||
<th class="px-4 py-2">Producent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline-variant/30 font-body-md text-body-md">
|
||||
@foreach ($allEngines as $e)
|
||||
<tr class="hover:bg-surface-container transition-colors">
|
||||
<td class="px-4 py-2">
|
||||
<input type="checkbox" name="engines[]" value="{{ $e->ENG_ID }}"
|
||||
{{ in_array((int) $e->ENG_ID, $assigned, true) ? 'checked' : '' }}
|
||||
class="w-4 h-4 accent-secondary">
|
||||
</td>
|
||||
<td class="px-4 py-2 font-semibold text-on-surface">{{ $e->ENG_NAME }}</td>
|
||||
<td class="px-4 py-2 text-right font-data-label">{{ $e->ENG_VOLTAGE }}</td>
|
||||
<td class="px-4 py-2 text-right font-data-label">{{ $e->ENG_POWER }}</td>
|
||||
<td class="px-4 py-2">{{ $e->manufacturer?->MCF_NAME }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="flex items-center gap-2 p-card-inner-padding border-t border-outline-variant/40">
|
||||
<button type="submit" class="bg-primary text-on-primary px-6 py-2.5 rounded-full font-title-sm text-title-sm hover:bg-primary-container transition-all active:scale-95 shadow-card">Zapisz</button>
|
||||
<a href="{{ route('aggregates.show', $pump) }}" class="px-5 py-2.5 rounded-full font-title-sm text-title-sm text-on-surface-variant hover:bg-surface-container transition-all">Anuluj</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,79 @@
|
||||
<x-app-layout :active="'aggregates.index'" title="SoftSPM 2 - Katalog agregatow">
|
||||
@php $fmt = fn ($v, $d = 3) => $v === null ? '-' : number_format(round((float) $v, $d), $d, ',', ' '); @endphp
|
||||
|
||||
<div class="space-y-stack-gap">
|
||||
|
||||
@if (session('status'))
|
||||
<div class="status-chip-ok px-3 py-2 rounded-lg font-body-sm text-body-sm">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface">Katalog agregatow</h1>
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1">Wynikow: <span class="font-data-label">{{ $aggregates->total() }}</span></p>
|
||||
</div>
|
||||
|
||||
{{-- Wyszukiwarka pomp --}}
|
||||
<form method="GET" action="{{ route('aggregates.index') }}"
|
||||
class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding">
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div class="space-y-1">
|
||||
<label for="by" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Kryterium</label>
|
||||
<select id="by" name="by" class="bg-surface-container-low border border-outline-variant px-3 py-2 rounded font-data-label text-on-surface">
|
||||
<option value="name" {{ $by === 'name' ? 'selected' : '' }}>Typ pompy</option>
|
||||
<option value="flow" {{ $by === 'flow' ? 'selected' : '' }}>Wydajnosc Q</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="q" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Szukaj</label>
|
||||
<input id="q" name="q" type="text" value="{{ $q }}" placeholder="typ lub Q"
|
||||
class="bg-surface-container-low border border-outline-variant px-3 py-2 rounded font-data-label text-on-surface">
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="mcf" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Producent</label>
|
||||
<select id="mcf" name="mcf" class="bg-surface-container-low border border-outline-variant px-3 py-2 rounded font-data-label text-on-surface">
|
||||
<option value="{{ $mcfAll }}" {{ (int) $mcf === $mcfAll ? 'selected' : '' }}>Wszyscy</option>
|
||||
@foreach ($manufacturers as $id => $name)
|
||||
<option value="{{ $id }}" {{ (int) $mcf === (int) $id ? 'selected' : '' }}>{{ $name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="bg-secondary text-on-secondary px-5 py-2.5 rounded-full font-title-sm text-title-sm hover:opacity-90 active:scale-95 transition-all">Szukaj</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card overflow-hidden">
|
||||
<table class="w-full text-left">
|
||||
<thead class="bg-secondary/10 border-b border-outline-variant/40">
|
||||
<tr class="font-data-label text-data-label text-on-surface-variant tracking-wide">
|
||||
<th class="px-4 py-3 w-12">Lp.</th>
|
||||
<x-sortable-th column="name">Typ pompy</x-sortable-th>
|
||||
<x-sortable-th column="flow" align="right">Q<sub>n</sub> [m³/h]</x-sortable-th>
|
||||
<x-sortable-th column="mcf">Producent</x-sortable-th>
|
||||
<x-sortable-th column="engines" align="right">Silniki</x-sortable-th>
|
||||
<th class="px-4 py-3 text-right">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline-variant/30 font-body-md text-body-md">
|
||||
@forelse ($aggregates as $a)
|
||||
<tr class="hover:bg-surface-container transition-colors">
|
||||
<td class="px-4 py-3 font-data-label text-on-surface-variant">{{ $aggregates->firstItem() + $loop->index }}.</td>
|
||||
<td class="px-4 py-3 font-semibold text-on-surface">
|
||||
<a href="{{ route('aggregates.show', $a) }}" class="hover:text-primary">{{ $a->PUP_NAME }}</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right font-data-label">{{ $fmt($a->PUP_Q_N) }}</td>
|
||||
<td class="px-4 py-3">{{ $a->manufacturer?->MCF_NAME }}</td>
|
||||
<td class="px-4 py-3 text-right font-data-label">{{ $a->engines_count }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href="{{ route('aggregates.show', $a) }}" class="font-body-sm text-body-sm text-secondary hover:underline">Szczegoly</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="px-4 py-8 text-center font-body-md text-body-md text-on-surface-variant">Brak pomp dla wybranych kryteriow.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>{{ $aggregates->links() }}</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,96 @@
|
||||
<x-app-layout :active="'aggregates.index'" title="SoftSPM 2 - Agregat {{ $pump->PUP_NAME }}">
|
||||
@php $fmt = fn ($v, $d = 2) => $v === null ? '-' : number_format(round((float) $v, $d), $d, ',', ' '); @endphp
|
||||
|
||||
<div class="space-y-stack-gap">
|
||||
|
||||
@if (session('status'))
|
||||
<div class="status-chip-ok px-3 py-2 rounded-lg font-body-sm text-body-sm">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('aggregates.index') }}" class="font-body-sm text-body-sm text-on-surface-variant hover:text-primary">← Katalog agregatow</a>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface">{{ $pump->PUP_NAME }}</h1>
|
||||
</div>
|
||||
<a href="{{ route('aggregates.edit', $pump) }}"
|
||||
class="bg-secondary text-on-secondary px-4 py-2 rounded-full font-title-sm text-title-sm hover:opacity-90 active:scale-95 transition-all">Przypisz silniki</a>
|
||||
</div>
|
||||
|
||||
{{-- Wykresy --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-stack-gap">
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding">
|
||||
<h2 class="font-title-sm text-title-sm text-tertiary mb-2">Wysokosc podnoszenia</h2>
|
||||
@if ($hDrawable)
|
||||
<x-pump-curve :plot="$hPlot" x-label="Q [m³/h]" y-label="H [m]" />
|
||||
@else
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant py-6 text-center">Brak parametrow energetycznych dla pompy.</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding">
|
||||
<h2 class="font-title-sm text-title-sm text-tertiary mb-2">Sprawnosc</h2>
|
||||
@if ($etaDrawable)
|
||||
<x-pump-curve :plot="$etaPlot" x-label="Q [m³/h]" y-label="η [-]" />
|
||||
@else
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant py-6 text-center">Brak parametrow energetycznych dla pompy.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Silniki dla pompy (kolumny wg oryginalu: Typ silnika / Napiecie / Producent) --}}
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card overflow-hidden">
|
||||
<div class="px-card-inner-padding pt-card-inner-padding">
|
||||
<h2 class="font-title-sm text-title-sm text-tertiary">Lista silnikow dla pompy: {{ $pump->PUP_NAME }}</h2>
|
||||
</div>
|
||||
<table class="w-full text-left mt-2">
|
||||
<thead class="bg-secondary/10 border-y border-outline-variant/40">
|
||||
<tr class="font-data-label text-data-label text-on-surface-variant tracking-wide">
|
||||
<th class="px-4 py-2">Typ silnika</th>
|
||||
<th class="px-4 py-2 text-right">Napiecie [V]</th>
|
||||
<th class="px-4 py-2">Producent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline-variant/30 font-body-md text-body-md">
|
||||
@forelse ($engines as $e)
|
||||
<tr class="cursor-pointer transition-colors {{ $engine && $engine->ENG_ID === $e->ENG_ID ? 'bg-secondary-container/60' : 'hover:bg-surface-container' }}"
|
||||
onclick="location.href='{{ route('aggregates.show', ['pump' => $pump->PUP_ID, 'engine' => $e->ENG_ID]) }}'">
|
||||
<td class="px-4 py-2 font-semibold text-on-surface">{{ $e->ENG_NAME }}</td>
|
||||
<td class="px-4 py-2 text-right font-data-label">{{ $e->ENG_VOLTAGE }}</td>
|
||||
<td class="px-4 py-2">{{ $e->MCF_NAME }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="3" class="px-4 py-4 text-center font-body-sm text-on-surface-variant">Brak przypisanych silnikow - kliknij "Przypisz silniki".</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Karta zestawu pompa + silnik --}}
|
||||
@if ($engine)
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding">
|
||||
<h2 class="font-title-sm text-title-sm text-primary mb-3">{{ $pump->PUP_NAME }} + {{ $engine->ENG_NAME }}</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<a href="{{ route('pumps.show', $pump) }}" class="block group">
|
||||
<p class="font-data-label text-data-label text-on-surface-variant tracking-wide mb-2 group-hover:text-primary">Pompa →</p>
|
||||
<dl class="grid grid-cols-2 gap-y-1 font-body-md text-body-md">
|
||||
<dt class="text-on-surface-variant">Typ pompy</dt><dd class="font-semibold">{{ $pump->PUP_NAME }}</dd>
|
||||
<dt class="text-on-surface-variant">Producent</dt><dd>{{ $pump->manufacturer?->MCF_NAME ?? '-' }}</dd>
|
||||
<dt class="text-on-surface-variant">Q<sub>n</sub> [m³/h]</dt><dd class="font-data-label">{{ $fmt($pump->PUP_Q_N, 3) }}</dd>
|
||||
<dt class="text-on-surface-variant">H<sub>p</sub> [m]</dt><dd class="font-data-label">{{ $fmt($pump->PUP_H_P) }}</dd>
|
||||
<dt class="text-on-surface-variant">η<sub>max</sub> [-]</dt><dd class="font-data-label">{{ $fmt($pump->PUP_ETA_MAX) }}</dd>
|
||||
</dl>
|
||||
</a>
|
||||
<a href="{{ route('engines.show', $engine) }}" class="block group">
|
||||
<p class="font-data-label text-data-label text-on-surface-variant tracking-wide mb-2 group-hover:text-primary">Silnik →</p>
|
||||
<dl class="grid grid-cols-2 gap-y-1 font-body-md text-body-md">
|
||||
<dt class="text-on-surface-variant">Typ silnika</dt><dd class="font-semibold">{{ $engine->ENG_NAME }}</dd>
|
||||
<dt class="text-on-surface-variant">Producent</dt><dd>{{ $engine->manufacturer?->MCF_NAME ?? '-' }}</dd>
|
||||
<dt class="text-on-surface-variant">Napiecie [V]</dt><dd class="font-data-label">{{ $engine->ENG_VOLTAGE ?? '-' }}</dd>
|
||||
<dt class="text-on-surface-variant">Moc [kW]</dt><dd class="font-data-label">{{ $fmt($engine->ENG_POWER) }}</dd>
|
||||
<dt class="text-on-surface-variant">Sprawnosc η [-]</dt><dd class="font-data-label">{{ $fmt($engine->ENG_ETA) }}</dd>
|
||||
</dl>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,47 @@
|
||||
<x-app-layout :active="'cables.index'" title="SoftSPM 2 - Nowy typ kabla">
|
||||
<div class="max-w-2xl space-y-stack-gap">
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('cables.index') }}" class="font-body-sm text-body-sm text-on-surface-variant hover:text-primary">← Wróć</a>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface">Nowy typ kabla</h1>
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="status-chip-alert px-3 py-2 rounded-lg font-body-sm text-body-sm">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('cables.store') }}" enctype="multipart/form-data"
|
||||
class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding space-y-4">
|
||||
@csrf
|
||||
|
||||
<x-well-field name="name" label="Typ kabla" :value="old('name')" required />
|
||||
<x-well-select name="manufacturer_id" label="Producent" :options="$manufacturers" :selected="old('manufacturer_id')" required placeholder="- wybierz -" />
|
||||
|
||||
<div class="space-y-1">
|
||||
<label for="description" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Opis</label>
|
||||
<textarea id="description" name="description" rows="3"
|
||||
class="w-full bg-surface-container-low border border-outline-variant px-3 py-2 rounded focus:ring-1 focus:ring-primary focus:border-primary font-body-md text-on-surface">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label for="catalogue_file" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Dokumentacja katalogowa</label>
|
||||
<input id="catalogue_file" name="catalogue_file" type="file" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
|
||||
class="w-full font-body-sm text-body-sm text-on-surface file:mr-3 file:px-4 file:py-2 file:rounded-full file:border-0 file:bg-secondary file:text-on-secondary file:font-title-sm file:cursor-pointer hover:file:opacity-90">
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant/70">PDF lub Office, do 8 MB.</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="image_file" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Zdjęcie</label>
|
||||
<input id="image_file" name="image_file" type="file" accept="image/*"
|
||||
class="w-full font-body-sm text-body-sm text-on-surface file:mr-3 file:px-4 file:py-2 file:rounded-full file:border-0 file:bg-secondary file:text-on-secondary file:font-title-sm file:cursor-pointer hover:file:opacity-90">
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant/70">JPG/PNG, do 8 MB.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 pt-2">
|
||||
<button type="submit" class="bg-primary text-on-primary px-6 py-2.5 rounded-full font-title-sm text-title-sm hover:bg-primary-container transition-all active:scale-95 shadow-card">Zapisz</button>
|
||||
<a href="{{ route('cables.index') }}" class="px-5 py-2.5 rounded-full font-title-sm text-title-sm text-on-surface-variant hover:bg-surface-container transition-all">Anuluj</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,56 @@
|
||||
<x-app-layout :active="'cables.index'" title="SoftSPM 2 - Edycja: {{ $cable->EQU_NAME }}">
|
||||
<div class="max-w-2xl space-y-stack-gap">
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('cables.show', $cable) }}" class="font-body-sm text-body-sm text-on-surface-variant hover:text-primary">← Wróć</a>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface">Edycja typu kabla</h1>
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="status-chip-alert px-3 py-2 rounded-lg font-body-sm text-body-sm">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('cables.update', $cable) }}" enctype="multipart/form-data"
|
||||
class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding space-y-4">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<x-well-field name="name" label="Typ kabla" :value="old('name', $cable->EQU_NAME)" required />
|
||||
<x-well-select name="manufacturer_id" label="Producent" :options="$manufacturers" :selected="old('manufacturer_id', $cable->EQU_MCF_ID)" required placeholder="- wybierz -" />
|
||||
|
||||
<div class="space-y-1">
|
||||
<label for="description" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Opis</label>
|
||||
<textarea id="description" name="description" rows="3"
|
||||
class="w-full bg-surface-container-low border border-outline-variant px-3 py-2 rounded focus:ring-1 focus:ring-primary focus:border-primary font-body-md text-on-surface">{{ old('description', $cable->EQU_DESCRIPTION) }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label for="catalogue_file" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Dokumentacja katalogowa</label>
|
||||
<input id="catalogue_file" name="catalogue_file" type="file" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
|
||||
class="w-full font-body-sm text-body-sm text-on-surface file:mr-3 file:px-4 file:py-2 file:rounded-full file:border-0 file:bg-secondary file:text-on-secondary file:font-title-sm file:cursor-pointer hover:file:opacity-90">
|
||||
@if ($document)
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant/70">Obecny: <a href="{{ route('equipment.file', $document->EQF_ID) }}" target="_blank" class="text-secondary hover:underline">{{ $document->EQF_FILE_NAME }}</a>. Wgranie nowego zastąpi.</p>
|
||||
@else
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant/70">Brak. PDF lub Office, do 8 MB.</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="image_file" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Zdjęcie</label>
|
||||
<input id="image_file" name="image_file" type="file" accept="image/*"
|
||||
class="w-full font-body-sm text-body-sm text-on-surface file:mr-3 file:px-4 file:py-2 file:rounded-full file:border-0 file:bg-secondary file:text-on-secondary file:font-title-sm file:cursor-pointer hover:file:opacity-90">
|
||||
@if ($image)
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant/70">Obecne: <a href="{{ route('equipment.file', $image->EQF_ID) }}" target="_blank" class="text-secondary hover:underline">{{ $image->EQF_FILE_NAME }}</a>. Wgranie nowego zastąpi.</p>
|
||||
@else
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant/70">Brak. JPG/PNG, do 8 MB.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 pt-2">
|
||||
<button type="submit" class="bg-primary text-on-primary px-6 py-2.5 rounded-full font-title-sm text-title-sm hover:bg-primary-container transition-all active:scale-95 shadow-card">Zapisz zmiany</button>
|
||||
<a href="{{ route('cables.show', $cable) }}" class="px-5 py-2.5 rounded-full font-title-sm text-title-sm text-on-surface-variant hover:bg-surface-container transition-all">Anuluj</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,75 @@
|
||||
<x-app-layout :active="'cables.index'" title="SoftSPM 2 - Katalog kabli">
|
||||
<div class="space-y-stack-gap">
|
||||
|
||||
@if (session('status'))
|
||||
<div class="status-chip-ok px-3 py-2 rounded-lg font-body-sm text-body-sm">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-end justify-between">
|
||||
<div>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface">Katalog kabli</h1>
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1">Wyników: <span class="font-data-label">{{ $cables->total() }}</span></p>
|
||||
</div>
|
||||
<a href="{{ route('cables.create') }}"
|
||||
class="bg-primary text-on-primary px-5 py-2.5 rounded-full font-title-sm text-title-sm hover:bg-primary-container transition-all active:scale-95 shadow-card">
|
||||
Dodaj typ kabla
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Wyszukiwarka --}}
|
||||
<form method="GET" action="{{ route('cables.index') }}"
|
||||
class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding">
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div class="space-y-1">
|
||||
<label for="q" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Typ kabla</label>
|
||||
<input id="q" name="q" type="text" value="{{ $q }}" placeholder="nazwa / typ"
|
||||
class="bg-surface-container-low border border-outline-variant px-3 py-2 rounded font-data-label text-on-surface">
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label for="mcf" class="font-data-label text-data-label text-on-surface-variant tracking-wide">Producent</label>
|
||||
<select id="mcf" name="mcf" class="bg-surface-container-low border border-outline-variant px-3 py-2 rounded font-data-label text-on-surface">
|
||||
<option value="{{ $mcfAll }}" {{ (int) $mcf === $mcfAll ? 'selected' : '' }}>Wszyscy</option>
|
||||
@foreach ($manufacturers as $id => $name)
|
||||
<option value="{{ $id }}" {{ (int) $mcf === (int) $id ? 'selected' : '' }}>{{ $name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="bg-secondary text-on-secondary px-5 py-2.5 rounded-full font-title-sm text-title-sm hover:opacity-90 transition-all active:scale-95">Szukaj</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Tabela --}}
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card overflow-hidden">
|
||||
<table class="w-full text-left">
|
||||
<thead class="bg-secondary/10 border-b border-outline-variant/40">
|
||||
<tr class="font-data-label text-data-label text-on-surface-variant tracking-wide">
|
||||
<th class="px-4 py-3 w-12">Lp.</th>
|
||||
<x-sortable-th column="name">Typ kabla</x-sortable-th>
|
||||
<x-sortable-th column="mcf">Producent</x-sortable-th>
|
||||
<th class="px-4 py-3 text-right">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline-variant/30 font-body-md text-body-md">
|
||||
@forelse ($cables as $cable)
|
||||
<tr class="hover:bg-surface-container transition-colors">
|
||||
<td class="px-4 py-3 font-data-label text-on-surface-variant">{{ $cables->firstItem() + $loop->index }}.</td>
|
||||
<td class="px-4 py-3 font-semibold text-on-surface">
|
||||
<a href="{{ route('cables.show', $cable) }}" class="hover:text-primary">{{ $cable->EQU_NAME }}</a>
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ $cable->manufacturer?->MCF_NAME }}</td>
|
||||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||
<a href="{{ route('cables.show', $cable) }}" class="font-body-sm text-body-sm text-secondary hover:underline">Szczegóły</a>
|
||||
<span class="text-outline-variant mx-1">·</span>
|
||||
<a href="{{ route('cables.edit', $cable) }}" class="font-body-sm text-body-sm text-secondary hover:underline">Edytuj</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="px-4 py-8 text-center font-body-md text-body-md text-on-surface-variant">Brak kabli dla wybranych kryteriów.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>{{ $cables->links() }}</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,61 @@
|
||||
<x-app-layout :active="'cables.index'" title="SoftSPM 2 - {{ $cable->EQU_NAME }}">
|
||||
<div class="space-y-stack-gap">
|
||||
|
||||
@if (session('status'))
|
||||
<div class="status-chip-ok px-3 py-2 rounded-lg font-body-sm text-body-sm">{{ session('status') }}</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="status-chip-alert px-3 py-2 rounded-lg font-body-sm text-body-sm">{{ session('error') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('cables.index') }}" class="font-body-sm text-body-sm text-on-surface-variant hover:text-primary">← Katalog kabli</a>
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-surface">{{ $cable->EQU_NAME }}</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('cables.edit', $cable) }}"
|
||||
class="bg-secondary text-on-secondary px-4 py-2 rounded-full font-title-sm text-title-sm hover:opacity-90 transition-all active:scale-95">Edytuj</a>
|
||||
@unless ($inStock)
|
||||
<x-confirm-delete :action="route('cables.destroy', $cable)"
|
||||
message="Czy na pewno usunąć typ kabla „{{ $cable->EQU_NAME }}”?" />
|
||||
@endunless
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-stack-gap">
|
||||
{{-- Dane --}}
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding">
|
||||
<h2 class="font-title-sm text-title-sm text-tertiary mb-3">Dane</h2>
|
||||
<dl class="grid grid-cols-3 gap-x-4 gap-y-2 font-body-md text-body-md">
|
||||
<dt class="text-on-surface-variant">Typ kabla</dt><dd class="col-span-2 font-semibold">{{ $cable->EQU_NAME }}</dd>
|
||||
<dt class="text-on-surface-variant">Producent</dt><dd class="col-span-2">{{ $cable->manufacturer?->MCF_NAME ?? '-' }}</dd>
|
||||
<dt class="text-on-surface-variant">Opis</dt><dd class="col-span-2">{!! $cable->EQU_DESCRIPTION ? nl2br(e($cable->EQU_DESCRIPTION)) : '-' !!}</dd>
|
||||
|
||||
@if ($document)
|
||||
<dt class="text-on-surface-variant">Dokumentacja katalogowa</dt>
|
||||
<dd class="col-span-2">
|
||||
<a href="{{ route('equipment.file', $document->EQF_ID) }}" target="_blank"
|
||||
class="text-secondary hover:underline break-all">{{ $document->EQF_FILE_NAME }}</a>
|
||||
</dd>
|
||||
@endif
|
||||
</dl>
|
||||
|
||||
@if ($inStock)
|
||||
<p class="mt-4 font-body-sm text-body-sm text-on-surface-variant/80">Kabel jest na stanie magazynowym - usunięcie zablokowane.</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Zdjęcie --}}
|
||||
<div class="bg-surface-container-lowest border border-outline-variant/40 rounded-lg shadow-card p-card-inner-padding">
|
||||
<h2 class="font-title-sm text-title-sm text-tertiary mb-3">Zdjęcie</h2>
|
||||
@if ($image)
|
||||
<img src="{{ route('equipment.file', $image->EQF_ID) }}" alt="{{ $cable->EQU_NAME }}"
|
||||
class="max-w-full h-auto rounded border border-outline-variant/40">
|
||||
@else
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant py-6 text-center">Brak zdjęcia.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user