Files
Jo 4206a8934e
Deploy via Portainer / deploy (push) Successful in 1s
fix: mobile menu styling + nextcloud search/mtime/bambu hint
2026-07-07 11:13:10 +02:00

321 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}3D Pricing{% endblock %} — 3D Pricing</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--sidebar-w: 220px;
--sidebar-collapsed-w: 52px;
--sidebar-transition: width .2s ease, margin-left .2s ease;
}
body { background: #f4f6f9; }
#sidebar {
width: var(--sidebar-w); min-height: 100vh;
background: #1a1d23; position: fixed; top: 0; left: 0;
display: flex; flex-direction: column;
transition: width .2s ease;
overflow: hidden;
z-index: 1000;
}
#sidebar.collapsed { width: var(--sidebar-collapsed-w); }
#sidebar .brand {
padding: 1rem 1.25rem; color: #fff;
font-weight: 700; font-size: 1.1rem;
border-bottom: 1px solid #2e323d;
display: flex; align-items: center; justify-content: space-between;
white-space: nowrap; position: relative;
}
#sidebar .brand .brand-text { transition: opacity .15s; }
#sidebar.collapsed .brand .brand-text { opacity: 0; pointer-events: none; }
#sidebar .brand span { color: #ff6b35; }
#sidebarToggle {
background: none; border: none; color: #9ba3af; cursor: pointer;
padding: .2rem .3rem; border-radius: 4px; flex-shrink: 0;
transition: color .15s, background .15s, right .2s ease;
font-size: 1rem; line-height: 1;
position: relative; z-index: 1;
}
#sidebarToggle:hover { color: #fff; background: #2e323d; }
/* En mode replié, centrer le bouton dans les 52px */
#sidebar.collapsed #sidebarToggle {
position: absolute;
left: 50%; transform: translateX(-50%);
}
#sidebar nav { flex: 1; padding: .5rem 0; overflow: hidden; }
#sidebar nav a {
display: flex; align-items: center; gap: .65rem;
padding: .6rem 1.25rem; color: #9ba3af;
text-decoration: none; font-size: .875rem;
transition: background .15s, color .15s;
white-space: nowrap;
}
#sidebar nav a:hover, #sidebar nav a.active { background: #2e323d; color: #fff; }
#sidebar nav a.active { border-left: 3px solid #ff6b35; }
#sidebar nav a i { flex-shrink: 0; font-size: 1rem; width: 1.1rem; text-align: center; }
#sidebar nav .nav-label { transition: opacity .15s; }
#sidebar.collapsed nav .nav-label { opacity: 0; }
#sidebar nav .nav-section {
padding: .75rem 1.25rem .25rem; font-size: .7rem;
text-transform: uppercase; letter-spacing: .08em; color: #4b5563;
transition: opacity .15s;
}
#sidebar.collapsed nav .nav-section { opacity: 0; }
#sidebar .sidebar-user { transition: opacity .15s; }
#sidebar.collapsed .sidebar-user { opacity: 0; pointer-events: none; }
#main {
margin-left: var(--sidebar-w);
padding: 1.75rem 2rem;
transition: margin-left .2s ease;
min-width: 0;
}
#main.sidebar-collapsed { margin-left: var(--sidebar-collapsed-w); }
.page-header { margin-bottom: 1.5rem; }
.page-header h1 { font-size: 1.4rem; font-weight: 700; margin: 0; }
.stat-card {
background: #fff; border-radius: 10px; padding: 1.25rem 1.5rem;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
.stat-card .label { font-size: .75rem; color: #6b7280; text-transform: uppercase; letter-spacing: .05em; }
.stat-card .value { font-size: 1.75rem; font-weight: 700; margin-top: .1rem; }
.card { border: none; border-radius: 10px; box-shadow: 0 1px 4px rgba(0,0,0,.06); }
.card-header { background: #fff; border-bottom: 1px solid #f0f0f0; font-weight: 600; }
.breakdown-row { display: flex; justify-content: space-between; padding: .2rem 0; font-size: .88rem; }
.breakdown-row.total { font-weight: 700; font-size: 1rem; border-top: 2px solid #dee2e6; padding-top: .5rem; margin-top: .3rem; }
.breakdown-row.subtotal { border-top: 1px solid #dee2e6; padding-top: .4rem; margin-top: .2rem; }
.breakdown-row.section-header { font-weight: 600; font-size: .8rem; text-transform: uppercase;
letter-spacing: .04em; color: #6b7280; padding-top: .6rem; margin-top: .2rem; border-top: 1px dashed #e5e7eb; }
.breakdown-muted { color: #6b7280; }
.final-price { font-size: 2rem; font-weight: 700; color: #ff6b35; }
.stock-ok { color: #22c55e; }
.stock-low { color: #f59e0b; }
.stock-empty { color: #ef4444; }
/* ── Mobile offcanvas nav — mêmes styles que #sidebar nav ──────────── */
#mobileSidebar nav { flex: 1; padding: .5rem 0; }
#mobileSidebar nav a {
display: flex; align-items: center; gap: .65rem;
padding: .6rem 1.25rem; color: #9ba3af;
text-decoration: none; font-size: .875rem;
transition: background .15s, color .15s;
white-space: nowrap;
}
#mobileSidebar nav a:hover,
#mobileSidebar nav a.active { background: #2e323d; color: #fff; }
#mobileSidebar nav a.active { border-left: 3px solid #ff6b35; }
#mobileSidebar nav a i { flex-shrink: 0; font-size: 1rem; width: 1.1rem; text-align: center; }
#mobileSidebar nav .nav-label { opacity: 1 !important; pointer-events: auto !important; }
#mobileSidebar nav .nav-section {
padding: .75rem 1.25rem .25rem; font-size: .7rem;
text-transform: uppercase; letter-spacing: .08em; color: #4b5563;
opacity: 1 !important;
}
#mobileSidebar .sidebar-user {
padding: .75rem 1.25rem; border-top: 1px solid #2e323d;
opacity: 1 !important; pointer-events: auto !important;
}
#mobileSidebar .sidebar-user div { font-size: .75rem; color: #9ba3af; }
#mobileSidebar .sidebar-user a {
font-size: .72rem; color: #6b7280; text-decoration: none; display: block; margin-top: .25rem;
}
/* ── Mobile responsive ───────────────────────────────────────────────── */
@media (max-width: 767.98px) {
#sidebar { display: none !important; }
#main { margin-left: 0 !important; padding: 1rem 0.75rem; }
/* Empêcher le zoom auto iOS sur les inputs */
input, select, textarea { font-size: max(1rem, 16px) !important; }
.card { border-radius: 8px; }
.stat-card { padding: 1rem; }
.stat-card .value { font-size: 1.4rem; }
.final-price { font-size: 1.5rem; }
}
</style>
</head>
<body>
<!-- ── Mobile top bar (d-md-none = visible seulement < 768px) ─────────── -->
<nav id="mobileTopBar"
class="d-flex d-md-none align-items-center sticky-top px-3 gap-2"
style="background:#1a1d23;height:52px;z-index:1100">
<button class="btn p-1" style="color:#9ba3af;font-size:1.3rem;line-height:1"
data-bs-toggle="offcanvas" data-bs-target="#mobileSidebar" aria-label="Menu">
<i class="bi bi-list"></i>
</button>
<span class="text-white fw-bold">
<i class="bi bi-layers-half me-1"></i>3D<span style="color:#ff6b35">Pricing</span>
</span>
</nav>
<!-- ── Mobile offcanvas sidebar ──────────────────────────────────────── -->
<div class="offcanvas offcanvas-start" tabindex="-1" id="mobileSidebar"
style="background:#1a1d23;width:240px">
<div class="offcanvas-header" style="border-bottom:1px solid #2e323d;padding:.75rem 1.25rem">
<span class="text-white fw-bold">
<i class="bi bi-layers-half me-1"></i>3D<span style="color:#ff6b35">Pricing</span>
</span>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas"></button>
</div>
<div class="offcanvas-body p-0" style="overflow-y:auto">
<div id="mobileNavInner"></div>
</div>
</div>
<div id="sidebar">
<div class="brand">
<span class="brand-text"><i class="bi bi-layers-half"></i> 3D<span>Pricing</span></span>
<button id="sidebarToggle" title="Réduire / Agrandir">
<i class="bi bi-layout-sidebar-reverse" id="sidebarToggleIcon"></i>
</button>
</div>
<nav>
<div class="nav-section">Navigation</div>
<a href="{{ url_for('dashboard') }}" class="{% if request.endpoint=='dashboard' %}active{% endif %}" title="Dashboard">
<i class="bi bi-speedometer2"></i><span class="nav-label"> Dashboard</span>
</a>
<a href="{{ url_for('jobs') }}" class="{% if request.endpoint in ['jobs','job_detail'] %}active{% endif %}" title="Commandes">
<i class="bi bi-printer"></i><span class="nav-label"> Commandes</span>
</a>
<a href="{{ url_for('new_job') }}" class="{% if request.endpoint=='new_job' %}active{% endif %}" title="Nouveau calcul">
<i class="bi bi-plus-circle"></i><span class="nav-label"> Nouveau calcul</span>
</a>
<a href="{{ url_for('clients') }}" class="{% if 'client' in request.endpoint %}active{% endif %}" title="Clients">
<i class="bi bi-building"></i><span class="nav-label"> Clients</span>
</a>
<a href="{{ url_for('materials') }}" class="{% if 'material' in request.endpoint %}active{% endif %}" title="Matières">
<i class="bi bi-boxes"></i><span class="nav-label"> Matières</span>
</a>
<a href="{{ url_for('profiles') }}" class="{% if 'profile' in request.endpoint %}active{% endif %}" title="Templates">
<i class="bi bi-bookmark-star"></i><span class="nav-label"> Templates</span>
</a>
<a href="{{ url_for('projects') }}" class="{% if request.endpoint in ['projects','new_project','edit_project','project_detail'] %}active{% endif %}" title="Projets">
<i class="bi bi-folder2-open"></i><span class="nav-label"> Projets</span>
</a>
<a href="{{ url_for('planning') }}" class="{% if request.endpoint in ['planning','printers_page','working_schedule','calendar_blocks'] %}active{% endif %}" title="Planning">
<i class="bi bi-calendar3"></i><span class="nav-label"> Planning</span>
</a>
<div class="nav-section">Analyse</div>
<a href="{{ url_for('stats') }}" class="{% if request.endpoint=='stats' %}active{% endif %}" title="Statistiques">
<i class="bi bi-bar-chart-line"></i><span class="nav-label"> Statistiques</span>
</a>
<a href="{{ url_for('export_csv') }}" title="Export CSV">
<i class="bi bi-download"></i><span class="nav-label"> Export CSV</span>
</a>
<div class="nav-section">Config</div>
<a href="{{ url_for('settings') }}" class="{% if request.endpoint=='settings' %}active{% endif %}" title="Paramètres">
<i class="bi bi-sliders"></i><span class="nav-label"> Paramètres</span>
</a>
<a href="{{ url_for('mobile_quick') }}" class="{% if request.endpoint=='mobile_quick' %}active{% endif %}" title="Vue mobile">
<i class="bi bi-phone"></i><span class="nav-label"> Vue mobile</span>
</a>
</nav>
{% if session.user %}
<div class="sidebar-user" style="padding:.75rem 1.25rem;border-top:1px solid #2e323d;margin-top:auto">
<div style="font-size:.75rem;color:#9ba3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"
title="{{ session.user.email }}">
<i class="bi bi-person-circle me-1"></i>{{ session.user.name }}
</div>
<a href="{{ url_for('logout') }}"
style="font-size:.72rem;color:#6b7280;text-decoration:none;display:block;margin-top:.25rem">
<i class="bi bi-box-arrow-left me-1"></i>Déconnexion
</a>
</div>
{% endif %}
</div>
<div id="main">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show">
{{ msg }} <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
(function () {
const SIDEBAR_KEY = 'sidebar_collapsed';
const sidebar = document.getElementById('sidebar');
const main = document.getElementById('main');
const btn = document.getElementById('sidebarToggle');
const icon = document.getElementById('sidebarToggleIcon');
function applyState(collapsed, animate) {
if (!animate) {
sidebar.style.transition = 'none';
main.style.transition = 'none';
}
sidebar.classList.toggle('collapsed', collapsed);
main.classList.toggle('sidebar-collapsed', collapsed);
icon.className = collapsed
? 'bi bi-layout-sidebar'
: 'bi bi-layout-sidebar-reverse';
if (!animate) {
// Force reflow then restore transition
sidebar.offsetHeight;
sidebar.style.transition = '';
main.style.transition = '';
}
}
// Restore state without animation on page load
const stored = localStorage.getItem(SIDEBAR_KEY) === 'true';
applyState(stored, false);
btn.addEventListener('click', () => {
const nowCollapsed = !sidebar.classList.contains('collapsed');
applyState(nowCollapsed, true);
localStorage.setItem(SIDEBAR_KEY, nowCollapsed);
});
})();
</script>
<!-- ── Peuple l'offcanvas mobile depuis le sidebar desktop ───────────── -->
<script>
(function () {
const sidebarNav = document.querySelector('#sidebar nav');
const sidebarUser = document.querySelector('#sidebar .sidebar-user');
const mobileInner = document.getElementById('mobileNavInner');
if (!sidebarNav || !mobileInner) return;
// Clone le nav et force l'affichage des labels (jamais en mode collapsed)
const navClone = sidebarNav.cloneNode(true);
navClone.querySelectorAll('.nav-label, .nav-section').forEach(el => {
el.style.opacity = '1';
el.style.pointerEvents = 'auto';
});
// Ferme l'offcanvas au clic sur un lien
navClone.querySelectorAll('a').forEach(a => a.setAttribute('data-bs-dismiss', 'offcanvas'));
mobileInner.appendChild(navClone);
// Clone la zone utilisateur en bas
if (sidebarUser) {
const userClone = sidebarUser.cloneNode(true);
userClone.style.cssText += ';opacity:1;pointer-events:auto;margin-top:auto';
userClone.querySelectorAll('a').forEach(a => a.setAttribute('data-bs-dismiss', 'offcanvas'));
mobileInner.appendChild(userClone);
}
})();
</script>
{% block scripts %}{% endblock %}
</body>
</html>