nordabiz/templates/release_notes.html
Maciej Pienczyn 4181a2e760 refactor: Migrate access control from is_admin to role-based system
Replace ~170 manual `if not current_user.is_admin` checks with:
- @role_required(SystemRole.ADMIN) for user management, security, ZOPK
- @role_required(SystemRole.OFFICE_MANAGER) for content management
- current_user.can_access_admin_panel() for admin UI access
- current_user.can_moderate_forum() for forum moderation
- current_user.can_edit_company(id) for company permissions

Add @office_manager_required decorator shortcut.
Add SQL migration to sync existing users' role field.

Role hierarchy: UNAFFILIATED(10) < MEMBER(20) < EMPLOYEE(30) < MANAGER(40) < OFFICE_MANAGER(50) < ADMIN(100)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:05:22 +01:00

526 lines
16 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Historia zmian - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.page-header {
margin-bottom: var(--spacing-xl);
text-align: center;
}
.page-header h1 {
font-size: var(--font-size-3xl);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.page-header p {
color: var(--text-secondary);
font-size: var(--font-size-lg);
}
.releases-container {
max-width: 800px;
margin: 0 auto;
}
.release-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
position: relative;
}
.release-card::before {
content: '';
position: absolute;
left: -20px;
top: 30px;
width: 12px;
height: 12px;
background: var(--primary);
border-radius: 50%;
border: 3px solid var(--surface);
box-shadow: 0 0 0 2px var(--primary);
}
.releases-timeline {
border-left: 2px solid var(--border);
padding-left: var(--spacing-xl);
margin-left: var(--spacing-sm);
}
.release-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-md);
flex-wrap: wrap;
gap: var(--spacing-sm);
}
.release-version {
font-size: var(--font-size-xl);
font-weight: 600;
color: var(--text-primary);
}
.release-date {
font-size: var(--font-size-sm);
color: var(--text-secondary);
background: var(--background);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
}
.release-badges {
display: flex;
gap: var(--spacing-xs);
flex-wrap: wrap;
}
.release-badge {
font-size: var(--font-size-xs);
padding: 2px 8px;
border-radius: var(--radius);
font-weight: 500;
text-transform: uppercase;
}
.badge-new {
background: #dcfce7;
color: #166534;
}
.badge-fix {
background: #fee2e2;
color: #991b1b;
}
.badge-improve {
background: #dbeafe;
color: #1e40af;
}
.badge-beta {
background: #fef3c7;
color: #92400e;
}
.release-changes {
margin-top: var(--spacing-md);
}
.change-category {
margin-bottom: var(--spacing-md);
}
.change-category h4 {
font-size: var(--font-size-sm);
color: var(--text-secondary);
text-transform: uppercase;
margin-bottom: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.change-category h4::before {
font-size: var(--font-size-base);
}
.change-category.new h4::before { content: '✨'; }
.change-category.fix h4::before { content: '🔧'; }
.change-category.improve h4::before { content: '⚡'; }
.change-category.security h4::before { content: '🔒'; }
.change-category.beta h4::before { content: '🧪'; }
.change-list {
list-style: none;
padding: 0;
margin: 0;
}
.change-list li {
padding: var(--spacing-xs) 0;
padding-left: var(--spacing-md);
position: relative;
color: var(--text-primary);
font-size: var(--font-size-sm);
}
.change-list li::before {
content: '•';
position: absolute;
left: 0;
color: var(--text-secondary);
}
.stats-banner {
background: linear-gradient(135deg, var(--primary), var(--primary-dark, #1a56db));
color: white;
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--spacing-lg);
text-align: center;
}
.notify-btn {
background: var(--primary);
color: white;
border: none;
padding: 6px 12px;
border-radius: var(--radius);
font-size: var(--font-size-xs);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
transition: background 0.2s;
}
.notify-btn:hover {
background: var(--primary-dark, #1a56db);
}
.notify-btn:disabled {
background: var(--text-secondary);
cursor: not-allowed;
}
.notify-btn.sent {
background: #16a34a;
}
.stat-item h3 {
font-size: var(--font-size-3xl);
font-weight: 700;
margin-bottom: var(--spacing-xs);
}
.stat-item p {
font-size: var(--font-size-sm);
opacity: 0.9;
}
@media (max-width: 768px) {
.releases-timeline {
padding-left: var(--spacing-md);
margin-left: 0;
}
.release-card::before {
left: -15px;
}
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>📋 Historia zmian</h1>
<p>Dziennik rozwoju platformy Norda Biznes Partner</p>
</div>
<div class="stats-banner">
<div class="stat-item">
<h3>{{ stats.companies }}</h3>
<p>Firm w katalogu</p>
</div>
<div class="stat-item">
<h3>{{ stats.categories }}</h3>
<p>Kategorii branżowych</p>
</div>
<div class="stat-item">
<h3>{{ releases|length }}</h3>
<p>Aktualizacji</p>
</div>
<div class="stat-item">
<h3>2025</h3>
<p>Rok startu</p>
</div>
</div>
<div class="releases-container">
<div class="releases-timeline">
{% for release in releases %}
<div class="release-card">
<div class="release-header">
<div>
<div class="release-version">{{ release.version }}</div>
<div class="release-badges">
{% for badge in release.badges %}
<span class="release-badge badge-{{ badge }}">{{ badge }}</span>
{% endfor %}
</div>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<div class="release-date">{{ release.date }}</div>
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
<button class="notify-btn" onclick="notifyRelease('{{ release.version }}', this)" title="Wyślij powiadomienia o tej wersji">
🔔 Powiadom
</button>
{% endif %}
</div>
</div>
<div class="release-changes">
{% if release.new %}
<div class="change-category new">
<h4>Nowości</h4>
<ul class="change-list">
{% for item in release.new %}
<li>{{ item|safe }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if release.improve %}
<div class="change-category improve">
<h4>Ulepszenia</h4>
<ul class="change-list">
{% for item in release.improve %}
<li>{{ item|safe }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if release.fix %}
<div class="change-category fix">
<h4>Naprawione</h4>
<ul class="change-list">
{% for item in release.fix %}
<li>{{ item|safe }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if release.security %}
<div class="change-category security">
<h4>Bezpieczeństwo</h4>
<ul class="change-list">
{% for item in release.security %}
<li>{{ item|safe }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if release.beta %}
<div class="change-category beta">
<h4>W fazie testów (BETA)</h4>
<ul class="change-list">
{% for item in release.beta %}
<li>{{ item|safe }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Confirm Modal -->
<div class="confirm-modal-overlay" id="confirmModal">
<div class="confirm-modal">
<div class="confirm-modal-icon">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
</div>
<h3 class="confirm-modal-title" id="confirmTitle">Wyślij powiadomienia</h3>
<p class="confirm-modal-message" id="confirmMessage">Czy na pewno chcesz wysłać powiadomienia?</p>
<p class="confirm-modal-info" id="confirmInfo"></p>
<div class="confirm-modal-actions">
<button type="button" class="btn btn-outline" onclick="closeConfirmModal()">Anuluj</button>
<button type="button" class="btn btn-primary" id="confirmButton">Wyślij</button>
</div>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.confirm-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
justify-content: center;
align-items: center;
animation: fadeIn 0.2s ease;
}
.confirm-modal-overlay.active {
display: flex;
}
.confirm-modal {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
max-width: 420px;
width: 90%;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.confirm-modal-icon {
width: 56px;
height: 56px;
border-radius: 50%;
background: #dbeafe;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto var(--spacing-md);
}
.confirm-modal-icon svg {
width: 28px;
height: 28px;
color: #2563eb;
}
.confirm-modal-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.confirm-modal-message {
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.confirm-modal-info {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
}
.confirm-modal-actions {
display: flex;
gap: var(--spacing-md);
justify-content: center;
}
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
.toast.success { border-left-color: var(--success); }
.toast.error { border-left-color: var(--error); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
</style>
{% endblock %}
{% block extra_js %}
// Toast function
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toastContainer');
const icons = { success: '✓', error: '✕', warning: '⚠', info: '' };
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||''}</span><span>${message}</span>`;
container.appendChild(toast);
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
}
// Confirm modal
let confirmCallback = null;
function showConfirmModal(title, message, info, onConfirm) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
document.getElementById('confirmInfo').textContent = info || '';
document.getElementById('confirmModal').classList.add('active');
confirmCallback = onConfirm;
}
function closeConfirmModal() {
document.getElementById('confirmModal').classList.remove('active');
confirmCallback = null;
}
document.getElementById('confirmButton').addEventListener('click', function() {
if (confirmCallback) {
confirmCallback();
}
closeConfirmModal();
});
document.getElementById('confirmModal').addEventListener('click', function(e) {
if (e.target === this) {
closeConfirmModal();
}
});
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
function notifyRelease(version, btn) {
showConfirmModal(
'Wyślij powiadomienia',
`Czy na pewno chcesz wysłać powiadomienia o wersji ${version}?`,
'Powiadomienie pojawi się przy ikonce dzwoneczka u wszystkich użytkowników.',
function() {
btn.disabled = true;
btn.innerHTML = '⏳ Wysyłanie...';
// Get first 3 highlights from the release card
const card = btn.closest('.release-card');
const highlights = [];
const items = card.querySelectorAll('.change-category.new .change-list li');
items.forEach((item, i) => {
if (i < 3) highlights.push(item.textContent.trim());
});
fetch('/admin/notify-release', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({
version: version,
highlights: highlights
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
btn.innerHTML = '✅ Wysłano';
btn.classList.add('sent');
showToast(data.message, 'success');
} else {
btn.innerHTML = '❌ Błąd';
btn.disabled = false;
showToast('Błąd: ' + data.error, 'error');
}
})
.catch(error => {
btn.innerHTML = '❌ Błąd';
btn.disabled = false;
showToast('Błąd połączenia', 'error');
});
}
);
}
{% endif %}
{% endblock %}