Phase 108 complete (v3.2 milestone):
Plan 108-01 — Delivery Status DB & CRUD:
- Tabela delivery_statuses z seedem 11 statusow systemowych
- DeliveryStatusRepository (CRUD + per-request static cache)
- DeliveryStatus::setRepository() — DB fallback dla static final class
- Panel /settings/delivery-statuses (zakladki Statusy + Mapowanie)
- Sidebar przebudowany: Statusy zamowien + Statusy przesylek
Plan 108-02 — Automation Dropdowns z DB + UI Refactor:
- Dropdowny automatyzacji ladowane z DB (warunek shipment_status + akcja update_shipment_status)
- Walidacja przez DeliveryStatus::getAllStatuses()
- Osobna podstrona formularza CRUD (delivery-status-form.php)
- Lista uproszczona: rename Terminal -> Koncowy, usunieta kolumna Typ
- BREAKING: drop backward compat dla starych grupowych kluczy automatyzacji
- Bug fix: path params w DeliveryStatusesController via \$request->input('id')
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
283 lines
14 KiB
PHP
283 lines
14 KiB
PHP
<!doctype html>
|
|
<html lang="pl">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title><?= $e($t('meta.title_pattern', ['title' => (string) ($title ?? $t('meta.default_panel_title'))])) ?></title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="/assets/css/app.css?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/css/app.css') ?: 0 ?>">
|
|
<link rel="stylesheet" href="/assets/css/modules/jquery-alerts.css?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/css/modules/jquery-alerts.css') ?: 0 ?>">
|
|
</head>
|
|
<body>
|
|
<?php $currentMenu = (string) ($activeMenu ?? ''); ?>
|
|
<?php $currentSettings = (string) ($activeSettings ?? ''); ?>
|
|
<?php $currentOrders = (string) ($activeOrders ?? ''); ?>
|
|
<?php $currentStatistics = (string) ($activeStatistics ?? ''); ?>
|
|
<div class="app-shell" id="js-app-shell">
|
|
<aside class="sidebar" id="js-sidebar">
|
|
<div class="sidebar__brand">
|
|
<span class="sidebar__brand-text"><?= $e($t('brand.name_prefix')) ?><strong><?= $e($t('brand.name_suffix')) ?></strong></span>
|
|
<button class="sidebar__collapse-btn" id="js-sidebar-collapse" title="Zwiń menu" aria-label="Zwiń menu">
|
|
<svg class="sidebar__collapse-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M15 18l-6-6 6-6"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<nav class="sidebar__nav" aria-label="<?= $e($t('navigation.main_menu')) ?>">
|
|
<details class="sidebar__group<?= $currentMenu === 'orders' ? ' is-active' : '' ?>"<?= $currentMenu === 'orders' ? ' open' : '' ?>>
|
|
<summary class="sidebar__group-toggle">
|
|
<span class="sidebar__icon">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M9 5H7a2 2 0 00-2 2v13a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/>
|
|
<rect x="9" y="3" width="6" height="4" rx="1"/>
|
|
<path d="M9 12h6M9 16h4"/>
|
|
</svg>
|
|
</span>
|
|
<span class="sidebar__label"><?= $e($t('navigation.orders')) ?></span>
|
|
<svg class="sidebar__toggle-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M6 9l6 6 6-6"/>
|
|
</svg>
|
|
</summary>
|
|
<div class="sidebar__group-links">
|
|
<a class="sidebar__sublink<?= $currentMenu === 'orders' && $currentOrders === 'list' ? ' is-active' : '' ?>" href="/orders/list">
|
|
<?= $e($t('navigation.orders_list')) ?>
|
|
</a>
|
|
</div>
|
|
</details>
|
|
|
|
<details class="sidebar__group<?= $currentMenu === 'statistics' ? ' is-active' : '' ?>"<?= $currentMenu === 'statistics' ? ' open' : '' ?>>
|
|
<summary class="sidebar__group-toggle">
|
|
<span class="sidebar__icon">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M4 20V10"/>
|
|
<path d="M10 20V4"/>
|
|
<path d="M16 20v-8"/>
|
|
<path d="M22 20v-5"/>
|
|
</svg>
|
|
</span>
|
|
<span class="sidebar__label"><?= $e($t('navigation.statistics')) ?></span>
|
|
<svg class="sidebar__toggle-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M6 9l6 6 6-6"/>
|
|
</svg>
|
|
</summary>
|
|
<div class="sidebar__group-links">
|
|
<a class="sidebar__sublink<?= $currentMenu === 'statistics' && $currentStatistics === 'orders' ? ' is-active' : '' ?>" href="/statistics/orders">
|
|
<?= $e($t('navigation.statistics_orders')) ?>
|
|
</a>
|
|
</div>
|
|
</details>
|
|
|
|
<a class="sidebar__link<?= ($currentMenu ?? '') === 'accounting' ? ' is-active' : '' ?>" href="/accounting">
|
|
<span class="sidebar__icon">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
<polyline points="14 2 14 8 20 8"/>
|
|
<line x1="16" y1="13" x2="8" y2="13"/>
|
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
|
<polyline points="10 9 9 9 8 9"/>
|
|
</svg>
|
|
</span>
|
|
<span class="sidebar__label"><?= $e($t('navigation.accounting_section')) ?></span>
|
|
</a>
|
|
|
|
<details class="sidebar__group<?= $currentMenu === 'settings' ? ' is-active' : '' ?>"<?= $currentMenu === 'settings' ? ' open' : '' ?>>
|
|
<summary class="sidebar__group-toggle">
|
|
<span class="sidebar__icon">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="3"/>
|
|
<path d="M12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
|
</svg>
|
|
</span>
|
|
<span class="sidebar__label"><?= $e($t('navigation.settings')) ?></span>
|
|
<svg class="sidebar__toggle-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M6 9l6 6 6-6"/>
|
|
</svg>
|
|
</summary>
|
|
<div class="sidebar__group-links">
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'company' ? ' is-active' : '' ?>" href="/settings/company">
|
|
<?= $e($t('navigation.company')) ?>
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'accounting' ? ' is-active' : '' ?>" href="/settings/accounting">
|
|
<?= $e($t('navigation.accounting')) ?>
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'users' ? ' is-active' : '' ?>" href="/settings/users">
|
|
<?= $e($t('navigation.users')) ?>
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && in_array($currentSettings, ['integrations', 'allegro', 'apaczka', 'inpost', 'shoppro'], true) ? ' is-active' : '' ?>" href="/settings/integrations">
|
|
<?= $e($t('navigation.integrations')) ?>
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'statuses' ? ' is-active' : '' ?>" href="/settings/statuses">
|
|
<?= $e($t('navigation.statuses')) ?>
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'email-mailboxes' ? ' is-active' : '' ?>" href="/settings/email-mailboxes">
|
|
Skrzynki pocztowe
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'email-templates' ? ' is-active' : '' ?>" href="/settings/email-templates">
|
|
Szablony e-mail
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'automation' ? ' is-active' : '' ?>" href="/settings/automation">
|
|
Zadania automatyczne
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'cron' ? ' is-active' : '' ?>" href="/settings/cron">
|
|
<?= $e($t('navigation.cron')) ?>
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'printing' ? ' is-active' : '' ?>" href="/settings/printing">
|
|
Drukowanie
|
|
</a>
|
|
<?php
|
|
$dsmUnmappedCount = 0;
|
|
try {
|
|
$appInstance = \App\Core\Application::instance();
|
|
if ($appInstance !== null) {
|
|
$dsmRepo = new \App\Modules\Shipments\DeliveryStatusMappingRepository($appInstance->db());
|
|
$dsmUnmappedCount = $dsmRepo->countAllUnmappedForBadge();
|
|
}
|
|
} catch (\Throwable) {
|
|
$dsmUnmappedCount = 0;
|
|
}
|
|
?>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && in_array($currentSettings, ['delivery-statuses', 'delivery-status-mappings'], true) ? ' is-active' : '' ?>" href="/settings/delivery-statuses">
|
|
<?= $e($t('navigation.delivery_statuses')) ?>
|
|
<?php if ($dsmUnmappedCount > 0): ?>
|
|
<span class="sidebar__badge" title="Niezmapowane statusy"><?= (int) $dsmUnmappedCount ?></span>
|
|
<?php endif; ?>
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'project-mappings' ? ' is-active' : '' ?>" href="/settings/project-mappings">
|
|
<?= $e($t('navigation.project_mapping')) ?>
|
|
</a>
|
|
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'database' ? ' is-active' : '' ?>" href="/settings/database">
|
|
<?= $e($t('navigation.database')) ?>
|
|
</a>
|
|
</div>
|
|
</details>
|
|
</nav>
|
|
</aside>
|
|
|
|
<div class="sidebar-backdrop" id="js-sidebar-backdrop"></div>
|
|
|
|
<div class="app-main">
|
|
<header class="topbar">
|
|
<button class="topbar__hamburger" id="js-hamburger" type="button" aria-label="Otwórz menu">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="3" y1="6" x2="21" y2="6"/>
|
|
<line x1="3" y1="12" x2="21" y2="12"/>
|
|
<line x1="3" y1="18" x2="21" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
<div class="global-search" id="js-global-search-wrap">
|
|
<input type="text" id="js-global-search" class="global-search__input" placeholder="Szukaj zamówień..." autocomplete="off">
|
|
<div class="global-search__results" id="js-global-search-results"></div>
|
|
</div>
|
|
<div>
|
|
<strong><?= $e((string) (($user['name'] ?? '') !== '' ? $user['name'] : ($user['email'] ?? ''))) ?></strong>
|
|
</div>
|
|
<form action="/logout" method="post">
|
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
|
<button type="submit" class="btn btn--secondary"><?= $e($t('actions.logout')) ?></button>
|
|
</form>
|
|
</header>
|
|
|
|
<main class="container">
|
|
<?= $content ?>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<script src="/assets/js/modules/jquery-alerts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/jquery-alerts.js') ?: 0 ?>"></script>
|
|
<script src="/assets/js/modules/global-search.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/global-search.js') ?: 0 ?>"></script>
|
|
<script>
|
|
(function () {
|
|
var STORAGE_KEY = 'sidebarCollapsed';
|
|
var sidebar = document.getElementById('js-sidebar');
|
|
var collapseBtn = document.getElementById('js-sidebar-collapse');
|
|
if (!sidebar || !collapseBtn) return;
|
|
|
|
function setCollapsed(collapsed) {
|
|
sidebar.classList.toggle('is-collapsed', collapsed);
|
|
collapseBtn.setAttribute('title', collapsed ? 'Rozwiń menu' : 'Zwiń menu');
|
|
collapseBtn.setAttribute('aria-label', collapsed ? 'Rozwiń menu' : 'Zwiń menu');
|
|
if (collapsed) {
|
|
sidebar.querySelectorAll('details[open]').forEach(function (det) {
|
|
det.removeAttribute('open');
|
|
});
|
|
}
|
|
try { localStorage.setItem(STORAGE_KEY, collapsed ? '1' : '0'); } catch (e) {}
|
|
}
|
|
|
|
try {
|
|
if (localStorage.getItem(STORAGE_KEY) === '1') setCollapsed(true);
|
|
} catch (e) {}
|
|
|
|
collapseBtn.addEventListener('click', function () {
|
|
setCollapsed(!sidebar.classList.contains('is-collapsed'));
|
|
});
|
|
|
|
sidebar.querySelectorAll('details > summary').forEach(function (summary) {
|
|
summary.addEventListener('click', function (e) {
|
|
if (sidebar.classList.contains('is-collapsed')) {
|
|
e.preventDefault();
|
|
setCollapsed(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Mobile menu
|
|
var hamburger = document.getElementById('js-hamburger');
|
|
var backdrop = document.getElementById('js-sidebar-backdrop');
|
|
var mobileQuery = window.matchMedia('(max-width: 768px)');
|
|
|
|
function closeMobileMenu() {
|
|
sidebar.classList.remove('is-mobile-open');
|
|
backdrop.classList.remove('is-visible');
|
|
document.body.classList.remove('no-scroll');
|
|
}
|
|
|
|
function openMobileMenu() {
|
|
sidebar.classList.add('is-mobile-open');
|
|
backdrop.classList.add('is-visible');
|
|
document.body.classList.add('no-scroll');
|
|
}
|
|
|
|
if (hamburger) {
|
|
hamburger.addEventListener('click', function () {
|
|
if (sidebar.classList.contains('is-mobile-open')) {
|
|
closeMobileMenu();
|
|
} else {
|
|
openMobileMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (backdrop) {
|
|
backdrop.addEventListener('click', closeMobileMenu);
|
|
}
|
|
|
|
// Close button (collapse-btn acts as X on mobile)
|
|
collapseBtn.addEventListener('click', function () {
|
|
if (mobileQuery.matches) {
|
|
closeMobileMenu();
|
|
}
|
|
});
|
|
|
|
// Close on navigation link click (mobile only)
|
|
sidebar.querySelectorAll('.sidebar__link, .sidebar__sublink').forEach(function (link) {
|
|
link.addEventListener('click', function () {
|
|
if (mobileQuery.matches) {
|
|
closeMobileMenu();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Reset state on resize to desktop
|
|
mobileQuery.addEventListener('change', function (e) {
|
|
if (!e.matches) {
|
|
closeMobileMenu();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|