Initial commit
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
@props([
|
||||
'active' => null, // nazwa trasy aktywnej pozycji (np. 'wells.index')
|
||||
'section' => null, // klucz sekcji (gdy strona nie jest pozycją menu, np. landing sekcji)
|
||||
'title' => 'SoftSPM 2',
|
||||
])
|
||||
|
||||
@php
|
||||
$sections = config('navigation');
|
||||
|
||||
// Ustal aktywną sekcję: jawny prop > sekcja zawierająca aktywną pozycję > pierwsza.
|
||||
$activeSectionKey = $section;
|
||||
if (! $activeSectionKey && $active) {
|
||||
foreach ($sections as $s) {
|
||||
foreach ($s['items'] as $it) {
|
||||
if (($it['route'] ?? null) === $active) {
|
||||
$activeSectionKey = $s['key'];
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$activeSectionKey = $activeSectionKey ?? ($sections[0]['key'] ?? null);
|
||||
$activeSection = collect($sections)->firstWhere('key', $activeSectionKey);
|
||||
|
||||
// Motyw z cookie (serwer ustawia klasę przy renderze - brak migotania).
|
||||
// Czytamy SUROWE $_COOKIE - cookie 'theme' ustawia JS bez szyfrowania, więc
|
||||
// request()->cookie() (przez EncryptCookies) by go nie odczytał.
|
||||
$theme = ($_COOKIE['theme'] ?? null) === 'dark' ? 'dark' : 'light';
|
||||
|
||||
// Inicjały użytkownika do awatara w menu.
|
||||
$uname = auth()->user()?->USR_USERNAME ?? '?';
|
||||
$unameParts = preg_split('/[^\p{L}\p{N}]+/u', $uname, -1, PREG_SPLIT_NO_EMPTY) ?: [$uname];
|
||||
$initials = mb_strtoupper(count($unameParts) >= 2
|
||||
? mb_substr($unameParts[0], 0, 1).mb_substr($unameParts[1], 0, 1)
|
||||
: mb_substr($uname, 0, 2));
|
||||
|
||||
$languages = ['pl' => 'PL', 'en' => 'EN', 'de' => 'DE'];
|
||||
@endphp
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="{{ $theme }}" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ $title }}</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface min-h-screen">
|
||||
|
||||
{{-- ===== Górny pasek: SEKCJE (zakres) ===== --}}
|
||||
<header class="sticky top-0 z-30 bg-surface-container-lowest border-b border-outline-variant/40">
|
||||
<div class="flex items-center justify-between h-16 px-gutter">
|
||||
{{-- Logo + sekcje razem, wyrównane do lewej --}}
|
||||
<div class="flex items-center gap-10 min-w-0">
|
||||
<a href="{{ route('dashboard') }}" class="shrink-0">
|
||||
<img src="{{ asset('images/logo.png') }}" alt="SoftSPM 2" class="h-9 w-auto">
|
||||
</a>
|
||||
|
||||
<nav class="hidden md:flex items-center gap-6 font-body-md text-body-md overflow-x-auto">
|
||||
@foreach ($sections as $s)
|
||||
@php
|
||||
$landing = isset($s['route']) && Route::has($s['route'])
|
||||
? route($s['route'])
|
||||
: route('section', $s['key']);
|
||||
@endphp
|
||||
<a href="{{ $landing }}"
|
||||
class="whitespace-nowrap py-1 border-b-2 transition-colors hover:text-primary
|
||||
{{ $activeSectionKey === $s['key'] ? 'text-primary font-semibold border-primary' : 'text-on-surface border-transparent' }}">
|
||||
{{ $s['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
{{-- Przełącznik motywu jasny/ciemny --}}
|
||||
<button type="button" id="theme-toggle" title="Przełącz motyw jasny/ciemny" aria-label="Przełącz motyw jasny/ciemny"
|
||||
class="p-2 rounded-full text-on-surface-variant hover:bg-surface-container hover:text-primary transition-colors">
|
||||
{{-- W jasnym pokazuje księżyc (przejście w ciemny) --}}
|
||||
<svg class="w-5 h-5 dark:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9z"/>
|
||||
</svg>
|
||||
{{-- W ciemnym pokazuje słońce (przejście w jasny) --}}
|
||||
<svg class="w-5 h-5 hidden dark:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{{-- Menu użytkownika: awatar -> rozwijane (język + wyloguj) --}}
|
||||
<div class="relative" id="user-menu">
|
||||
<button type="button" id="user-menu-button" aria-haspopup="true" aria-expanded="false"
|
||||
class="flex items-center gap-2 rounded-full hover:opacity-90 transition-opacity">
|
||||
<span class="grid place-items-center w-9 h-9 rounded-full bg-primary text-on-primary font-data-label text-data-label">{{ $initials }}</span>
|
||||
</button>
|
||||
|
||||
<div id="user-menu-panel"
|
||||
class="hidden absolute right-0 mt-2 w-60 bg-surface-container-lowest border border-outline-variant/40 rounded-xl shadow-card py-2 z-40">
|
||||
<div class="px-4 py-2 border-b border-outline-variant/30">
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant">Zalogowano jako</p>
|
||||
<p class="font-data-label text-data-label text-on-surface truncate">{{ $uname }}</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3">
|
||||
<p class="font-data-label text-data-label text-on-surface-variant/70 uppercase tracking-wider mb-2">Język</p>
|
||||
<div class="flex gap-1.5">
|
||||
@foreach ($languages as $code => $label)
|
||||
<a href="{{ route('locale.set', $code) }}"
|
||||
class="px-3 py-1 rounded-md font-data-label text-data-label transition-colors
|
||||
{{ app()->getLocale() === $code ? 'bg-secondary-container text-on-secondary-container font-semibold' : 'text-on-surface-variant hover:bg-surface-container' }}">
|
||||
{{ $label }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-outline-variant/30 mt-1 pt-1">
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="w-full text-left px-4 py-2 font-body-md text-body-md text-on-surface hover:bg-surface-container transition-colors">
|
||||
Wyloguj
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex min-h-[calc(100vh-4rem)]">
|
||||
{{-- ===== Lewy sidebar: POZYCJE aktywnej sekcji ===== --}}
|
||||
<aside class="hidden lg:block w-72 shrink-0 bg-surface-container-lowest border-r border-outline-variant/40 p-gutter">
|
||||
<p class="font-data-label text-data-label text-on-surface-variant uppercase tracking-wider mb-4">
|
||||
{{ $activeSection['label'] ?? 'Nawigacja' }}
|
||||
</p>
|
||||
<nav class="space-y-1">
|
||||
@forelse ($activeSection['items'] ?? [] as $it)
|
||||
@php
|
||||
$r = $it['route'] ?? null;
|
||||
$url = ($r && Route::has($r)) ? route($r) : '#';
|
||||
$isActive = $r && $r === $active;
|
||||
@endphp
|
||||
<a href="{{ $url }}"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-full font-title-sm text-title-sm transition-all
|
||||
{{ $isActive ? 'bg-secondary-container text-on-secondary-container font-semibold' : 'text-on-surface-variant hover:bg-surface-variant/50' }}">
|
||||
<x-icon :name="$it['icon'] ?? ''" class="w-5 h-5 shrink-0" />
|
||||
<span>{{ $it['label'] }}@if (! empty($it['append_department']) && session('department')) - {{ session('department') }}@endif</span>
|
||||
</a>
|
||||
@empty
|
||||
<p class="px-3 py-2 font-body-sm text-body-sm text-on-surface-variant">Moduł w przygotowaniu.</p>
|
||||
@endforelse
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{{-- ===== Treść ===== --}}
|
||||
<main class="flex-1 p-gutter">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="border-t border-outline-variant/30 px-gutter py-3 text-center font-body-sm text-body-sm text-on-surface-variant">
|
||||
© SoftSPM Przemysław Zatorski · v.2.0
|
||||
</footer>
|
||||
|
||||
{{-- ===== Globalny preloader (pokazywany gdy nawigacja/wyszukiwanie trwa dłużej) ===== --}}
|
||||
<div id="page-loader" class="fixed inset-0 z-[70] hidden items-center justify-center bg-on-surface/30 backdrop-blur-sm">
|
||||
<div class="flex flex-col items-center gap-3 bg-surface-container-lowest/95 px-7 py-6 rounded-xl shadow-card border border-outline-variant/40">
|
||||
<div class="w-12 h-12 rounded-full border-4 border-secondary/25 border-t-secondary animate-spin"></div>
|
||||
<span class="font-data-label text-data-label text-on-surface-variant tracking-wide">Ładowanie...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ===== Wspólny modal potwierdzenia (wyśrodkowany) ===== --}}
|
||||
<div id="confirm-modal" class="fixed inset-0 z-50 hidden items-center justify-center p-4">
|
||||
<div class="absolute inset-0 bg-on-surface/40 backdrop-blur-sm" data-confirm-cancel></div>
|
||||
<div class="relative w-full max-w-md bg-surface-container-lowest rounded-xl shadow-card border border-outline-variant/40 p-6 space-y-4">
|
||||
<h3 id="confirm-modal-title" class="font-headline-md text-headline-md text-on-surface">Potwierdzenie</h3>
|
||||
<p id="confirm-modal-message" class="font-body-md text-body-md text-on-surface-variant">Czy na pewno?</p>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button type="button" data-confirm-cancel
|
||||
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</button>
|
||||
<button type="button" id="confirm-modal-ok"
|
||||
class="bg-error text-on-error px-5 py-2.5 rounded-full font-title-sm text-title-sm hover:opacity-90 active:scale-95 transition-all">Usuń</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const modal = document.getElementById('confirm-modal');
|
||||
if (!modal) return;
|
||||
const titleEl = document.getElementById('confirm-modal-title');
|
||||
const msgEl = document.getElementById('confirm-modal-message');
|
||||
const okBtn = document.getElementById('confirm-modal-ok');
|
||||
let pendingForm = null;
|
||||
|
||||
function open(form) {
|
||||
pendingForm = form;
|
||||
titleEl.textContent = form.dataset.confirmTitle || 'Potwierdzenie usunięcia';
|
||||
msgEl.textContent = form.dataset.confirmMessage || 'Czy na pewno?';
|
||||
okBtn.textContent = form.dataset.confirmButton || 'Usuń';
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
}
|
||||
function close() {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
pendingForm = null;
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
const trigger = e.target.closest('[data-confirm-trigger]');
|
||||
if (trigger) { e.preventDefault(); open(trigger.closest('form')); return; }
|
||||
if (e.target.closest('[data-confirm-cancel]')) { close(); }
|
||||
});
|
||||
okBtn.addEventListener('click', function () { if (pendingForm) pendingForm.submit(); });
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
{{-- Globalny preloader: pojawia się tylko gdy odpowiedź serwera trwa dłużej niż DELAY --}}
|
||||
<script>
|
||||
(function () {
|
||||
const loader = document.getElementById('page-loader');
|
||||
if (!loader) return;
|
||||
const DELAY = 250; // ms - pokaż dopiero gdy ładowanie się przeciąga
|
||||
let timer = null;
|
||||
|
||||
function show() { loader.classList.remove('hidden'); loader.classList.add('flex'); }
|
||||
function hide() { clearTimeout(timer); loader.classList.add('hidden'); loader.classList.remove('flex'); }
|
||||
function schedule() { clearTimeout(timer); timer = setTimeout(show, DELAY); }
|
||||
|
||||
// Wysłanie formularza (wyszukiwarki, filtry, zapisy) - oprócz otwieranych w nowej karcie.
|
||||
document.addEventListener('submit', function (e) {
|
||||
const form = e.target;
|
||||
if (form.matches && form.matches('form') && form.target !== '_blank') schedule();
|
||||
}, true);
|
||||
|
||||
// Nawigacja kliknięciem w wewnętrzny link (paginacja, menu, akcje).
|
||||
document.addEventListener('click', function (e) {
|
||||
const a = e.target.closest && e.target.closest('a[href]');
|
||||
if (!a) return;
|
||||
const href = a.getAttribute('href');
|
||||
if (!href || href.charAt(0) === '#' || a.target === '_blank' || a.hasAttribute('download')) return;
|
||||
if (a.origin && a.origin !== window.location.origin) return; // tylko ten sam serwer
|
||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
schedule();
|
||||
});
|
||||
|
||||
// Powrót z cache przeglądarki (przycisk Wstecz) - ukryj ewentualny preloader.
|
||||
window.addEventListener('pageshow', hide);
|
||||
})();
|
||||
</script>
|
||||
|
||||
{{-- Motyw (jasny/ciemny) + menu użytkownika --}}
|
||||
<script>
|
||||
(function () {
|
||||
// ===== Motyw - zapis w cookie na ~400 dni, odświeżany przy każdej wizycie =====
|
||||
const COOKIE_MAX_AGE = 60 * 60 * 24 * 400; // sekundy (~400 dni, maksimum przeglądarek)
|
||||
function applyTheme(t) {
|
||||
const r = document.documentElement;
|
||||
r.classList.toggle('dark', t === 'dark');
|
||||
r.classList.toggle('light', t !== 'dark');
|
||||
document.cookie = 'theme=' + t + ';path=/;max-age=' + COOKIE_MAX_AGE + ';samesite=lax';
|
||||
}
|
||||
// Odśwież okno ważności cookie (utrzymanie wyboru "na zawsze" mimo limitu przeglądarki).
|
||||
applyTheme(document.documentElement.classList.contains('dark') ? 'dark' : 'light');
|
||||
|
||||
const toggle = document.getElementById('theme-toggle');
|
||||
if (toggle) toggle.addEventListener('click', function () {
|
||||
applyTheme(document.documentElement.classList.contains('dark') ? 'light' : 'dark');
|
||||
});
|
||||
|
||||
// ===== Menu użytkownika (rozwijane) =====
|
||||
const btn = document.getElementById('user-menu-button');
|
||||
const panel = document.getElementById('user-menu-panel');
|
||||
if (btn && panel) {
|
||||
function closeMenu() { panel.classList.add('hidden'); btn.setAttribute('aria-expanded', 'false'); }
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
const willOpen = panel.classList.contains('hidden');
|
||||
panel.classList.toggle('hidden');
|
||||
btn.setAttribute('aria-expanded', String(willOpen));
|
||||
});
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!panel.contains(e.target) && !btn.contains(e.target)) closeMenu();
|
||||
});
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeMenu(); });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
@props([
|
||||
'variant' => 'secondary', // primary | secondary | ghost | danger
|
||||
'as' => 'button', // 'button' | 'a'
|
||||
'disabled' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
// Jednolity styl przycisków dla CAŁEGO projektu. Zawsze pigułka px-5 py-2.5.
|
||||
$base = 'inline-flex items-center justify-center px-5 py-2.5 rounded-full font-title-sm text-title-sm transition-all';
|
||||
$variants = [
|
||||
'primary' => 'bg-primary text-on-primary hover:bg-primary-container active:scale-95 shadow-card',
|
||||
'secondary' => 'bg-secondary text-on-secondary hover:opacity-90 active:scale-95',
|
||||
'ghost' => 'text-on-surface-variant hover:bg-surface-container',
|
||||
'danger' => 'bg-error text-on-error hover:opacity-90 active:scale-95',
|
||||
];
|
||||
$v = $variants[$variant] ?? $variants['secondary'];
|
||||
$cls = $base.' '.$v.($disabled ? ' opacity-50 cursor-not-allowed pointer-events-none' : '');
|
||||
@endphp
|
||||
|
||||
@if ($as === 'a')
|
||||
<a {{ $attributes->merge(['class' => $cls]) }}>{{ $slot }}</a>
|
||||
@else
|
||||
<button @disabled($disabled) {{ $attributes->merge(['type' => 'button', 'class' => $cls]) }}>{{ $slot }}</button>
|
||||
@endif
|
||||
@@ -0,0 +1,20 @@
|
||||
@props([
|
||||
'action',
|
||||
'message' => 'Czy na pewno usunąć ten obiekt? Tej operacji nie można cofnąć.',
|
||||
'title' => 'Potwierdzenie usunięcia',
|
||||
'button' => 'Usuń',
|
||||
'label' => 'Usuń',
|
||||
])
|
||||
|
||||
{{-- Formularz usuwania - przycisk otwiera wspólny modal (patrz app-layout). --}}
|
||||
<form method="POST" action="{{ $action }}"
|
||||
data-confirm-message="{{ $message }}"
|
||||
data-confirm-title="{{ $title }}"
|
||||
data-confirm-button="{{ $button }}">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="button" data-confirm-trigger
|
||||
class="bg-error text-on-error px-4 py-2 rounded-full font-title-sm text-title-sm hover:opacity-90 transition-all active:scale-95">
|
||||
{{ $label }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -0,0 +1,35 @@
|
||||
@props([
|
||||
'points', // kolekcja {x, y}
|
||||
'xLabel' => 'Q',
|
||||
'yLabel' => 'Y',
|
||||
'color' => '#006876',
|
||||
])
|
||||
|
||||
@php
|
||||
$w = 340; $h = 210; $padL = 40; $padR = 12; $padT = 12; $padB = 26;
|
||||
$plotW = $w - $padL - $padR; $plotH = $h - $padT - $padB;
|
||||
$xMax = max(0.0001, (float) $points->max('x'));
|
||||
$yMax = max(0.0001, (float) $points->max('y'));
|
||||
$sx = fn ($x) => $padL + ($x / $xMax) * $plotW;
|
||||
$sy = fn ($y) => $padT + $plotH - ($y / $yMax) * $plotH;
|
||||
$poly = $points->map(fn ($p) => round($sx($p->x), 1).','.round($sy($p->y), 1))->implode(' ');
|
||||
@endphp
|
||||
|
||||
<svg viewBox="0 0 {{ $w }} {{ $h }}" class="w-full h-auto" xmlns="http://www.w3.org/2000/svg">
|
||||
{{-- osie --}}
|
||||
<line x1="{{ $padL }}" y1="{{ $padT }}" x2="{{ $padL }}" y2="{{ $padT + $plotH }}" stroke="#cbd5d6" stroke-width="1"/>
|
||||
<line x1="{{ $padL }}" y1="{{ $padT + $plotH }}" x2="{{ $padL + $plotW }}" y2="{{ $padT + $plotH }}" stroke="#cbd5d6" stroke-width="1"/>
|
||||
{{-- krzywa --}}
|
||||
<polyline points="{{ $poly }}" fill="none" stroke="{{ $color }}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
{{-- punkty --}}
|
||||
@foreach ($points as $p)
|
||||
<circle cx="{{ round($sx($p->x), 1) }}" cy="{{ round($sy($p->y), 1) }}" r="3" fill="{{ $color }}"/>
|
||||
@endforeach
|
||||
{{-- skrajne wartości osi --}}
|
||||
<text x="{{ $padL - 4 }}" y="{{ $padT + 6 }}" font-size="8" fill="#888" text-anchor="end">{{ rtrim(rtrim(number_format($yMax, 1, '.', ''), '0'), '.') }}</text>
|
||||
<text x="{{ $padL - 4 }}" y="{{ $padT + $plotH }}" font-size="8" fill="#888" text-anchor="end">0</text>
|
||||
<text x="{{ $padL + $plotW }}" y="{{ $padT + $plotH + 12 }}" font-size="8" fill="#888" text-anchor="end">{{ rtrim(rtrim(number_format($xMax, 2, '.', ''), '0'), '.') }}</text>
|
||||
{{-- etykiety osi --}}
|
||||
<text x="{{ $padL }}" y="{{ $padT - 3 }}" font-size="9" fill="#555">{{ $yLabel }}</text>
|
||||
<text x="{{ $padL + $plotW }}" y="{{ $h - 4 }}" font-size="9" fill="#555" text-anchor="end">{{ $xLabel }}</text>
|
||||
</svg>
|
||||
@@ -0,0 +1,55 @@
|
||||
@props([
|
||||
'eta', // sprawność η (0..~0.7) lub null
|
||||
'ry', // granica czerwony/żółty (η)
|
||||
'yg', // granica żółty/zielony (η)
|
||||
'zone' => null, // 'green'|'yellow'|'red' (kolor opisu)
|
||||
])
|
||||
|
||||
@php
|
||||
// Skala 0..0.7 jak legacy. Pozycja znormalizowana: 0=lewo (wysoka η, zielona), 1=prawo.
|
||||
$clamp = fn ($v) => max(0.0, min(1.0, $v));
|
||||
$pYG = $clamp(1 - ($yg / 0.7)); // granica zielony/żółty
|
||||
$pRY = $clamp(1 - ($ry / 0.7)); // granica żółty/czerwony
|
||||
$pYG = min($pYG, $pRY);
|
||||
$pEta = $eta === null ? null : $clamp(1 - ($eta / 0.7));
|
||||
|
||||
// Wyraźne strefy z krótkim przejściem (jak oryginał: green | yellow | red).
|
||||
$t = 0.03;
|
||||
$stops = [];
|
||||
$prev = 0.0;
|
||||
$add = function ($off, $color) use (&$stops, &$prev, $clamp) {
|
||||
$off = max($prev, $clamp($off)); // offsety rosnące
|
||||
$stops[] = [round($off * 100, 2), $color];
|
||||
$prev = $off;
|
||||
};
|
||||
$add(0, '#1f9d57');
|
||||
$add($pYG - $t, '#1f9d57');
|
||||
$add($pYG + $t, '#f4c20d');
|
||||
$add($pRY - $t, '#f4c20d');
|
||||
$add($pRY + $t, '#d32f2f');
|
||||
$add(1, '#d32f2f');
|
||||
|
||||
$id = 'eg-'.substr(md5(($eta ?? 0).$ry.$yg), 0, 6);
|
||||
$pct = fn ($v) => round($v * 100, 2);
|
||||
$zoneColor = ['green' => '#1f9d57', 'yellow' => '#f4c20d', 'red' => '#d32f2f'][$zone] ?? '#5b6566';
|
||||
@endphp
|
||||
|
||||
@if ($eta === null)
|
||||
<div class="h-10 rounded bg-surface-container grid place-items-center font-body-sm text-body-sm text-on-surface-variant">
|
||||
Brak oceny
|
||||
</div>
|
||||
@else
|
||||
<svg viewBox="0 0 100 16" class="w-full h-12" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="{{ $id }}" x1="0" y1="0" x2="1" y2="0">
|
||||
@foreach ($stops as [$off, $color])
|
||||
<stop offset="{{ $off }}%" stop-color="{{ $color }}"/>
|
||||
@endforeach
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="3" width="100" height="10" rx="1.5" fill="url(#{{ $id }})"/>
|
||||
{{-- Znacznik (dzwon) - pozycja wg η --}}
|
||||
<line x1="{{ $pct($pEta) }}" y1="0.5" x2="{{ $pct($pEta) }}" y2="15.5" stroke="#101415" stroke-width="0.9"/>
|
||||
<line x1="{{ $pct($pEta) }}" y1="0.5" x2="{{ $pct($pEta) }}" y2="15.5" stroke="#ffffff" stroke-width="0.3"/>
|
||||
</svg>
|
||||
@endif
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="light" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ $title ?? 'SoftSPM 2' }}</title>
|
||||
|
||||
{{-- Air-gap: brak CDN. Assety budowane lokalnie przez Vite, fonty self-hosted. --}}
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="min-h-screen bg-surface text-on-surface antialiased">
|
||||
{{ $slot }}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
@props(['name' => '', 'class' => 'w-5 h-5'])
|
||||
|
||||
@php
|
||||
// Ikony jako wbudowane SVG (Lucide-style) - działają offline, bez fontu.
|
||||
// Klucze = nazwy 'icon' z config/navigation.php.
|
||||
$paths = [
|
||||
'home' => '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/>',
|
||||
'map' => '<path d="M3 6 9 3l6 3 6-3v15l-6 3-6-3-6 3z"/><path d="M9 3v15"/><path d="M15 6v15"/>',
|
||||
'inventory_2' => '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
|
||||
'monitoring' => '<path d="M3 12h4l3 8 4-16 3 8h4"/>',
|
||||
'engineering' => '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
|
||||
'biotech' => '<path d="M10 2v6.3a1 1 0 0 1-.4.8l-3.6 2.6A4 4 0 0 0 8.5 19h7a4 4 0 0 0 2.5-7.3l-3.6-2.6a1 1 0 0 1-.4-.8V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>',
|
||||
'build' => '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
|
||||
'settings' => '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
|
||||
'menu_book' => '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
|
||||
'water_drop' => '<path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C4 11.1 3 13 3 15a7 7 0 0 0 7 7z"/>',
|
||||
'bolt' => '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>',
|
||||
'precision_manufacturing' => '<path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7 5V8l-7 5V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M7 18h.01"/><path d="M12 18h.01"/><path d="M17 18h.01"/>',
|
||||
'checklist' => '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/>',
|
||||
'cable' => '<path d="M5 3v4"/><path d="M3 5h4"/><path d="M19 17v4"/><path d="M17 19h4"/><path d="M5 7a7 7 0 0 0 7 7 7 7 0 0 1 7 7"/>',
|
||||
'plumbing' => '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
|
||||
'speed' => '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
|
||||
];
|
||||
$svg = $paths[$name] ?? '<circle cx="12" cy="12" r="2"/>';
|
||||
@endphp
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
{{ $attributes->merge(['class' => $class]) }}>
|
||||
{!! $svg !!}
|
||||
</svg>
|
||||
@@ -0,0 +1,96 @@
|
||||
@props([
|
||||
'power' => null, // ['pe','pod','paz','psl','psa','pp2','pst','pgh','sum'] lub null
|
||||
'on' => false,
|
||||
'time' => null, // czas pomiaru
|
||||
])
|
||||
|
||||
@php
|
||||
// Schemat układu pompowego (zastępuje statyczny rozklad_mocy.png). Bez strzałek -
|
||||
// rozkład mocy opisany legendą pod rysunkiem (każda składowa = inny kolor).
|
||||
$f = fn ($v) => $v === null ? '-' : number_format(round((float) $v, 2), 2, ',', ' ');
|
||||
$pumpColor = $on ? '#16a34a' : '#9aa6a8';
|
||||
$lab = '#2f3e45';
|
||||
|
||||
// Legenda: [kolor (unikalny), opis, klucz wartości].
|
||||
$legend = [
|
||||
['#ff7a1a', 'Moc pobrana - kabel przyłączeniowy', 'pe'],
|
||||
['#d32f2f', 'Moc na wyjściu pompy (agregatu)', 'pod'],
|
||||
['#2563eb', 'Podnoszenie stałe (do zwierciadła statycznego)', 'pst'],
|
||||
['#0891b2', 'Geo-hydraulika (na filtrach studni)', 'pgh'],
|
||||
['#7c3aed', 'Odpływ (za zasuwą dławiącą)', 'pp2'],
|
||||
['#f59e0b', 'Straty: agregat + zasilanie', 'paz'],
|
||||
['#65a30d', 'Straty liniowe (rurociąg tłoczny)', 'psl'],
|
||||
['#db2777', 'Straty w armaturze (zawór)', 'psa'],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@if ($power === null)
|
||||
<div class="py-10 text-center font-body-sm text-body-sm text-on-surface-variant">Brak danych pomiarowych do bilansu mocy.</div>
|
||||
@else
|
||||
<svg viewBox="110 0 350 580" class="w-full h-auto max-h-[30rem] mx-auto block" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="ps-ground" width="8" height="8" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="8" stroke="#b9a58c" stroke-width="2"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
{{-- ===== Instalacja powierzchniowa ===== --}}
|
||||
<rect x="216" y="90" width="8" height="56" fill="#7aa7c7"/>
|
||||
<path d="M220 92 H360" stroke="#7aa7c7" stroke-width="8" fill="none"/>
|
||||
{{-- zawór dławiący --}}
|
||||
<polygon points="248,82 248,102 272,82 272,102" fill="#e0e6ea" stroke="#3b3b3b" stroke-width="1.2"/>
|
||||
<text x="244" y="80" font-size="8" fill="{{ $lab }}" text-anchor="end">zawór</text>
|
||||
{{-- manometry + Q --}}
|
||||
<circle cx="234" cy="68" r="7" fill="#fff" stroke="#3b3b3b" stroke-width="1.3"/><text x="234" y="57" font-size="9" fill="{{ $lab }}" text-anchor="middle">p₁</text>
|
||||
<circle cx="296" cy="68" r="7" fill="#fff" stroke="#3b3b3b" stroke-width="1.3"/><text x="296" y="57" font-size="9" fill="{{ $lab }}" text-anchor="middle">p₂</text>
|
||||
<rect x="316" y="80" width="16" height="14" rx="2" fill="#fff" stroke="#3b3b3b" stroke-width="1.3"/><text x="324" y="91" font-size="8" font-weight="700" fill="#3b3b3b" text-anchor="middle">Q</text>
|
||||
{{-- głowica/silnik nawierzchniowy + skrzynka kabla --}}
|
||||
<rect x="198" y="120" width="56" height="22" rx="2" fill="#cfd8dd" stroke="#5b6566"/>
|
||||
<rect x="198" y="104" width="18" height="16" rx="2" fill="#37424a"/>
|
||||
<text x="194" y="100" font-size="8" fill="{{ $lab }}" text-anchor="end">kabel</text>
|
||||
|
||||
{{-- ===== Grunt + studnia ===== --}}
|
||||
<rect x="0" y="175" width="460" height="14" fill="url(#ps-ground)"/>
|
||||
<line x1="0" y1="175" x2="460" y2="175" stroke="#8f706c" stroke-width="1.5"/>
|
||||
<line x1="196" y1="175" x2="196" y2="540" stroke="#5b6566" stroke-width="3"/>
|
||||
<line x1="246" y1="175" x2="246" y2="540" stroke="#5b6566" stroke-width="3"/>
|
||||
<line x1="196" y1="540" x2="246" y2="540" stroke="#5b6566" stroke-width="3"/>
|
||||
<rect x="198" y="345" width="48" height="195" fill="#cfeaf5" opacity="0.55"/>
|
||||
|
||||
{{-- zwierciadła --}}
|
||||
<line x1="196" y1="315" x2="246" y2="315" stroke="#1565c0" stroke-width="1.3" stroke-dasharray="4 3"/>
|
||||
<text x="252" y="318" font-size="9" fill="{{ $lab }}">H<tspan baseline-shift="sub" font-size="6.5">st</tspan></text>
|
||||
<line x1="196" y1="345" x2="246" y2="345" stroke="#1565c0" stroke-width="1.8"/>
|
||||
<text x="252" y="348" font-size="9" fill="{{ $lab }}">H<tspan baseline-shift="sub" font-size="6.5">d</tspan></text>
|
||||
|
||||
{{-- rurociąg pionowy + kabel zasilający --}}
|
||||
<rect x="216" y="142" width="8" height="332" fill="#7aa7c7"/>
|
||||
<line x1="208" y1="142" x2="208" y2="470" stroke="#37424a" stroke-width="2.5"/>
|
||||
|
||||
{{-- filtry studni (perforacja) --}}
|
||||
@for ($i = 0; $i < 7; $i++)
|
||||
<line x1="188" y1="{{ 474 + $i * 9 }}" x2="196" y2="{{ 474 + $i * 9 }}" stroke="#5b6566" stroke-width="2"/>
|
||||
<line x1="246" y1="{{ 474 + $i * 9 }}" x2="254" y2="{{ 474 + $i * 9 }}" stroke="#5b6566" stroke-width="2"/>
|
||||
@endfor
|
||||
<text x="184" y="524" font-size="8" fill="{{ $lab }}" text-anchor="end">filtry</text>
|
||||
|
||||
{{-- agregat: pompa + silnik --}}
|
||||
<rect x="206" y="472" width="30" height="36" rx="3" fill="{{ $pumpColor }}" stroke="#2f3e45" stroke-width="1.5"/>
|
||||
<rect x="211" y="508" width="20" height="18" rx="2" fill="#46555d"/>
|
||||
<text x="262" y="494" font-size="8" fill="{{ $lab }}">agregat</text>
|
||||
|
||||
{{-- Czas pomiaru --}}
|
||||
<text x="452" y="566" font-size="11" fill="#1d4ed8" text-anchor="end">Czas pomiaru: {{ $time ?? '-' }}</text>
|
||||
</svg>
|
||||
|
||||
{{-- Legenda: kolor + opis + wartość (każda składowa innym kolorem) --}}
|
||||
<div class="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-x-5 gap-y-1.5 font-body-sm text-body-sm">
|
||||
@foreach ($legend as [$color, $desc, $key])
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-3.5 h-3.5 rounded-sm shrink-0" style="background: {{ $color }}"></span>
|
||||
<span class="text-on-surface-variant flex-1">{{ $desc }}</span>
|
||||
<span class="font-data-label text-on-surface whitespace-nowrap">{{ $f($power[$key]) }} kW</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,178 @@
|
||||
@props([
|
||||
'plot', // ['segments','raw','xMax','yMax','coeffs']
|
||||
'xLabel' => 'Q [m³/h]',
|
||||
'yLabel' => 'Y',
|
||||
'interactive' => false, // przeciągalny marker [Q,H]
|
||||
'id' => 'curve',
|
||||
'initialQ' => null, // początkowe Q markera
|
||||
'etaCoeffs' => [], // współczynniki η (do aktualizacji panelu)
|
||||
'qInput' => null, // selektor pola Q (input)
|
||||
'hOutput' => null, // selektor wyświetlania H
|
||||
'etaOutput' => null, // selektor wyświetlania η
|
||||
'opX' => null, // stały punkt pracy: Q (np. z pomiaru SYNDIS)
|
||||
'opY' => null, // stały punkt pracy: wartość Y w tym Q
|
||||
'opColor' => '#c62828', // kolor punktu pracy
|
||||
'opYLabel' => null, // gdy ustawione - przy Q pokazuje też tę wartość (np. H)
|
||||
'band' => false, // pionowy pasek podświetlenia przy opX (wykres η)
|
||||
])
|
||||
|
||||
@php
|
||||
$w = 380; $h = 230; $padL = 48; $padR = 12; $padT = 14; $padB = 30;
|
||||
$plotW = $w - $padL - $padR; $plotH = $h - $padT - $padB;
|
||||
$xMax = max(0.0001, (float) $plot['xMax']);
|
||||
$yMax = max(0.0001, (float) $plot['yMax']);
|
||||
$sx = fn ($x) => $padL + ($x / $xMax) * $plotW;
|
||||
$sy = fn ($y) => $padT + $plotH - ($y / $yMax) * $plotH;
|
||||
$fmt = fn ($v) => $v >= 100 ? number_format($v, 0, ',', ' ')
|
||||
: ($v >= 10 ? number_format($v, 1, ',', '') : number_format($v, 2, ',', ''));
|
||||
@endphp
|
||||
|
||||
<svg viewBox="0 0 {{ $w }} {{ $h }}" class="w-full h-auto select-none" xmlns="http://www.w3.org/2000/svg"
|
||||
@if ($interactive)
|
||||
id="{{ $id }}"
|
||||
data-padl="{{ $padL }}" data-padt="{{ $padT }}" data-plotw="{{ $plotW }}" data-ploth="{{ $plotH }}"
|
||||
data-xmax="{{ $xMax }}" data-ymax="{{ $yMax }}"
|
||||
data-coeffs="{{ json_encode(array_values($plot['coeffs'] ?? [])) }}"
|
||||
data-etacoeffs="{{ json_encode(array_values($etaCoeffs ?? [])) }}"
|
||||
data-scope="{{ json_encode($plot['raw']->pluck('x')->values()->all()) }}"
|
||||
@endif
|
||||
>
|
||||
{{-- Siatka + etykiety osi --}}
|
||||
@for ($g = 0; $g <= 4; $g++)
|
||||
@php $gy = $padT + $plotH * $g / 4; $vy = $yMax * (1 - $g / 4); @endphp
|
||||
<line x1="{{ $padL }}" y1="{{ round($gy, 1) }}" x2="{{ $padL + $plotW }}" y2="{{ round($gy, 1) }}" stroke="var(--chart-grid)" stroke-width="1"/>
|
||||
<text x="{{ $padL - 5 }}" y="{{ round($gy + 3, 1) }}" font-size="8" fill="var(--chart-tick)" text-anchor="end">{{ $fmt($vy) }}</text>
|
||||
@endfor
|
||||
@for ($g = 0; $g <= 4; $g++)
|
||||
@php $gx = $padL + $plotW * $g / 4; $vx = $xMax * $g / 4; @endphp
|
||||
<line x1="{{ round($gx, 1) }}" y1="{{ $padT }}" x2="{{ round($gx, 1) }}" y2="{{ $padT + $plotH }}" stroke="var(--chart-grid)" stroke-width="1"/>
|
||||
<text x="{{ round($gx, 1) }}" y="{{ $padT + $plotH + 12 }}" font-size="8" fill="var(--chart-tick)" text-anchor="middle">{{ $fmt($vx) }}</text>
|
||||
@endfor
|
||||
|
||||
{{-- Osie --}}
|
||||
<line x1="{{ $padL }}" y1="{{ $padT }}" x2="{{ $padL }}" y2="{{ $padT + $plotH }}" stroke="var(--chart-axis)" stroke-width="1"/>
|
||||
<line x1="{{ $padL }}" y1="{{ $padT + $plotH }}" x2="{{ $padL + $plotW }}" y2="{{ $padT + $plotH }}" stroke="var(--chart-axis)" stroke-width="1"/>
|
||||
|
||||
{{-- Krzywa wielomianowa - kolorowe segmenty wg stref Q --}}
|
||||
@foreach ($plot['segments'] as $seg)
|
||||
@php $poly = collect($seg['points'])->map(fn ($p) => round($sx($p['x']), 1).','.round($sy($p['y']), 1))->implode(' '); @endphp
|
||||
<polyline points="{{ $poly }}" fill="none" stroke="{{ $seg['color'] }}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
@endforeach
|
||||
|
||||
{{-- Pasek podświetlenia przy zadanym Q (wykres η) --}}
|
||||
@if ($band && $opX !== null && $opX >= 0 && $opX <= $xMax)
|
||||
<rect x="{{ round($sx($opX) - 6, 1) }}" y="{{ $padT }}" width="12" height="{{ $plotH }}" fill="#f4c20d" opacity="0.22"/>
|
||||
@endif
|
||||
|
||||
{{-- Punkty surowe (niebieskie) --}}
|
||||
@foreach ($plot['raw'] as $p)
|
||||
<circle cx="{{ round($sx($p->x), 1) }}" cy="{{ round($sy($p->y), 1) }}" r="3" fill="var(--chart-raw)"/>
|
||||
@endforeach
|
||||
|
||||
{{-- Stały punkt pracy (wartości zmierzone) - na rzeczywistej wysokości, nie rzutowany na krzywą --}}
|
||||
@if ($opX !== null && $opY !== null && $opX >= 0 && $opX <= $xMax)
|
||||
<circle cx="{{ round($sx($opX), 1) }}" cy="{{ round($sy(max(0, $opY)), 1) }}" r="5" fill="{{ $opColor }}" stroke="#ffffff" stroke-width="1.5"/>
|
||||
<text x="{{ $padL + $plotW }}" y="{{ $padT + 2 }}" font-size="7" fill="var(--color-secondary)" text-anchor="end">Q = {{ number_format(round((float) $opX, 3), 3, ',', '') }}</text>
|
||||
@if ($opYLabel !== null)
|
||||
<text x="{{ $padL + $plotW }}" y="{{ $padT + 11 }}" font-size="7" fill="var(--color-secondary)" text-anchor="end">{{ $opYLabel }} = {{ number_format(round((float) $opY, 1), 1, ',', ' ') }}</text>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Interaktywny marker + tooltip --}}
|
||||
@if ($interactive)
|
||||
<g id="{{ $id }}-tip" style="display:none">
|
||||
<rect id="{{ $id }}-tiprect" rx="3" height="18" fill="#181c1d" opacity="0.9"></rect>
|
||||
<text id="{{ $id }}-tiptext" font-size="9" fill="#ffffff" text-anchor="middle"></text>
|
||||
</g>
|
||||
<circle id="{{ $id }}-marker" r="6" fill="#a61317" stroke="#ffffff" stroke-width="2" style="cursor:grab"></circle>
|
||||
@endif
|
||||
|
||||
{{-- Etykiety osi --}}
|
||||
<text x="{{ $padL - 4 }}" y="{{ $padT - 4 }}" font-size="9" fill="var(--chart-label)">{{ $yLabel }}</text>
|
||||
<text x="{{ $padL + $plotW }}" y="{{ $h - 3 }}" font-size="9" fill="var(--chart-label)" text-anchor="end">{{ $xLabel }}</text>
|
||||
</svg>
|
||||
|
||||
@if ($interactive)
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1 text-center">
|
||||
Przeciągnij <span class="inline-block w-2 h-2 rounded-full align-middle" style="background:#a61317"></span> po wykresie, aby odczytać H dla danego Q.
|
||||
</p>
|
||||
<script>
|
||||
(function () {
|
||||
function init() {
|
||||
const svg = document.getElementById(@json($id));
|
||||
if (!svg) return;
|
||||
const d = svg.dataset;
|
||||
const padL = +d.padl, padT = +d.padt, plotW = +d.plotw, plotH = +d.ploth, xMax = +d.xmax, yMax = +d.ymax;
|
||||
const hC = JSON.parse(d.coeffs || '[]');
|
||||
const etaC = JSON.parse(d.etacoeffs || '[]');
|
||||
const poly = (c, x) => c.reduce((s, ci, i) => s + ci * Math.pow(x, i), 0);
|
||||
const sx = q => padL + (q / xMax) * plotW;
|
||||
const sy = v => padT + plotH - (Math.max(0, v) / yMax) * plotH;
|
||||
const marker = document.getElementById(@json($id . '-marker'));
|
||||
const tip = document.getElementById(@json($id . '-tip'));
|
||||
const tipR = document.getElementById(@json($id . '-tiprect'));
|
||||
const tipT = document.getElementById(@json($id . '-tiptext'));
|
||||
const qInput = @json($qInput) ? document.querySelector(@json($qInput)) : null;
|
||||
const hOut = @json($hOutput) ? document.querySelector(@json($hOutput)) : null;
|
||||
const etaOut = @json($etaOutput) ? document.querySelector(@json($etaOutput)) : null;
|
||||
const fmt = (v, dd) => v.toFixed(dd).replace('.', ',');
|
||||
|
||||
// Kolor punktu wg przedziału (strefy Q) - jak segmenty krzywej.
|
||||
const scope = JSON.parse(d.scope || '[]');
|
||||
const BAND = { red: '#c62828', amber: '#f9a825', green: '#2e7d32' };
|
||||
function bandColor(q) {
|
||||
if (scope.length < 7) return BAND.green;
|
||||
if (q < scope[2]) return BAND.red;
|
||||
if (q < scope[3]) return BAND.amber;
|
||||
if (q < scope[5]) return BAND.green;
|
||||
if (q < scope[6]) return BAND.amber;
|
||||
return BAND.red;
|
||||
}
|
||||
|
||||
let hovering = false, dragging = false;
|
||||
function tipVisible() { tip.style.display = (hovering || dragging) ? '' : 'none'; }
|
||||
|
||||
function update(q) {
|
||||
q = Math.max(0, Math.min(q, xMax));
|
||||
const hVal = Math.max(0, poly(hC, q));
|
||||
const mx = sx(q), my = sy(hVal);
|
||||
marker.setAttribute('cx', mx);
|
||||
marker.setAttribute('cy', my);
|
||||
marker.setAttribute('fill', bandColor(q));
|
||||
const label = 'Q=' + fmt(q, 3) + ' H=' + fmt(hVal, 2);
|
||||
tipT.textContent = label;
|
||||
const tw = label.length * 5.2 + 10;
|
||||
let tx = Math.max(padL, Math.min(mx - tw / 2, padL + plotW - tw));
|
||||
const ty = Math.max(padT, my - 26);
|
||||
tipR.setAttribute('x', tx); tipR.setAttribute('y', ty); tipR.setAttribute('width', tw);
|
||||
tipT.setAttribute('x', tx + tw / 2); tipT.setAttribute('y', ty + 13);
|
||||
if (qInput) qInput.value = fmt(q, 3);
|
||||
if (hOut) hOut.textContent = fmt(hVal, 2);
|
||||
if (etaOut && etaC.length) etaOut.textContent = fmt(Math.max(0, poly(etaC, q)), 2);
|
||||
}
|
||||
function clientToQ(e) {
|
||||
const pt = svg.createSVGPoint();
|
||||
pt.x = e.clientX; pt.y = e.clientY;
|
||||
const loc = pt.matrixTransform(svg.getScreenCTM().inverse());
|
||||
return (loc.x - padL) / plotW * xMax;
|
||||
}
|
||||
|
||||
svg.style.cursor = 'ew-resize';
|
||||
// Tooltip tylko przy najechaniu na punkt (lub w trakcie przeciągania).
|
||||
marker.addEventListener('pointerenter', () => { hovering = true; tipVisible(); });
|
||||
marker.addEventListener('pointerleave', () => { hovering = false; tipVisible(); });
|
||||
svg.addEventListener('pointerdown', e => { dragging = true; tipVisible(); try { svg.setPointerCapture(e.pointerId); } catch (_) {} update(clientToQ(e)); });
|
||||
svg.addEventListener('pointermove', e => { if (dragging) update(clientToQ(e)); });
|
||||
svg.addEventListener('pointerup', () => { dragging = false; tipVisible(); });
|
||||
svg.addEventListener('pointercancel', () => { dragging = false; tipVisible(); });
|
||||
|
||||
// Domyślnie marker w Q_n; tooltip ukryty do najechania.
|
||||
const initQ = @json($initialQ);
|
||||
update(initQ === null ? xMax : initQ);
|
||||
tipVisible();
|
||||
}
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
||||
else init();
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
@@ -0,0 +1,59 @@
|
||||
@props([
|
||||
'chart', // ['curves'=>[['name','plot'=>[...]],...], 'xMax', 'yMax']
|
||||
'q' => null, // zadane Q - pionowa linia robocza
|
||||
'xLabel' => 'Q [m³/h]',
|
||||
'yLabel' => 'H [m]',
|
||||
])
|
||||
|
||||
@php
|
||||
$w = 760; $h = 360; $padL = 52; $padR = 16; $padT = 16; $padB = 34;
|
||||
$plotW = $w - $padL - $padR; $plotH = $h - $padT - $padB;
|
||||
$xMax = max(0.0001, (float) $chart['xMax']);
|
||||
$yMax = max(0.0001, (float) $chart['yMax']);
|
||||
$sx = fn ($x) => $padL + ($x / $xMax) * $plotW;
|
||||
$sy = fn ($y) => $padT + $plotH - ($y / $yMax) * $plotH;
|
||||
$fmt = fn ($v) => $v >= 100 ? number_format($v, 0, ',', ' ')
|
||||
: ($v >= 10 ? number_format($v, 1, ',', '') : number_format($v, 2, ',', ''));
|
||||
@endphp
|
||||
|
||||
<svg viewBox="0 0 {{ $w }} {{ $h }}" class="w-full h-auto select-none" xmlns="http://www.w3.org/2000/svg">
|
||||
{{-- Siatka + etykiety osi --}}
|
||||
@for ($g = 0; $g <= 4; $g++)
|
||||
@php $gy = $padT + $plotH * $g / 4; $vy = $yMax * (1 - $g / 4); @endphp
|
||||
<line x1="{{ $padL }}" y1="{{ round($gy, 1) }}" x2="{{ $padL + $plotW }}" y2="{{ round($gy, 1) }}" stroke="var(--chart-grid)" stroke-width="1"/>
|
||||
<text x="{{ $padL - 6 }}" y="{{ round($gy + 3, 1) }}" font-size="9" fill="var(--chart-tick)" text-anchor="end">{{ $fmt($vy) }}</text>
|
||||
@endfor
|
||||
@for ($g = 0; $g <= 5; $g++)
|
||||
@php $gx = $padL + $plotW * $g / 5; $vx = $xMax * $g / 5; @endphp
|
||||
<line x1="{{ round($gx, 1) }}" y1="{{ $padT }}" x2="{{ round($gx, 1) }}" y2="{{ $padT + $plotH }}" stroke="var(--chart-grid)" stroke-width="1"/>
|
||||
<text x="{{ round($gx, 1) }}" y="{{ $padT + $plotH + 14 }}" font-size="9" fill="var(--chart-tick)" text-anchor="middle">{{ $fmt($vx) }}</text>
|
||||
@endfor
|
||||
|
||||
{{-- Osie --}}
|
||||
<line x1="{{ $padL }}" y1="{{ $padT }}" x2="{{ $padL }}" y2="{{ $padT + $plotH }}" stroke="var(--chart-axis)" stroke-width="1"/>
|
||||
<line x1="{{ $padL }}" y1="{{ $padT + $plotH }}" x2="{{ $padL + $plotW }}" y2="{{ $padT + $plotH }}" stroke="var(--chart-axis)" stroke-width="1"/>
|
||||
|
||||
{{-- Linia robocza przy zadanym Q --}}
|
||||
@if ($q !== null && $q > 0 && $q <= $xMax)
|
||||
<line x1="{{ round($sx($q), 1) }}" y1="{{ $padT }}" x2="{{ round($sx($q), 1) }}" y2="{{ $padT + $plotH }}"
|
||||
stroke="var(--color-secondary)" stroke-width="1.5" stroke-dasharray="4 3"/>
|
||||
<text x="{{ round($sx($q), 1) }}" y="{{ $padT - 4 }}" font-size="9" fill="var(--color-secondary)" text-anchor="middle">Q = {{ $fmt($q) }}</text>
|
||||
@endif
|
||||
|
||||
{{-- Nałożone krzywe H wszystkich dopasowanych pomp (kolorowe segmenty stref Q) --}}
|
||||
@foreach ($chart['curves'] as $curve)
|
||||
@foreach ($curve['plot']['segments'] as $seg)
|
||||
@php $poly = collect($seg['points'])->map(fn ($p) => round($sx($p['x']), 1).','.round($sy($p['y']), 1))->implode(' '); @endphp
|
||||
<polyline points="{{ $poly }}" fill="none" stroke="{{ $seg['color'] }}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.85"/>
|
||||
@endforeach
|
||||
{{-- Nazwa pompy przy końcu krzywej --}}
|
||||
@php $last = collect($curve['plot']['raw'])->last(); @endphp
|
||||
@if ($last)
|
||||
<text x="{{ round($sx($last->x) + 3, 1) }}" y="{{ round($sy($last->y), 1) }}" font-size="8" fill="var(--chart-label)">{{ $curve['name'] }}</text>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- Etykiety osi --}}
|
||||
<text x="{{ $padL - 6 }}" y="{{ $padT - 5 }}" font-size="10" fill="var(--chart-label)">{{ $yLabel }}</text>
|
||||
<text x="{{ $padL + $plotW }}" y="{{ $h - 4 }}" font-size="10" fill="var(--chart-label)" text-anchor="end">{{ $xLabel }}</text>
|
||||
</svg>
|
||||
@@ -0,0 +1,32 @@
|
||||
@props(['column', 'align' => 'left'])
|
||||
|
||||
{{--
|
||||
Klikalny nagłówek tabeli z sortowaniem.
|
||||
- 'column' : klucz sortowania (musi być na whiteliście w kontrolerze).
|
||||
- 'align' : left | right | center (domyślnie left).
|
||||
Zachowuje bieżące filtry (cały query string), resetuje paginację (?page).
|
||||
Strzałka: aktywna kolumna = kierunek (rosnąco/malejąco), pozostałe = neutralna.
|
||||
--}}
|
||||
|
||||
@php
|
||||
$activeSort = request('sort');
|
||||
$activeDir = request('dir', 'asc') === 'desc' ? 'desc' : 'asc';
|
||||
$isActive = $activeSort === $column;
|
||||
$nextDir = $isActive && $activeDir === 'asc' ? 'desc' : 'asc';
|
||||
|
||||
$query = array_merge(request()->except('page'), ['sort' => $column, 'dir' => $nextDir]);
|
||||
$url = request()->url() . '?' . http_build_query($query);
|
||||
|
||||
$alignClass = ['left' => 'text-left', 'right' => 'text-right', 'center' => 'text-center'][$align] ?? 'text-left';
|
||||
$justifyClass = ['left' => 'justify-start', 'right' => 'justify-end', 'center' => 'justify-center'][$align] ?? 'justify-start';
|
||||
|
||||
$arrow = $isActive ? ($activeDir === 'asc' ? '▲' : '▼') : '↕';
|
||||
@endphp
|
||||
|
||||
<th {{ $attributes->merge(['class' => "px-4 py-3 $alignClass"]) }}>
|
||||
<a href="{{ $url }}"
|
||||
class="group inline-flex items-center gap-1 {{ $justifyClass }} hover:text-primary transition-colors select-none">
|
||||
<span>{{ $slot }}</span>
|
||||
<span class="text-[0.85em] leading-none {{ $isActive ? 'text-primary' : 'text-on-surface-variant/40 group-hover:text-on-surface-variant' }}">{{ $arrow }}</span>
|
||||
</a>
|
||||
</th>
|
||||
@@ -0,0 +1,19 @@
|
||||
@props([
|
||||
'name',
|
||||
'label',
|
||||
'value' => null,
|
||||
'type' => 'text',
|
||||
'required' => false,
|
||||
'readonly' => false,
|
||||
])
|
||||
|
||||
<div {{ $attributes->only('class')->merge(['class' => 'space-y-1']) }}>
|
||||
<label for="{{ $name }}" class="font-data-label text-data-label text-on-surface-variant tracking-wide">
|
||||
{{ $label }}@if ($required) <span class="text-primary">*</span>@endif
|
||||
</label>
|
||||
<input id="{{ $name }}" name="{{ $name }}" type="{{ $type }}"
|
||||
value="{{ old($name, $value) }}"
|
||||
@required($required) @readonly($readonly)
|
||||
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-data-label text-on-surface transition-all
|
||||
{{ $readonly ? 'opacity-70 cursor-not-allowed' : '' }}">
|
||||
</div>
|
||||
@@ -0,0 +1,92 @@
|
||||
@props([
|
||||
'throttle' => null, // strefa oceny dławienia: green|yellow|red|null
|
||||
'loss' => null, // strefa oceny strat liniowych
|
||||
'on' => false, // czy pompa załączona
|
||||
'grey' => false, // tryb bez oceny (SPM - inne)
|
||||
'type' => 'SPM', // etykieta typu (BASE/OPT/FULL)
|
||||
])
|
||||
|
||||
@php
|
||||
// Dynamiczny schemat układu pompowego (zastępuje statyczne PNG SPM_*).
|
||||
// Kolory podstawiane wg ocen: zawór dławiący = dławienie, rurociąg = straty.
|
||||
$zc = ['green' => '#2e7d32', 'yellow' => '#f9a825', 'red' => '#c62828'];
|
||||
$valveColor = $grey ? '#9aa6a8' : ($zc[$throttle] ?? '#9aa6a8');
|
||||
$pipeColor = $grey ? '#9aa6a8' : ($zc[$loss] ?? '#9aa6a8');
|
||||
$pumpColor = $on ? '#16a34a' : '#9aa6a8';
|
||||
$label = '#2f3e45'; // stały ciemny - schemat na jasnym podkładzie w obu motywach
|
||||
@endphp
|
||||
|
||||
<svg viewBox="0 0 360 480" class="w-full h-auto max-h-[28rem] mx-auto" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="ws-ground" width="8" height="8" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="8" stroke="#b9a58c" stroke-width="2"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
{{-- ===== Instalacja powierzchniowa (tłoczenie) ===== --}}
|
||||
{{-- pion z głowicy do poziomu --}}
|
||||
<rect x="156" y="60" width="8" height="62" fill="{{ $pipeColor }}"/>
|
||||
{{-- kolano + rurociąg tłoczny poziomy --}}
|
||||
<path d="M156 64 H300" stroke="{{ $pipeColor }}" stroke-width="8" fill="none"/>
|
||||
|
||||
{{-- zawór dławiący (kolor = ocena dławienia) --}}
|
||||
<g>
|
||||
<polygon points="214,52 214,76 238,52 238,76" fill="{{ $valveColor }}" stroke="#3b3b3b" stroke-width="1.2"/>
|
||||
<line x1="226" y1="48" x2="226" y2="40" stroke="#3b3b3b" stroke-width="2"/>
|
||||
<rect x="219" y="36" width="14" height="5" rx="1" fill="#3b3b3b"/>
|
||||
</g>
|
||||
|
||||
{{-- manometry p1 / p2 --}}
|
||||
<circle cx="196" cy="46" r="9" fill="#ffffff" stroke="#3b3b3b" stroke-width="1.5"/>
|
||||
<line x1="196" y1="55" x2="196" y2="61" stroke="#3b3b3b" stroke-width="2"/>
|
||||
<text x="196" y="32" font-size="11" font-weight="600" fill="{{ $label }}" text-anchor="middle">p₁</text>
|
||||
|
||||
<circle cx="258" cy="46" r="9" fill="#ffffff" stroke="#3b3b3b" stroke-width="1.5"/>
|
||||
<line x1="258" y1="55" x2="258" y2="61" stroke="#3b3b3b" stroke-width="2"/>
|
||||
<text x="258" y="32" font-size="11" font-weight="600" fill="{{ $label }}" text-anchor="middle">p₂</text>
|
||||
|
||||
{{-- przepływomierz Q --}}
|
||||
<rect x="280" y="55" width="20" height="18" rx="2" fill="#ffffff" stroke="#3b3b3b" stroke-width="1.5"/>
|
||||
<text x="290" y="68" font-size="10" font-weight="700" fill="#3b3b3b" text-anchor="middle">Q</text>
|
||||
{{-- wylot --}}
|
||||
<path d="M300 64 H324 M318 58 L324 64 L318 70" stroke="#3b3b3b" stroke-width="2.5" fill="none"/>
|
||||
|
||||
{{-- ===== Grunt ===== --}}
|
||||
<rect x="0" y="122" width="360" height="14" fill="url(#ws-ground)"/>
|
||||
<line x1="0" y1="122" x2="360" y2="122" stroke="#8f706c" stroke-width="1.5"/>
|
||||
|
||||
{{-- ===== Otwór studni (obudowa) ===== --}}
|
||||
<line x1="138" y1="122" x2="138" y2="452" stroke="#5b6566" stroke-width="3"/>
|
||||
<line x1="182" y1="122" x2="182" y2="452" stroke="#5b6566" stroke-width="3"/>
|
||||
<line x1="138" y1="452" x2="182" y2="452" stroke="#5b6566" stroke-width="3"/>
|
||||
|
||||
{{-- woda (zwierciadło dynamiczne) --}}
|
||||
<rect x="140" y="232" width="40" height="219" fill="#cfeaf5" opacity="0.65"/>
|
||||
{{-- zwierciadło statyczne Hst (przerywane) --}}
|
||||
<line x1="140" y1="206" x2="180" y2="206" stroke="#1565c0" stroke-width="1.4" stroke-dasharray="4 3"/>
|
||||
<text x="188" y="209" font-size="10" fill="{{ $label }}">H<tspan baseline-shift="sub" font-size="7">st</tspan></text>
|
||||
{{-- zwierciadło dynamiczne Hd --}}
|
||||
<line x1="140" y1="232" x2="180" y2="232" stroke="#1565c0" stroke-width="2"/>
|
||||
<text x="188" y="235" font-size="10" fill="{{ $label }}">H<tspan baseline-shift="sub" font-size="7">d</tspan></text>
|
||||
|
||||
{{-- rurociąg pionowy (kolor = straty liniowe) --}}
|
||||
<rect x="156" y="122" width="8" height="262" fill="{{ $pipeColor }}"/>
|
||||
|
||||
{{-- ===== Agregat pompowy ===== --}}
|
||||
{{-- pompa --}}
|
||||
<rect x="150" y="384" width="20" height="38" rx="3" fill="{{ $pumpColor }}" stroke="#2f3e45" stroke-width="1.5"/>
|
||||
{{-- silnik --}}
|
||||
<rect x="152" y="422" width="16" height="22" rx="2" fill="#46555d"/>
|
||||
{{-- króciec ssawny --}}
|
||||
<line x1="160" y1="444" x2="160" y2="450" stroke="#46555d" stroke-width="3"/>
|
||||
{{-- ciśnienie tłoczne pompy Pp --}}
|
||||
<circle cx="160" cy="372" r="7" fill="#ffffff" stroke="#3b3b3b" stroke-width="1.3"/>
|
||||
<text x="172" y="362" font-size="10" fill="{{ $label }}">P<tspan baseline-shift="sub" font-size="7">p</tspan></text>
|
||||
|
||||
{{-- etykieta typu + opis (prawa strona) --}}
|
||||
<text x="214" y="150" font-size="13" font-weight="700" fill="{{ $label }}">{{ $type }}</text>
|
||||
<text x="214" y="170" font-size="9" fill="{{ $label }}">Układ mierzy:</text>
|
||||
<text x="214" y="182" font-size="9" fill="{{ $label }}">p₁, p₂, P<tspan baseline-shift="sub" font-size="6.5">p</tspan>, Q,</text>
|
||||
<text x="214" y="194" font-size="9" fill="{{ $label }}">położenie zwierciadła:</text>
|
||||
<text x="214" y="206" font-size="9" fill="{{ $label }}">H<tspan baseline-shift="sub" font-size="6.5">d</tspan>, H<tspan baseline-shift="sub" font-size="6.5">st</tspan></text>
|
||||
</svg>
|
||||
@@ -0,0 +1,25 @@
|
||||
@props([
|
||||
'name',
|
||||
'label',
|
||||
'options', // kolekcja: [klucz => etykieta] lub lista etykiet (gdy valueIsLabel)
|
||||
'selected' => null,
|
||||
'required' => false,
|
||||
'placeholder' => null,
|
||||
'valueIsLabel' => false, // true → value = etykieta (słowniki opisowe: WLL_SYS_TYPE itp.)
|
||||
])
|
||||
|
||||
<div {{ $attributes->only('class')->merge(['class' => 'space-y-1']) }}>
|
||||
<label for="{{ $name }}" class="font-data-label text-data-label text-on-surface-variant tracking-wide">
|
||||
{{ $label }}@if ($required) <span class="text-primary">*</span>@endif
|
||||
</label>
|
||||
<select id="{{ $name }}" name="{{ $name }}" @required($required)
|
||||
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-data-label text-on-surface transition-all">
|
||||
@if ($placeholder)
|
||||
<option value="">{{ $placeholder }}</option>
|
||||
@endif
|
||||
@foreach ($options as $key => $text)
|
||||
@php($optionValue = $valueIsLabel ? $text : $key)
|
||||
<option value="{{ $optionValue }}" @selected((string) old($name, $selected) === (string) $optionValue)>{{ $text }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
Reference in New Issue
Block a user