296 lines
16 KiB
PHP
296 lines
16 KiB
PHP
@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>
|