All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
153 lines
7.1 KiB
HTML
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> |