Enable configuration of an Imprint.

This commit is contained in:
2025-07-06 17:49:14 +02:00
parent 5b857a9a33
commit b08ae5feca
6 changed files with 315 additions and 12 deletions

24
app.py
View File

@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file, abort
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings
from data_formatting import (
format_duration, prepare_export_data, prepare_team_hours_export_data,
@@ -1583,6 +1583,22 @@ def verify_2fa():
def about():
return render_template('about.html', title='About')
@app.route('/imprint')
def imprint():
"""Display the imprint/legal page if enabled"""
branding = BrandingSettings.get_current()
# Check if imprint is enabled
if not branding or not branding.imprint_enabled:
abort(404)
title = branding.imprint_title or 'Imprint'
content = branding.imprint_content or ''
return render_template('imprint.html',
title=title,
content=content)
@app.route('/contact', methods=['GET', 'POST'])
@login_required
def contact():
@@ -2483,6 +2499,12 @@ def system_admin_branding():
branding.app_name = request.form.get('app_name', g.branding.app_name).strip()
branding.logo_alt_text = request.form.get('logo_alt_text', '').strip()
branding.primary_color = request.form.get('primary_color', '#007bff').strip()
# Handle imprint settings
branding.imprint_enabled = 'imprint_enabled' in request.form
branding.imprint_title = request.form.get('imprint_title', 'Imprint').strip()
branding.imprint_content = request.form.get('imprint_content', '').strip()
branding.updated_by_id = g.user.id
# Handle logo upload

View File

@@ -277,6 +277,11 @@ class BrandingSettings(db.Model):
favicon_filename = db.Column(db.String(255), nullable=True) # Filename of uploaded favicon
primary_color = db.Column(db.String(7), nullable=True, default='#007bff') # Hex color
# Imprint/Legal page settings
imprint_enabled = db.Column(db.Boolean, default=False) # Enable/disable imprint page
imprint_title = db.Column(db.String(200), nullable=True, default='Imprint') # Page title
imprint_content = db.Column(db.Text, nullable=True) # HTML content for imprint page
# Meta fields
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)

View File

@@ -563,6 +563,51 @@ footer {
transition: margin-left 0.3s ease;
}
.site-footer {
border-top: 1px solid #e0e0e0;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.footer-info {
font-size: 0.875rem;
color: #666;
}
.footer-info p {
margin: 0;
}
.footer-links {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.footer-links a {
color: #666;
text-decoration: none;
transition: color 0.2s;
}
.footer-links a:hover {
color: var(--primary-color);
}
.footer-separator {
color: #999;
margin: 0 0.5rem;
}
/* Full width footer when no user (no sidebar) */
body:not(.has-user) footer {
margin-left: 0;
@@ -1280,6 +1325,11 @@ input[type="time"]::-webkit-datetime-edit {
footer {
margin-left: 0;
}
.footer-content {
flex-direction: column;
text-align: center;
}
.mobile-overlay.active {
display: block;

99
templates/imprint.html Normal file
View File

@@ -0,0 +1,99 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="imprint-page">
<h1>{{ title }}</h1>
<div class="imprint-content">
{{ content|safe }}
</div>
<div class="imprint-footer">
<a href="{{ url_for('home') }}" class="btn btn-secondary">← Back to Home</a>
</div>
</div>
</div>
<style>
.imprint-page {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.imprint-page h1 {
margin-bottom: 2rem;
color: #333;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 1rem;
}
.imprint-content {
margin-bottom: 3rem;
line-height: 1.8;
color: #555;
}
.imprint-content h2 {
margin-top: 2rem;
margin-bottom: 1rem;
color: #333;
}
.imprint-content h3 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #444;
}
.imprint-content p {
margin-bottom: 1rem;
}
.imprint-content a {
color: var(--primary-color);
text-decoration: none;
}
.imprint-content a:hover {
text-decoration: underline;
}
.imprint-content ul,
.imprint-content ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
.imprint-content li {
margin-bottom: 0.5rem;
}
.imprint-content strong {
font-weight: 600;
color: #333;
}
.imprint-footer {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid #e0e0e0;
text-align: center;
}
@media (max-width: 768px) {
.imprint-page {
padding: 1rem;
margin: 1rem;
}
.imprint-page h1 {
font-size: 1.5rem;
}
}
</style>
{% endblock %}

View File

@@ -66,9 +66,6 @@
{{ g.branding.app_name if g.branding else 'TimeTrack' }}
{% endif %}
</a>
{% if g.company %}
<small class="company-name">{{ g.company.name }}</small>
{% endif %}
</div>
<button class="mobile-nav-toggle" id="mobile-nav-toggle">
<span></span>
@@ -82,7 +79,7 @@
{% if g.user %}
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<h2>
<!-- <h2>
<a href="{{ url_for('home') }}">
{% if g.branding and g.branding.logo_filename %}
<img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}"
@@ -92,12 +89,8 @@
{{ g.branding.app_name if g.branding else 'TimeTrack' }}
{% endif %}
</a>
-->
</h2>
{% if g.company %}
<div class="company-info">
<small class="text-muted">{{ g.company.name }}</small>
</div>
{% endif %}
<button class="sidebar-toggle" id="sidebar-toggle">
<span></span>
<span></span>
@@ -216,8 +209,19 @@
{% block content %}{% endblock %}
</main>
<footer>
<p>&copy; {{ current_year }} {{ g.branding.app_name }}. All rights reserved.</p>
<footer class="site-footer">
<div class="footer-content">
<div class="footer-info">
<p>&copy; {{ current_year }} {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% if g.company %} - {{ g.company.name }}{% endif %}. All rights reserved.</p>
</div>
<div class="footer-links">
<a href="{{ url_for('about') }}">About</a>
{% if g.branding and g.branding.imprint_enabled %}
<span class="footer-separator"></span>
<a href="{{ url_for('imprint') }}">{{ g.branding.imprint_title or 'Imprint' }}</a>
{% endif %}
</div>
</div>
</footer>
<script src="{{ url_for('static', filename='js/script.js') }}"></script>

View File

@@ -136,6 +136,46 @@
</div>
</div>
<!-- Imprint/Legal Page -->
<div class="form-section">
<h3>⚖️ Imprint / Legal Page</h3>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" name="imprint_enabled" id="imprint_enabled"
{% if branding.imprint_enabled %}checked{% endif %}>
<span class="toggle-slider"></span>
<span class="toggle-text">Enable Imprint Page</span>
</label>
<small class="form-text text-muted">
When enabled, an "Imprint" link will appear in the footer linking to your custom legal page.
</small>
</div>
<div class="imprint-settings" id="imprint-settings" style="{% if not branding.imprint_enabled %}display: none;{% endif %}">
<div class="form-group">
<label for="imprint_title">Page Title</label>
<input type="text" id="imprint_title" name="imprint_title"
value="{{ branding.imprint_title or 'Imprint' }}"
class="form-control"
placeholder="Imprint">
<small class="form-text text-muted">
The title that will be displayed on the imprint page.
</small>
</div>
<div class="form-group">
<label for="imprint_content">Page Content (HTML supported)</label>
<textarea id="imprint_content" name="imprint_content"
class="form-control content-editor"
rows="15"
placeholder="Enter your imprint/legal information here...">{{ branding.imprint_content or '' }}</textarea>
<small class="form-text text-muted">
You can use HTML to format your content. Common tags: &lt;h2&gt;, &lt;h3&gt;, &lt;p&gt;, &lt;strong&gt;, &lt;br&gt;, &lt;a href=""&gt;
</small>
</div>
</div>
</div>
<!-- Save Button -->
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 Save Branding Settings</button>
@@ -289,6 +329,77 @@
}
/* Sync color inputs */
/* Toggle label styling - ensuring proper alignment */
.form-group .toggle-label {
display: inline-flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
margin-bottom: 0.5rem;
padding: 0;
}
.toggle-label input[type="checkbox"] {
display: none;
}
.toggle-slider {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
background: #ccc;
border-radius: 24px;
transition: background 0.3s;
flex-shrink: 0;
}
.toggle-slider::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: transform 0.3s;
}
.toggle-label input[type="checkbox"]:checked + .toggle-slider {
background: var(--primary-color);
}
.toggle-label input[type="checkbox"]:checked + .toggle-slider::before {
transform: translateX(26px);
}
.toggle-text {
font-weight: 500;
color: #495057;
line-height: 1;
}
/* Content editor styling */
.content-editor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
background: #f8f9fa;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 0.75rem;
resize: vertical;
}
.imprint-settings {
margin-top: 1.5rem;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
transition: all 0.3s ease;
}
</style>
<script>
@@ -306,6 +417,18 @@ document.addEventListener('DOMContentLoaded', function() {
colorPicker.value = this.value;
}
});
// Toggle imprint settings visibility
const imprintEnabled = document.getElementById('imprint_enabled');
const imprintSettings = document.getElementById('imprint-settings');
imprintEnabled.addEventListener('change', function() {
if (this.checked) {
imprintSettings.style.display = 'block';
} else {
imprintSettings.style.display = 'none';
}
});
});
</script>
{% endblock %}