Add system announcement feature.

This commit is contained in:
2025-07-04 08:05:11 +02:00
parent 667040d7f8
commit e31401a939
8 changed files with 1105 additions and 4 deletions

240
app.py
View File

@@ -1,5 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement
from data_formatting import (
format_duration, prepare_export_data, prepare_team_hours_export_data,
format_table_data, format_graph_data, format_team_data
@@ -167,13 +167,46 @@ def initialize_app():
@app.context_processor
def inject_globals():
"""Make certain variables available to all templates."""
# Get active announcements for current user
active_announcements = []
if g.user:
active_announcements = Announcement.get_active_announcements_for_user(g.user)
# Get tracking script settings
tracking_script_enabled = False
tracking_script_code = ''
try:
tracking_enabled_setting = SystemSettings.query.filter_by(key='tracking_script_enabled').first()
if tracking_enabled_setting:
tracking_script_enabled = tracking_enabled_setting.value == 'true'
tracking_code_setting = SystemSettings.query.filter_by(key='tracking_script_code').first()
if tracking_code_setting:
tracking_script_code = tracking_code_setting.value
except Exception:
pass # In case database isn't available yet
return {
'Role': Role,
'AccountType': AccountType,
'current_year': datetime.now().year
'current_year': datetime.now().year,
'active_announcements': active_announcements,
'tracking_script_enabled': tracking_script_enabled,
'tracking_script_code': tracking_script_code
}
# Template filters for date/time formatting
@app.template_filter('from_json')
def from_json_filter(json_str):
"""Parse JSON string to Python object."""
if not json_str:
return []
try:
import json
return json.loads(json_str)
except (json.JSONDecodeError, TypeError):
return []
@app.template_filter('format_date')
def format_date_filter(dt):
"""Format date according to user preferences."""
@@ -1937,6 +1970,8 @@ def system_admin_settings():
# Update system settings
registration_enabled = request.form.get('registration_enabled') == 'on'
email_verification = request.form.get('email_verification_required') == 'on'
tracking_script_enabled = request.form.get('tracking_script_enabled') == 'on'
tracking_script_code = request.form.get('tracking_script_code', '')
# Update or create settings
reg_setting = SystemSettings.query.filter_by(key='registration_enabled').first()
@@ -1961,6 +1996,28 @@ def system_admin_settings():
)
db.session.add(email_setting)
tracking_enabled_setting = SystemSettings.query.filter_by(key='tracking_script_enabled').first()
if tracking_enabled_setting:
tracking_enabled_setting.value = 'true' if tracking_script_enabled else 'false'
else:
tracking_enabled_setting = SystemSettings(
key='tracking_script_enabled',
value='true' if tracking_script_enabled else 'false',
description='Controls whether custom tracking script is enabled'
)
db.session.add(tracking_enabled_setting)
tracking_code_setting = SystemSettings.query.filter_by(key='tracking_script_code').first()
if tracking_code_setting:
tracking_code_setting.value = tracking_script_code
else:
tracking_code_setting = SystemSettings(
key='tracking_script_code',
value=tracking_script_code,
description='Custom tracking script code (HTML/JavaScript)'
)
db.session.add(tracking_code_setting)
db.session.commit()
flash('System settings updated successfully.', 'success')
return redirect(url_for('system_admin_settings'))
@@ -1973,6 +2030,10 @@ def system_admin_settings():
settings['registration_enabled'] = setting.value == 'true'
elif setting.key == 'email_verification_required':
settings['email_verification_required'] = setting.value == 'true'
elif setting.key == 'tracking_script_enabled':
settings['tracking_script_enabled'] = setting.value == 'true'
elif setting.key == 'tracking_script_code':
settings['tracking_script_code'] = setting.value
# System statistics
total_companies = Company.query.count()
@@ -1986,6 +2047,181 @@ def system_admin_settings():
total_users=total_users,
total_system_admins=total_system_admins)
@app.route('/system-admin/announcements')
@system_admin_required
def system_admin_announcements():
"""System Admin: Manage announcements"""
page = request.args.get('page', 1, type=int)
per_page = 20
announcements = Announcement.query.order_by(Announcement.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False)
return render_template('system_admin_announcements.html',
title='System Admin - Announcements',
announcements=announcements)
@app.route('/system-admin/announcements/new', methods=['GET', 'POST'])
@system_admin_required
def system_admin_announcement_new():
"""System Admin: Create new announcement"""
if request.method == 'POST':
title = request.form.get('title')
content = request.form.get('content')
announcement_type = request.form.get('announcement_type', 'info')
is_urgent = request.form.get('is_urgent') == 'on'
is_active = request.form.get('is_active') == 'on'
# Handle date fields
start_date = request.form.get('start_date')
end_date = request.form.get('end_date')
start_datetime = None
end_datetime = None
if start_date:
try:
start_datetime = datetime.strptime(start_date, '%Y-%m-%dT%H:%M')
except ValueError:
pass
if end_date:
try:
end_datetime = datetime.strptime(end_date, '%Y-%m-%dT%H:%M')
except ValueError:
pass
# Handle targeting
target_all_users = request.form.get('target_all_users') == 'on'
target_roles = None
target_companies = None
if not target_all_users:
selected_roles = request.form.getlist('target_roles')
selected_companies = request.form.getlist('target_companies')
if selected_roles:
import json
target_roles = json.dumps(selected_roles)
if selected_companies:
import json
target_companies = json.dumps([int(c) for c in selected_companies])
announcement = Announcement(
title=title,
content=content,
announcement_type=announcement_type,
is_urgent=is_urgent,
is_active=is_active,
start_date=start_datetime,
end_date=end_datetime,
target_all_users=target_all_users,
target_roles=target_roles,
target_companies=target_companies,
created_by_id=g.user.id
)
db.session.add(announcement)
db.session.commit()
flash('Announcement created successfully.', 'success')
return redirect(url_for('system_admin_announcements'))
# Get roles and companies for targeting options
roles = [role.value for role in Role]
companies = Company.query.order_by(Company.name).all()
return render_template('system_admin_announcement_form.html',
title='Create Announcement',
announcement=None,
roles=roles,
companies=companies)
@app.route('/system-admin/announcements/<int:id>/edit', methods=['GET', 'POST'])
@system_admin_required
def system_admin_announcement_edit(id):
"""System Admin: Edit announcement"""
announcement = Announcement.query.get_or_404(id)
if request.method == 'POST':
announcement.title = request.form.get('title')
announcement.content = request.form.get('content')
announcement.announcement_type = request.form.get('announcement_type', 'info')
announcement.is_urgent = request.form.get('is_urgent') == 'on'
announcement.is_active = request.form.get('is_active') == 'on'
# Handle date fields
start_date = request.form.get('start_date')
end_date = request.form.get('end_date')
if start_date:
try:
announcement.start_date = datetime.strptime(start_date, '%Y-%m-%dT%H:%M')
except ValueError:
announcement.start_date = None
else:
announcement.start_date = None
if end_date:
try:
announcement.end_date = datetime.strptime(end_date, '%Y-%m-%dT%H:%M')
except ValueError:
announcement.end_date = None
else:
announcement.end_date = None
# Handle targeting
announcement.target_all_users = request.form.get('target_all_users') == 'on'
if not announcement.target_all_users:
selected_roles = request.form.getlist('target_roles')
selected_companies = request.form.getlist('target_companies')
if selected_roles:
import json
announcement.target_roles = json.dumps(selected_roles)
else:
announcement.target_roles = None
if selected_companies:
import json
announcement.target_companies = json.dumps([int(c) for c in selected_companies])
else:
announcement.target_companies = None
else:
announcement.target_roles = None
announcement.target_companies = None
announcement.updated_at = datetime.now()
db.session.commit()
flash('Announcement updated successfully.', 'success')
return redirect(url_for('system_admin_announcements'))
# Get roles and companies for targeting options
roles = [role.value for role in Role]
companies = Company.query.order_by(Company.name).all()
return render_template('system_admin_announcement_form.html',
title='Edit Announcement',
announcement=announcement,
roles=roles,
companies=companies)
@app.route('/system-admin/announcements/<int:id>/delete', methods=['POST'])
@system_admin_required
def system_admin_announcement_delete(id):
"""System Admin: Delete announcement"""
announcement = Announcement.query.get_or_404(id)
db.session.delete(announcement)
db.session.commit()
flash('Announcement deleted successfully.', 'success')
return redirect(url_for('system_admin_announcements'))
@app.route('/admin/work-policies', methods=['GET', 'POST'])
@admin_required
@company_required

View File

@@ -15,7 +15,7 @@ try:
from app import app, db
from models import (User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project,
Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType,
ProjectCategory, Task, SubTask, TaskStatus, TaskPriority)
ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement)
from werkzeug.security import generate_password_hash
FLASK_AVAILABLE = True
except ImportError:
@@ -272,6 +272,30 @@ def create_missing_tables(cursor):
)
""")
# Announcement table
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcement'")
if not cursor.fetchone():
print("Creating announcement table...")
cursor.execute("""
CREATE TABLE announcement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
is_active BOOLEAN DEFAULT 1,
is_urgent BOOLEAN DEFAULT 0,
announcement_type VARCHAR(20) DEFAULT 'info',
start_date TIMESTAMP,
end_date TIMESTAMP,
target_all_users BOOLEAN DEFAULT 1,
target_roles TEXT,
target_companies TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by_id INTEGER NOT NULL,
FOREIGN KEY (created_by_id) REFERENCES user (id)
)
""")
def migrate_to_company_model(db_path):
"""Migrate to company-based multi-tenancy model."""
@@ -717,6 +741,32 @@ def init_system_settings():
db.session.commit()
print("Email verification setting initialized to enabled")
# Check if tracking_script_enabled setting exists
tracking_script_setting = SystemSettings.query.filter_by(key='tracking_script_enabled').first()
if not tracking_script_setting:
print("Adding tracking_script_enabled system setting...")
tracking_script_setting = SystemSettings(
key='tracking_script_enabled',
value='false',
description='Controls whether custom tracking script is enabled'
)
db.session.add(tracking_script_setting)
db.session.commit()
print("Tracking script setting initialized to disabled")
# Check if tracking_script_code setting exists
tracking_script_code_setting = SystemSettings.query.filter_by(key='tracking_script_code').first()
if not tracking_script_code_setting:
print("Adding tracking_script_code system setting...")
tracking_script_code_setting = SystemSettings(
key='tracking_script_code',
value='',
description='Custom tracking script code (HTML/JavaScript)'
)
db.session.add(tracking_script_code_setting)
db.session.commit()
print("Tracking script code setting initialized")
def create_new_database(db_path):
"""Create a new database with all tables."""

View File

@@ -528,3 +528,88 @@ class SubTask(db.Model):
def can_user_access(self, user):
"""Check if a user can access this subtask"""
return self.parent_task.can_user_access(user)
# Announcement model for system-wide announcements
class Announcement(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text, nullable=False)
# Announcement properties
is_active = db.Column(db.Boolean, default=True)
is_urgent = db.Column(db.Boolean, default=False) # For urgent announcements with different styling
announcement_type = db.Column(db.String(20), default='info') # info, warning, success, danger
# Scheduling
start_date = db.Column(db.DateTime, nullable=True) # When to start showing
end_date = db.Column(db.DateTime, nullable=True) # When to stop showing
# Targeting
target_all_users = db.Column(db.Boolean, default=True)
target_roles = db.Column(db.Text, nullable=True) # JSON string of roles if not all users
target_companies = db.Column(db.Text, nullable=True) # JSON string of company IDs if not all companies
# Metadata
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Relationships
created_by = db.relationship('User', foreign_keys=[created_by_id])
def __repr__(self):
return f'<Announcement {self.title}>'
def is_visible_now(self):
"""Check if announcement should be visible at current time"""
if not self.is_active:
return False
now = datetime.now()
# Check start date
if self.start_date and now < self.start_date:
return False
# Check end date
if self.end_date and now > self.end_date:
return False
return True
def is_visible_to_user(self, user):
"""Check if announcement should be visible to specific user"""
if not self.is_visible_now():
return False
# If targeting all users, show to everyone
if self.target_all_users:
return True
# Check role targeting
if self.target_roles:
import json
try:
target_roles = json.loads(self.target_roles)
if user.role.value not in target_roles:
return False
except (json.JSONDecodeError, AttributeError):
pass
# Check company targeting
if self.target_companies:
import json
try:
target_companies = json.loads(self.target_companies)
if user.company_id not in target_companies:
return False
except (json.JSONDecodeError, AttributeError):
pass
return True
@staticmethod
def get_active_announcements_for_user(user):
"""Get all active announcements visible to a specific user"""
announcements = Announcement.query.filter_by(is_active=True).all()
return [ann for ann in announcements if ann.is_visible_to_user(user)]

View File

@@ -1501,3 +1501,111 @@ input[type="time"]::-webkit-datetime-edit {
gap: 0.5rem;
}
}
/* Announcement Styles */
.announcements {
margin-bottom: 1.5rem;
}
.announcements .alert {
margin-bottom: 1rem;
padding: 1rem;
border-radius: 8px;
border: 1px solid transparent;
position: relative;
}
.announcements .alert-info {
background-color: #d1ecf1;
border-color: #bee5eb;
color: #0c5460;
}
.announcements .alert-warning {
background-color: #fff3cd;
border-color: #ffeaa7;
color: #856404;
}
.announcements .alert-success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
}
.announcements .alert-danger {
background-color: #f8d7da;
border-color: #f5c6cb;
color: #721c24;
}
.announcements .alert-urgent {
border-width: 2px;
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.15);
animation: pulse-urgent 2s infinite;
}
@keyframes pulse-urgent {
0% { box-shadow: 0 4px 12px rgba(220, 53, 69, 0.15); }
50% { box-shadow: 0 6px 16px rgba(220, 53, 69, 0.25); }
100% { box-shadow: 0 4px 12px rgba(220, 53, 69, 0.15); }
}
.announcement-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.announcement-header strong {
font-size: 1.1rem;
}
.urgent-badge {
background: #dc3545;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
animation: blink 1.5s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.6; }
}
.announcement-content {
margin-bottom: 0.5rem;
line-height: 1.5;
}
.announcement-content p {
margin-bottom: 0.5rem;
}
.announcement-content p:last-child {
margin-bottom: 0;
}
.announcement-date {
color: #6c757d;
font-size: 0.875rem;
font-style: italic;
}
/* Mobile responsiveness for announcements */
@media (max-width: 768px) {
.announcement-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.urgent-badge {
align-self: flex-end;
}
}

View File

@@ -58,6 +58,7 @@
{% if g.user.role == Role.SYSTEM_ADMIN %}
<li class="nav-divider">System Admin</li>
<li><a href="{{ url_for('system_admin_dashboard') }}" data-tooltip="System Dashboard"><i class="nav-icon">🌐</i><span class="nav-text">System Dashboard</span></a></li>
<li><a href="{{ url_for('system_admin_announcements') }}" data-tooltip="Announcements"><i class="nav-icon">📢</i><span class="nav-text">Announcements</span></a></li>
{% endif %}
{% elif g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR] %}
<li class="nav-divider">{{ g.user.username }}</li>
@@ -88,6 +89,30 @@
<div class="mobile-overlay" id="mobile-overlay"></div>
<main class="main-content">
<!-- System Announcements -->
{% if active_announcements %}
<div class="announcements">
{% for announcement in active_announcements %}
<div class="alert alert-{{ announcement.announcement_type }}{% if announcement.is_urgent %} alert-urgent{% endif %}">
<div class="announcement-header">
<strong>{{ announcement.title }}</strong>
{% if announcement.is_urgent %}
<span class="urgent-badge">URGENT</span>
{% endif %}
</div>
<div class="announcement-content">
{{ announcement.content|safe }}
</div>
{% if announcement.created_at %}
<small class="announcement-date">
Posted: {{ announcement.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
@@ -107,5 +132,10 @@
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
<script src="{{ url_for('static', filename='js/sidebar.js') }}"></script>
<!-- Custom Tracking Script -->
{% if tracking_script_enabled and tracking_script_code %}
{{ tracking_script_code|safe }}
{% endif %}
</body>
</html>

View File

@@ -0,0 +1,369 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="header-section">
<h1>{{ "Edit" if announcement else "Create" }} Announcement</h1>
<p class="subtitle">{{ "Update" if announcement else "Create new" }} system announcement for users</p>
<a href="{{ url_for('system_admin_announcements') }}" class="btn btn-secondary">
← Back to Announcements
</a>
</div>
<div class="form-section">
<form method="POST" class="announcement-form">
<div class="form-group">
<label for="title">Title</label>
<input type="text"
id="title"
name="title"
value="{{ announcement.title if announcement else '' }}"
required
maxlength="200"
class="form-control">
</div>
<div class="form-group">
<label for="content">Content</label>
<textarea id="content"
name="content"
required
rows="6"
class="form-control">{{ announcement.content if announcement else '' }}</textarea>
<small class="form-text">You can use HTML formatting in the content.</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="announcement_type">Type</label>
<select id="announcement_type" name="announcement_type" class="form-control">
<option value="info" {{ 'selected' if announcement and announcement.announcement_type == 'info' else '' }}>Info</option>
<option value="warning" {{ 'selected' if announcement and announcement.announcement_type == 'warning' else '' }}>Warning</option>
<option value="success" {{ 'selected' if announcement and announcement.announcement_type == 'success' else '' }}>Success</option>
<option value="danger" {{ 'selected' if announcement and announcement.announcement_type == 'danger' else '' }}>Danger</option>
</select>
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox"
name="is_urgent"
{{ 'checked' if announcement and announcement.is_urgent else '' }}>
<span class="checkmark"></span>
Mark as Urgent
</label>
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox"
name="is_active"
{{ 'checked' if not announcement or announcement.is_active else '' }}>
<span class="checkmark"></span>
Active
</label>
</div>
</div>
<div class="form-section">
<h3>Scheduling</h3>
<div class="form-row">
<div class="form-group">
<label for="start_date">Start Date/Time (Optional)</label>
<input type="datetime-local"
id="start_date"
name="start_date"
value="{{ announcement.start_date.strftime('%Y-%m-%dT%H:%M') if announcement and announcement.start_date else '' }}"
class="form-control">
<small class="form-text">Leave empty to show immediately</small>
</div>
<div class="form-group">
<label for="end_date">End Date/Time (Optional)</label>
<input type="datetime-local"
id="end_date"
name="end_date"
value="{{ announcement.end_date.strftime('%Y-%m-%dT%H:%M') if announcement and announcement.end_date else '' }}"
class="form-control">
<small class="form-text">Leave empty for no expiry</small>
</div>
</div>
</div>
<div class="form-section">
<h3>Targeting</h3>
<div class="form-row">
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox"
name="target_all_users"
id="target_all_users"
{{ 'checked' if not announcement or announcement.target_all_users else '' }}
onchange="toggleTargeting()">
<span class="checkmark"></span>
Target All Users
</label>
</div>
</div>
<div id="targeting_options" style="display: {{ 'none' if not announcement or announcement.target_all_users else 'block' }};">
<div class="form-row">
<div class="form-group">
<label>Target Roles</label>
<div class="checkbox-list">
{% set selected_roles = [] %}
{% if announcement and announcement.target_roles %}
{% set selected_roles = announcement.target_roles|from_json %}
{% endif %}
{% for role in roles %}
<label class="checkbox-container">
<input type="checkbox"
name="target_roles"
value="{{ role }}"
{{ 'checked' if role in selected_roles else '' }}>
<span class="checkmark"></span>
{{ role }}
</label>
{% endfor %}
</div>
</div>
<div class="form-group">
<label>Target Companies</label>
<div class="checkbox-list">
{% set selected_companies = [] %}
{% if announcement and announcement.target_companies %}
{% set selected_companies = announcement.target_companies|from_json %}
{% endif %}
{% for company in companies %}
<label class="checkbox-container">
<input type="checkbox"
name="target_companies"
value="{{ company.id }}"
{{ 'checked' if company.id in selected_companies else '' }}>
<span class="checkmark"></span>
{{ company.name }}
</label>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
{{ "Update" if announcement else "Create" }} Announcement
</button>
<a href="{{ url_for('system_admin_announcements') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
<script>
function toggleTargeting() {
const targetAllUsers = document.getElementById('target_all_users');
const targetingOptions = document.getElementById('targeting_options');
if (targetAllUsers.checked) {
targetingOptions.style.display = 'none';
} else {
targetingOptions.style.display = 'block';
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
toggleTargeting();
});
</script>
<style>
.header-section {
margin-bottom: 2rem;
}
.subtitle {
color: #6c757d;
margin-bottom: 1rem;
}
.form-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
}
.announcement-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-section h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #495057;
font-size: 1.2rem;
border-bottom: 1px solid #e9ecef;
padding-bottom: 0.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #495057;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
.form-control:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
.form-text {
display: block;
margin-top: 0.25rem;
color: #6c757d;
font-size: 0.875rem;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.checkbox-container {
display: block;
position: relative;
padding-left: 35px;
margin-bottom: 12px;
cursor: pointer;
font-size: 16px;
user-select: none;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 25px;
width: 25px;
background-color: #eee;
border-radius: 4px;
}
.checkbox-container:hover input ~ .checkmark {
background-color: #ccc;
}
.checkbox-container input:checked ~ .checkmark {
background-color: #2196F3;
}
.checkmark:after {
content: "";
position: absolute;
display: none;
}
.checkbox-container input:checked ~ .checkmark:after {
display: block;
}
.checkbox-container .checkmark:after {
left: 9px;
top: 5px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
.checkbox-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.5rem;
max-height: 200px;
overflow-y: auto;
border: 1px solid #ced4da;
border-radius: 0.25rem;
padding: 0.75rem;
background: #f8f9fa;
}
.form-actions {
display: flex;
gap: 1rem;
padding-top: 1rem;
border-top: 1px solid #e9ecef;
}
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
text-decoration: none;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
text-align: center;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
}
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.checkbox-list {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,165 @@
{% extends "layout.html" %}
{% block content %}
<div class="content-header">
<div class="header-row">
<h1>System Announcements</h1>
<a href="{{ url_for('system_admin_announcement_new') }}" class="btn btn-primary">
<i class="icon"></i> New Announcement
</a>
</div>
</div>
<div class="content-body">
{% if announcements.items %}
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Title</th>
<th>Type</th>
<th>Status</th>
<th>Start Date</th>
<th>End Date</th>
<th>Target</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for announcement in announcements.items %}
<tr class="{% if not announcement.is_active %}inactive{% endif %}">
<td>
<strong>{{ announcement.title }}</strong>
{% if announcement.is_urgent %}
<span class="badge badge-danger">URGENT</span>
{% endif %}
</td>
<td>
<span class="badge badge-{{ announcement.announcement_type }}">
{{ announcement.announcement_type.title() }}
</span>
</td>
<td>
{% if announcement.is_active %}
{% if announcement.is_visible_now() %}
<span class="badge badge-success">Active</span>
{% else %}
<span class="badge badge-warning">Scheduled</span>
{% endif %}
{% else %}
<span class="badge badge-secondary">Inactive</span>
{% endif %}
</td>
<td>
{% if announcement.start_date %}
{{ announcement.start_date.strftime('%Y-%m-%d %H:%M') }}
{% else %}
<em>Immediate</em>
{% endif %}
</td>
<td>
{% if announcement.end_date %}
{{ announcement.end_date.strftime('%Y-%m-%d %H:%M') }}
{% else %}
<em>No expiry</em>
{% endif %}
</td>
<td>
{% if announcement.target_all_users %}
All Users
{% else %}
<span class="text-muted">Targeted</span>
{% endif %}
</td>
<td>{{ announcement.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="action-buttons">
<a href="{{ url_for('system_admin_announcement_edit', id=announcement.id) }}"
class="btn btn-sm btn-outline-primary" title="Edit">
✏️
</a>
<form method="POST" action="{{ url_for('system_admin_announcement_delete', id=announcement.id) }}"
style="display: inline-block;"
onsubmit="return confirm('Are you sure you want to delete this announcement?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
🗑️
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if announcements.pages > 1 %}
<div class="pagination-container">
<div class="pagination">
{% if announcements.has_prev %}
<a href="{{ url_for('system_admin_announcements', page=announcements.prev_num) }}" class="page-link">« Previous</a>
{% endif %}
{% for page_num in announcements.iter_pages() %}
{% if page_num %}
{% if page_num != announcements.page %}
<a href="{{ url_for('system_admin_announcements', page=page_num) }}" class="page-link">{{ page_num }}</a>
{% else %}
<span class="page-link current">{{ page_num }}</span>
{% endif %}
{% else %}
<span class="page-link"></span>
{% endif %}
{% endfor %}
{% if announcements.has_next %}
<a href="{{ url_for('system_admin_announcements', page=announcements.next_num) }}" class="page-link">Next »</a>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<div class="empty-state">
<h3>No announcements found</h3>
<p>Create your first announcement to communicate with users.</p>
<a href="{{ url_for('system_admin_announcement_new') }}" class="btn btn-primary">
Create Announcement
</a>
</div>
{% endif %}
</div>
<style>
.inactive {
opacity: 0.6;
}
.badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75em;
font-weight: bold;
text-transform: uppercase;
}
.badge-info { background: #17a2b8; color: white; }
.badge-warning { background: #ffc107; color: #212529; }
.badge-success { background: #28a745; color: white; }
.badge-danger { background: #dc3545; color: white; }
.badge-secondary { background: #6c757d; color: white; }
.action-buttons {
display: flex;
gap: 5px;
}
.empty-state {
text-align: center;
padding: 3rem;
color: #6c757d;
}
</style>
{% endblock %}

View File

@@ -69,6 +69,43 @@
</div>
</div>
<div class="setting-group">
<div class="setting-header">
<h4>Tracking Script</h4>
<p>Enable custom tracking script (e.g., Google Analytics)</p>
</div>
<div class="setting-control">
<label class="toggle-label">
<input type="checkbox" name="tracking_script_enabled"
{% if settings.get('tracking_script_enabled', False) %}checked{% endif %}>
<span class="toggle-slider"></span>
<span class="toggle-text">Enable tracking script</span>
</label>
<small class="setting-description">
When enabled, the custom tracking script will be included on all pages.
Use this for analytics tracking, monitoring, or other custom JavaScript.
</small>
</div>
</div>
<div class="setting-group full-width">
<div class="setting-header">
<h4>Tracking Script Code</h4>
<p>Enter your tracking script code (HTML/JavaScript)</p>
</div>
<div class="setting-control">
<textarea name="tracking_script_code"
class="form-control code-textarea"
rows="8"
placeholder="<!-- Paste your tracking script here (e.g., Google Analytics, custom JavaScript) -->"
>{{ settings.get('tracking_script_code', '') }}</textarea>
<small class="setting-description">
This code will be inserted at the bottom of every page before the closing &lt;/body&gt; tag.
Common use cases: Google Analytics, Facebook Pixel, custom JavaScript, monitoring scripts.
</small>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Settings</button>
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-secondary">Cancel</a>
@@ -235,6 +272,27 @@
border-radius: 8px;
}
.setting-group.full-width {
grid-template-columns: 1fr;
}
.code-textarea {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.4;
background: #f8f9fa;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 0.75rem;
resize: vertical;
min-height: 120px;
}
.form-control {
width: 100%;
box-sizing: border-box;
}
.setting-header h4 {
margin: 0 0 0.5rem 0;
color: #495057;