Files
MES_Core/templates/base.html
2026-04-03 01:10:05 +03:00

153 lines
7.1 KiB
HTML

{% load static %}
<!DOCTYPE html>
<html lang="ru" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}ShiftFlow MES{% endblock %}</title>
<link rel="icon" href="{% static 'favicon.svg' %}" type="image/svg+xml">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body class="d-flex flex-column min-vh-100">
{% if user.is_authenticated %}
{% include 'components/_navbar.html' %}
{% endif %}
<main class="container-fluid py-3 flex-grow-1 d-flex flex-column">
{% block content %}{% endblock %}
</main>
{% if user.is_authenticated %}
{% include 'components/_footer.html' %}
{% endif %}
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"></script>
<script>
function updateThemeIcon(theme) {
const icon = document.getElementById('theme-icon');
if (icon) icon.className = theme === 'dark' ? 'bi bi-brightness-high-fill' : 'bi bi-moon-stars-fill';
}
function toggleTheme() {
const html = document.documentElement;
const newTheme = html.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark';
html.setAttribute('data-bs-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
}
function sfParseDate(text) {
const s = (text || '').trim();
if (!s) return null;
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
const d = new Date(s + 'T00:00:00');
return isNaN(d.getTime()) ? null : d;
}
const m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{2}|\d{4})$/);
if (m) {
const dd = parseInt(m[1], 10);
const mm = parseInt(m[2], 10) - 1;
let yy = parseInt(m[3], 10);
if (yy < 100) yy += 2000;
const d = new Date(yy, mm, dd);
return isNaN(d.getTime()) ? null : d;
}
return null;
}
function sfParseNumber(text) {
const s = (text || '').toString().trim();
if (!s) return null;
const cleaned = s
.replace(/\s+/g, '')
.replace(/,/g, '.')
.replace(/[^0-9.\-]/g, '');
if (!cleaned || cleaned === '-' || cleaned === '.') return null;
const n = parseFloat(cleaned);
return isNaN(n) ? null : n;
}
function sfMakeSortable(table) {
const thead = table.querySelector('thead');
const tbody = table.querySelector('tbody');
if (!thead || !tbody) return;
const ths = Array.from(thead.querySelectorAll('th'));
ths.forEach((th, idx) => {
if ((th.getAttribute('data-sort') || '').toLowerCase() === 'false') return;
th.style.cursor = 'pointer';
th.addEventListener('click', () => {
const cur = table.getAttribute('data-sort-col');
const sameCol = cur !== null && String(idx) === String(cur);
const dir = sameCol && table.getAttribute('data-sort-dir') === 'asc' ? 'desc' : 'asc';
table.setAttribute('data-sort-col', String(idx));
table.setAttribute('data-sort-dir', dir);
const rows = Array.from(tbody.querySelectorAll('tr'));
// Комментарий: сортировка делается на клиенте. Мы просто переупорядочиваем строки в tbody.
// Это работает для всех таблиц, где разметка уже готова, без переписывания вьюх.
rows.sort((a, b) => {
const aCell = a.children[idx];
const bCell = b.children[idx];
const aText = (aCell ? aCell.textContent : '').trim();
const bText = (bCell ? bCell.textContent : '').trim();
const type = (th.getAttribute('data-sort-type') || '').toLowerCase();
if (type === 'number') {
const an = sfParseNumber(aText);
const bn = sfParseNumber(bText);
if (an === null && bn === null) return 0;
if (an === null) return dir === 'asc' ? 1 : -1;
if (bn === null) return dir === 'asc' ? -1 : 1;
return dir === 'asc' ? (an - bn) : (bn - an);
}
if (type === 'date') {
const ad = sfParseDate(aText);
const bd = sfParseDate(bText);
const at = ad ? ad.getTime() : null;
const bt = bd ? bd.getTime() : null;
if (at === null && bt === null) return 0;
if (at === null) return dir === 'asc' ? 1 : -1;
if (bt === null) return dir === 'asc' ? -1 : 1;
return dir === 'asc' ? (at - bt) : (bt - at);
}
// Попытка автоматически понять тип
const an = sfParseNumber(aText);
const bn = sfParseNumber(bText);
if (an !== null && bn !== null) return dir === 'asc' ? (an - bn) : (bn - an);
const ad = sfParseDate(aText);
const bd = sfParseDate(bText);
if (ad && bd) return dir === 'asc' ? (ad.getTime() - bd.getTime()) : (bd.getTime() - ad.getTime());
const cmp = aText.localeCompare(bText, 'ru', { numeric: true, sensitivity: 'base' });
return dir === 'asc' ? cmp : -cmp;
});
const frag = document.createDocumentFragment();
rows.forEach(r => frag.appendChild(r));
tbody.appendChild(frag);
});
});
}
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-bs-theme', savedTheme);
updateThemeIcon(savedTheme);
// Включаем сортировку для таблиц, которые явно помечены data-sortable="1".
document.querySelectorAll('table[data-sortable="1"]').forEach(sfMakeSortable);
});
</script>
</body>
</html>