Streamline System Admin styles.

This commit is contained in:
2025-07-09 07:50:59 +02:00
parent f476858df1
commit 17190d08d6
11 changed files with 5169 additions and 2118 deletions

View File

@@ -1,17 +1,37 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="container"> <div class="announcement-form-container">
<div class="header-section"> <!-- Header Section -->
<h1>{{ "Edit" if announcement else "Create" }} Announcement</h1> <div class="page-header">
<p class="subtitle">{{ "Update" if announcement else "Create new" }} system announcement for users</p> <div class="header-content">
<div class="header-left">
<h1 class="page-title">
<span class="page-icon"><i class="ti ti-speakerphone"></i></span>
{{ "Edit" if announcement else "Create" }} Announcement
</h1>
<p class="page-subtitle">{{ "Update" if announcement else "Create new" }} system announcement for users</p>
</div>
<div class="header-actions">
<a href="{{ url_for('announcements.index') }}" class="btn btn-secondary"> <a href="{{ url_for('announcements.index') }}" class="btn btn-secondary">
<i class="ti ti-arrow-left"></i> Back to Announcements <i class="ti ti-arrow-left"></i>
Back to Announcements
</a> </a>
</div> </div>
</div>
</div>
<div class="form-section"> <!-- Main Form -->
<form method="POST" class="announcement-form"> <form method="POST" class="announcement-form">
<!-- Basic Information -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-forms"></i></span>
Basic Information
</h2>
</div>
<div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="title">Title</label> <label for="title">Title</label>
<input type="text" <input type="text"
@@ -20,7 +40,8 @@
value="{{ announcement.title if announcement else '' }}" value="{{ announcement.title if announcement else '' }}"
required required
maxlength="200" maxlength="200"
class="form-control"> class="form-control"
placeholder="Enter announcement title">
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -29,7 +50,8 @@
name="content" name="content"
required required
rows="6" rows="6"
class="form-control">{{ announcement.content if announcement else '' }}</textarea> class="form-control"
placeholder="Enter announcement content">{{ announcement.content if announcement else '' }}</textarea>
<small class="form-text">You can use HTML formatting in the content.</small> <small class="form-text">You can use HTML formatting in the content.</small>
</div> </div>
@@ -37,39 +59,57 @@
<div class="form-group"> <div class="form-group">
<label for="announcement_type">Type</label> <label for="announcement_type">Type</label>
<select id="announcement_type" name="announcement_type" class="form-control"> <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="info" {{ 'selected' if announcement and announcement.announcement_type == 'info' else '' }}>
<option value="warning" {{ 'selected' if announcement and announcement.announcement_type == 'warning' else '' }}>Warning</option> <i class="ti ti-info-circle"></i> Info
<option value="success" {{ 'selected' if announcement and announcement.announcement_type == 'success' else '' }}>Success</option> </option>
<option value="danger" {{ 'selected' if announcement and announcement.announcement_type == 'danger' else '' }}>Danger</option> <option value="warning" {{ 'selected' if announcement and announcement.announcement_type == 'warning' else '' }}>
<i class="ti ti-alert-triangle"></i> Warning
</option>
<option value="success" {{ 'selected' if announcement and announcement.announcement_type == 'success' else '' }}>
<i class="ti ti-circle-check"></i> Success
</option>
<option value="danger" {{ 'selected' if announcement and announcement.announcement_type == 'danger' else '' }}>
<i class="ti ti-alert-circle"></i> Danger
</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="checkbox-container"> <label>Options</label>
<div class="checkbox-group">
<label class="toggle-label">
<input type="checkbox" <input type="checkbox"
name="is_urgent" name="is_urgent"
{{ 'checked' if announcement and announcement.is_urgent else '' }}> {{ 'checked' if announcement and announcement.is_urgent else '' }}>
<span class="checkmark"></span> <span class="toggle-slider"></span>
Mark as Urgent <span class="toggle-text">Mark as Urgent</span>
</label> </label>
</div>
<div class="form-group"> <label class="toggle-label">
<label class="checkbox-container">
<input type="checkbox" <input type="checkbox"
name="is_active" name="is_active"
{{ 'checked' if not announcement or announcement.is_active else '' }}> {{ 'checked' if not announcement or announcement.is_active else '' }}>
<span class="checkmark"></span> <span class="toggle-slider"></span>
Active <span class="toggle-text">Active</span>
</label> </label>
</div> </div>
</div> </div>
</div>
</div>
</div>
<div class="form-section"> <!-- Scheduling -->
<h3>Scheduling</h3> <div class="card">
<div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-calendar-event"></i></span>
Scheduling
</h2>
</div>
<div class="card-body">
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label for="start_date">Start Date/Time (Optional)</label> <label for="start_date">Start Date/Time</label>
<input type="datetime-local" <input type="datetime-local"
id="start_date" id="start_date"
name="start_date" name="start_date"
@@ -79,7 +119,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="end_date">End Date/Time (Optional)</label> <label for="end_date">End Date/Time</label>
<input type="datetime-local" <input type="datetime-local"
id="end_date" id="end_date"
name="end_date" name="end_date"
@@ -89,60 +129,67 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="form-section"> <!-- Targeting -->
<h3>Targeting</h3> <div class="card">
<div class="form-row"> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-target"></i></span>
Targeting
</h2>
</div>
<div class="card-body">
<div class="form-group"> <div class="form-group">
<label class="checkbox-container"> <label class="toggle-label main-toggle">
<input type="checkbox" <input type="checkbox"
name="target_all_users" name="target_all_users"
id="target_all_users" id="target_all_users"
{{ 'checked' if not announcement or announcement.target_all_users else '' }} {{ 'checked' if not announcement or announcement.target_all_users else '' }}
onchange="toggleTargeting()"> onchange="toggleTargeting()">
<span class="checkmark"></span> <span class="toggle-slider"></span>
Target All Users <span class="toggle-text">Target All Users</span>
</label> </label>
</div> <small class="form-text">When enabled, announcement will be shown to all users regardless of role or company</small>
</div> </div>
<div id="targeting_options" style="display: {{ 'none' if not announcement or announcement.target_all_users else 'block' }};"> <div id="targeting_options" style="display: {{ 'none' if not announcement or announcement.target_all_users else 'block' }};">
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>Target Roles</label> <label><i class="ti ti-user-check"></i> Target Roles</label>
<div class="checkbox-list"> <div class="checkbox-list">
{% set selected_roles = [] %} {% set selected_roles = [] %}
{% if announcement and announcement.target_roles %} {% if announcement and announcement.target_roles %}
{% set selected_roles = announcement.target_roles|from_json %} {% set selected_roles = announcement.target_roles|from_json %}
{% endif %} {% endif %}
{% for role in roles %} {% for role in roles %}
<label class="checkbox-container"> <label class="checkbox-item">
<input type="checkbox" <input type="checkbox"
name="target_roles" name="target_roles"
value="{{ role }}" value="{{ role }}"
{{ 'checked' if role in selected_roles else '' }}> {{ 'checked' if role in selected_roles else '' }}>
<span class="checkmark"></span> <span class="checkbox-custom"></span>
{{ role }} <span class="checkbox-label">{{ role }}</span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Target Companies</label> <label><i class="ti ti-building"></i> Target Companies</label>
<div class="checkbox-list"> <div class="checkbox-list">
{% set selected_companies = [] %} {% set selected_companies = [] %}
{% if announcement and announcement.target_companies %} {% if announcement and announcement.target_companies %}
{% set selected_companies = announcement.target_companies|from_json %} {% set selected_companies = announcement.target_companies|from_json %}
{% endif %} {% endif %}
{% for company in companies %} {% for company in companies %}
<label class="checkbox-container"> <label class="checkbox-item">
<input type="checkbox" <input type="checkbox"
name="target_companies" name="target_companies"
value="{{ company.id }}" value="{{ company.id }}"
{{ 'checked' if company.id in selected_companies else '' }}> {{ 'checked' if company.id in selected_companies else '' }}>
<span class="checkmark"></span> <span class="checkbox-custom"></span>
{{ company.name }} <span class="checkbox-label">{{ company.name }}</span>
</label> </label>
{% endfor %} {% endfor %}
</div> </div>
@@ -150,16 +197,18 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Form Actions -->
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="ti ti-device-floppy"></i>
{{ "Update" if announcement else "Create" }} Announcement {{ "Update" if announcement else "Create" }} Announcement
</button> </button>
<a href="{{ url_for('announcements.index') }}" class="btn btn-secondary">Cancel</a> <a href="{{ url_for('announcements.index') }}" class="btn btn-secondary">Cancel</a>
</div> </div>
</form> </form>
</div> </div>
</div>
<script> <script>
function toggleTargeting() { function toggleTargeting() {
@@ -180,155 +229,334 @@ document.addEventListener('DOMContentLoaded', function() {
</script> </script>
<style> <style>
.header-section { /* Container */
margin-bottom: 2rem; .announcement-form-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
} }
.subtitle { /* Page Header */
color: #6c757d; .page-header {
margin-bottom: 1rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
} border-radius: 16px;
.form-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 2rem; padding: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
} }
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-icon {
font-size: 2.5rem;
display: inline-block;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
/* Cards */
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
margin-bottom: 1.5rem;
overflow: hidden;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card-header {
background: #f8f9fa;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
color: #1f2937;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title .icon {
font-size: 1.5rem;
}
.card-body {
padding: 1.5rem;
}
/* Form */
.announcement-form { .announcement-form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 0;
}
.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 { .form-group {
margin-bottom: 1rem; margin-bottom: 1.5rem;
} }
.form-group label { .form-group label {
display: block; display: block;
font-weight: 600;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-weight: 500; color: #374151;
color: #495057;
} }
.form-control { .form-control {
width: 100%; width: 100%;
padding: 0.5rem; padding: 0.625rem 1rem;
border: 1px solid #ced4da; border: 1px solid #e5e7eb;
border-radius: 4px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
box-sizing: border-box; transition: all 0.2s ease;
} }
.form-control:focus { .form-control:focus {
outline: none; outline: none;
border-color: #007bff; border-color: #667eea;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
textarea.form-control {
resize: vertical;
min-height: 120px;
} }
.form-text { .form-text {
display: block; display: block;
margin-top: 0.25rem; margin-top: 0.25rem;
color: #6c757d;
font-size: 0.875rem; font-size: 0.875rem;
color: #6b7280;
} }
.form-row { .form-row {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
/* Toggle Switches */
.checkbox-group {
display: flex;
flex-direction: column;
gap: 1rem; gap: 1rem;
} }
.checkbox-container { .toggle-label {
display: block; display: inline-flex;
position: relative; align-items: center;
padding-left: 35px; gap: 0.75rem;
margin-bottom: 12px;
cursor: pointer; cursor: pointer;
font-size: 16px; margin-bottom: 0;
user-select: none; padding: 0;
} }
.checkbox-container input { .toggle-label input[type="checkbox"] {
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; display: none;
} }
.checkbox-container input:checked ~ .checkmark:after { .toggle-slider {
display: block; position: relative;
display: inline-block;
width: 50px;
height: 24px;
background: #e5e7eb;
border-radius: 24px;
transition: background 0.3s;
flex-shrink: 0;
} }
.checkbox-container .checkmark:after { .toggle-slider::before {
left: 9px; content: '';
top: 5px; position: absolute;
width: 5px; width: 20px;
height: 10px; height: 20px;
border: solid white; border-radius: 50%;
border-width: 0 3px 3px 0; background: white;
transform: rotate(45deg); top: 2px;
left: 2px;
transition: transform 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
} }
.toggle-label input[type="checkbox"]:checked + .toggle-slider {
background: #667eea;
}
.toggle-label input[type="checkbox"]:checked + .toggle-slider::before {
transform: translateX(26px);
}
.toggle-text {
font-weight: 500;
color: #1f2937;
}
.main-toggle {
margin-bottom: 1rem;
}
/* Checkbox Lists */
.checkbox-list { .checkbox-list {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.5rem; gap: 0.75rem;
max-height: 200px; max-height: 250px;
overflow-y: auto; overflow-y: auto;
border: 1px solid #ced4da; border: 1px solid #e5e7eb;
border-radius: 0.25rem; border-radius: 8px;
padding: 0.75rem; padding: 1rem;
background: #f8f9fa; background: #f8f9fa;
} }
.checkbox-item {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.25rem 0;
}
.checkbox-item input[type="checkbox"] {
display: none;
}
.checkbox-custom {
width: 20px;
height: 20px;
border: 2px solid #e5e7eb;
border-radius: 4px;
background: white;
transition: all 0.2s ease;
position: relative;
flex-shrink: 0;
}
.checkbox-item:hover .checkbox-custom {
border-color: #667eea;
}
.checkbox-item input[type="checkbox"]:checked + .checkbox-custom {
background: #667eea;
border-color: #667eea;
}
.checkbox-item input[type="checkbox"]:checked + .checkbox-custom::after {
content: '';
position: absolute;
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox-label {
color: #374151;
font-size: 0.95rem;
}
/* Form Actions */
.form-actions { .form-actions {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
padding-top: 1rem; padding: 2rem;
border-top: 1px solid #e9ecef; background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
margin-top: 2rem;
} }
/* Button styles now centralized in main style.css */ /* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
/* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.announcement-form-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.form-row { .form-row {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -336,6 +564,36 @@ document.addEventListener('DOMContentLoaded', function() {
.checkbox-list { .checkbox-list {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.form-actions {
flex-direction: column;
} }
.btn {
width: 100%;
justify-content: center;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -1,19 +1,35 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="content-header"> <div class="announcements-container">
<div class="header-row"> <!-- Header Section -->
<h1>System Announcements</h1> <div class="page-header">
<a href="{{ url_for('announcements.create') }}" class="btn btn-md btn-primary"> <div class="header-content">
<i class="icon"></i> New Announcement <div class="header-left">
<h1 class="page-title">
<span class="page-icon"><i class="ti ti-speakerphone"></i></span>
System Announcements
</h1>
<p class="page-subtitle">Manage system-wide announcements and notifications</p>
</div>
<div class="header-actions">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">
<i class="ti ti-arrow-left"></i>
Back to Dashboard
</a>
<a href="{{ url_for('announcements.create') }}" class="btn btn-primary">
<i class="ti ti-plus"></i>
New Announcement
</a> </a>
</div> </div>
</div> </div>
</div>
<div class="content-body"> <!-- Announcements Table -->
{% if announcements.items %} {% if announcements.items %}
<div class="table-container"> <div class="card">
<table class="data-table"> <div class="card-body no-padding">
<table class="table">
<thead> <thead>
<tr> <tr>
<th>Title</th> <th>Title</th>
@@ -28,62 +44,72 @@
</thead> </thead>
<tbody> <tbody>
{% for announcement in announcements.items %} {% for announcement in announcements.items %}
<tr class="{% if not announcement.is_active %}inactive{% endif %}"> <tr class="{% if not announcement.is_active %}inactive-row{% endif %}">
<td> <td>
<div class="announcement-title">
<strong>{{ announcement.title }}</strong> <strong>{{ announcement.title }}</strong>
{% if announcement.is_urgent %} {% if announcement.is_urgent %}
<span class="badge badge-danger">URGENT</span> <span class="badge badge-urgent">URGENT</span>
{% endif %} {% endif %}
</div>
</td> </td>
<td> <td>
<span class="badge badge-{{ announcement.announcement_type }}"> <span class="type-badge type-{{ announcement.announcement_type }}">
{{ announcement.announcement_type.title() }} {{ announcement.announcement_type.title() }}
</span> </span>
</td> </td>
<td> <td>
{% if announcement.is_active %} {% if announcement.is_active %}
{% if announcement.is_visible_now() %} {% if announcement.is_visible_now() %}
<span class="badge badge-success">Active</span> <span class="status-badge status-active">Active</span>
{% else %} {% else %}
<span class="badge badge-warning">Scheduled</span> <span class="status-badge status-scheduled">Scheduled</span>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="badge badge-secondary">Inactive</span> <span class="status-badge status-inactive">Inactive</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if announcement.start_date %} {% if announcement.start_date %}
{{ announcement.start_date.strftime('%Y-%m-%d %H:%M') }} <span class="date-text">{{ announcement.start_date.strftime('%Y-%m-%d %H:%M') }}</span>
{% else %} {% else %}
<em>Immediate</em> <em class="text-muted">Immediate</em>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if announcement.end_date %} {% if announcement.end_date %}
{{ announcement.end_date.strftime('%Y-%m-%d %H:%M') }} <span class="date-text">{{ announcement.end_date.strftime('%Y-%m-%d %H:%M') }}</span>
{% else %} {% else %}
<em>No expiry</em> <em class="text-muted">No expiry</em>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if announcement.target_all_users %} {% if announcement.target_all_users %}
<span class="target-badge target-all">
<i class="ti ti-users"></i>
All Users All Users
</span>
{% else %} {% else %}
<span class="text-muted">Targeted</span> <span class="target-badge target-specific">
<i class="ti ti-target"></i>
Targeted
</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ announcement.created_at.strftime('%Y-%m-%d') }}</td> <td>
<span class="date-text">{{ announcement.created_at.strftime('%Y-%m-%d') }}</span>
</td>
<td> <td>
<div class="action-buttons"> <div class="action-buttons">
<a href="{{ url_for('announcements.edit', id=announcement.id) }}" <a href="{{ url_for('announcements.edit', id=announcement.id) }}"
class="btn btn-sm btn-outline-primary" title="Edit"> class="btn-icon" title="Edit">
✏️ <i class="ti ti-pencil"></i>
</a> </a>
<form method="POST" action="{{ url_for('announcements.delete', id=announcement.id) }}" <form method="POST" action="{{ url_for('announcements.delete', id=announcement.id) }}"
style="display: inline-block;" style="display: inline-block;"
onsubmit="return confirm('Are you sure you want to delete this announcement?')"> 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 type="submit" class="btn-icon danger" title="Delete">
🗑️ <i class="ti ti-trash"></i>
</button> </button>
</form> </form>
</div> </div>
@@ -93,15 +119,20 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
<!-- Pagination --> <!-- Pagination -->
{% if announcements.pages > 1 %} {% if announcements.pages > 1 %}
<div class="pagination-container"> <div class="pagination-container">
<div class="pagination"> <div class="pagination">
{% if announcements.has_prev %} {% if announcements.has_prev %}
<a href="{{ url_for('announcements.index', page=announcements.prev_num) }}" class="page-link">« Previous</a> <a href="{{ url_for('announcements.index', page=announcements.prev_num) }}" class="page-link">
<i class="ti ti-chevron-left"></i>
Previous
</a>
{% endif %} {% endif %}
<div class="page-numbers">
{% for page_num in announcements.iter_pages() %} {% for page_num in announcements.iter_pages() %}
{% if page_num %} {% if page_num %}
{% if page_num != announcements.page %} {% if page_num != announcements.page %}
@@ -110,22 +141,34 @@
<span class="page-link current">{{ page_num }}</span> <span class="page-link current">{{ page_num }}</span>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="page-link"></span> <span class="page-dots">...</span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div>
{% if announcements.has_next %} {% if announcements.has_next %}
<a href="{{ url_for('announcements.index', page=announcements.next_num) }}" class="page-link">Next »</a> <a href="{{ url_for('announcements.index', page=announcements.next_num) }}" class="page-link">
Next
<i class="ti ti-chevron-right"></i>
</a>
{% endif %} {% endif %}
</div> </div>
<div class="pagination-info">
Showing {{ announcements.per_page * (announcements.page - 1) + 1 }} -
{{ announcements.per_page * (announcements.page - 1) + announcements.items|length }} of {{ announcements.total }} announcements
</div>
</div> </div>
{% endif %} {% endif %}
{% else %} {% else %}
<!-- Empty State -->
<div class="empty-state"> <div class="empty-state">
<h3>No announcements found</h3> <div class="empty-icon"><i class="ti ti-speakerphone"></i></div>
<p>Create your first announcement to communicate with users.</p> <h3 class="empty-title">No announcements found</h3>
<p class="empty-message">Create your first announcement to communicate with users.</p>
<a href="{{ url_for('announcements.create') }}" class="btn btn-primary"> <a href="{{ url_for('announcements.create') }}" class="btn btn-primary">
<i class="ti ti-plus"></i>
Create Announcement Create Announcement
</a> </a>
</div> </div>
@@ -133,33 +176,417 @@
</div> </div>
<style> <style>
.inactive { /* Container */
.announcements-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Page Header */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-icon {
font-size: 2.5rem;
display: inline-block;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
.header-actions {
display: flex;
gap: 1rem;
}
/* Cards */
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
margin-bottom: 1.5rem;
overflow: hidden;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card-body {
padding: 1.5rem;
}
.card-body.no-padding {
padding: 0;
}
/* Table */
.table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
.table th,
.table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
.table th {
background: #f8f9fa;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.8rem;
}
.table tr:hover {
background: #f8f9fa;
}
.inactive-row {
opacity: 0.6; opacity: 0.6;
} }
.badge { /* Announcement Title */
padding: 2px 8px; .announcement-title {
border-radius: 12px; display: flex;
font-size: 0.75em; align-items: center;
font-weight: bold; gap: 0.5rem;
text-transform: uppercase;
} }
.badge-info { background: #17a2b8; color: white; } /* Badges */
.badge-warning { background: #ffc107; color: #212529; } .badge,
.badge-success { background: #28a745; color: white; } .status-badge,
.badge-danger { background: #dc3545; color: white; } .type-badge,
.badge-secondary { background: #6c757d; color: white; } .target-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.badge-urgent {
background: #fee2e2;
color: #991b1b;
}
/* Type Badges */
.type-info {
background: #dbeafe;
color: #1e40af;
}
.type-warning {
background: #fef3c7;
color: #92400e;
}
.type-success {
background: #d1fae5;
color: #065f46;
}
.type-danger {
background: #fee2e2;
color: #991b1b;
}
/* Status Badges */
.status-active {
background: #d1fae5;
color: #065f46;
}
.status-scheduled {
background: #fef3c7;
color: #92400e;
}
.status-inactive {
background: #e5e7eb;
color: #374151;
}
/* Target Badges */
.target-badge {
font-size: 0.875rem;
}
.target-all {
background: #ede9fe;
color: #5b21b6;
}
.target-specific {
background: #dbeafe;
color: #1e40af;
}
/* Date Text */
.date-text {
color: #6b7280;
font-size: 0.9rem;
}
.text-muted {
color: #9ca3af;
font-style: italic;
}
/* Action Buttons */
.action-buttons { .action-buttons {
display: flex; display: flex;
gap: 5px; gap: 0.5rem;
} }
.btn-icon {
color: #6b7280;
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
cursor: pointer;
}
.btn-icon:hover {
background: #667eea;
color: white;
transform: translateY(-1px);
}
.btn-icon.danger:hover {
background: #dc2626;
}
/* Pagination */
.pagination-container {
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
background: white;
border-radius: 12px;
border: 1px solid #e5e7eb;
}
.pagination {
display: flex;
align-items: center;
gap: 0.5rem;
}
.page-numbers {
display: flex;
gap: 0.25rem;
}
.page-link {
padding: 0.5rem 1rem;
border: 1px solid #e5e7eb;
color: #667eea;
text-decoration: none;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.page-link:hover {
background: #f3f4f6;
border-color: #667eea;
}
.page-link.current {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-color: transparent;
}
.page-dots {
padding: 0.5rem;
color: #6b7280;
}
.pagination-info {
color: #6b7280;
font-size: 0.875rem;
}
/* Empty State */
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 3rem; padding: 4rem 2rem;
color: #6c757d; background: white;
border-radius: 12px;
border: 1px solid #e5e7eb;
} }
.empty-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
opacity: 0.3;
}
.empty-title {
font-size: 1.75rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 0.5rem;
}
.empty-message {
font-size: 1.1rem;
color: #6b7280;
margin-bottom: 2rem;
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
/* Responsive Design */
@media (max-width: 768px) {
.announcements-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.header-actions {
width: 100%;
flex-direction: column;
}
.table {
font-size: 0.8rem;
}
.table th,
.table td {
padding: 0.5rem;
}
.action-buttons {
flex-direction: column;
gap: 0.25rem;
}
.pagination-container {
flex-direction: column;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -1,22 +1,33 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="management-container"> <div class="branding-container">
<div class="management-header"> <!-- Header Section -->
<h1>🎨 Branding Settings</h1> <div class="page-header">
<div class="management-actions"> <div class="header-content">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary"><i class="ti ti-arrow-left"></i> Back to Dashboard</a> <div class="header-left">
<h1 class="page-title">
<span class="page-icon"><i class="ti ti-palette"></i></span>
Branding Settings
</h1>
<p class="page-subtitle">Customize the appearance and branding of {{ branding.app_name }}</p>
</div>
<div class="header-actions">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">
<i class="ti ti-arrow-left"></i>
Back to Dashboard
</a>
</div>
</div> </div>
</div> </div>
<p class="subtitle">Customize the appearance and branding of {{ branding.app_name }}</p>
<!-- Current Branding Preview --> <!-- Current Branding Preview -->
<div class="management-section"> <div class="card preview-card">
<h2>👁️ Current Branding Preview</h2>
<div class="management-card branding-preview-card">
<div class="card-header"> <div class="card-header">
<h3>Live Preview</h3> <h2 class="card-title">
<span class="icon"><i class="ti ti-eye"></i></span>
Current Branding Preview
</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="preview-demo"> <div class="preview-demo">
@@ -39,16 +50,20 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Branding Settings Form --> <!-- Branding Settings Form -->
<div class="management-section"> <div class="card">
<h2><i class="ti ti-tool"></i> Branding Configuration</h2> <div class="card-header">
<div class="management-card"> <h2 class="card-title">
<span class="icon"><i class="ti ti-settings"></i></span>
Branding Configuration
</h2>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data" class="settings-form"> <form method="POST" enctype="multipart/form-data" class="settings-form">
<!-- Application Name --> <!-- Application Name -->
<div class="form-section"> <div class="form-section">
<h3>📝 Basic Information</h3> <h3><i class="ti ti-forms"></i> Basic Information</h3>
<div class="form-group"> <div class="form-group">
<label for="app_name">Application Name</label> <label for="app_name">Application Name</label>
<input type="text" id="app_name" name="app_name" <input type="text" id="app_name" name="app_name"
@@ -56,7 +71,7 @@
class="form-control" class="form-control"
placeholder="TimeTrack" placeholder="TimeTrack"
required> required>
<small class="form-text text-muted"> <small class="form-text">
This name will appear in the title, navigation, and throughout the interface. This name will appear in the title, navigation, and throughout the interface.
</small> </small>
</div> </div>
@@ -67,7 +82,7 @@
value="{{ branding.logo_alt_text }}" value="{{ branding.logo_alt_text }}"
class="form-control" class="form-control"
placeholder="Company Logo"> placeholder="Company Logo">
<small class="form-text text-muted"> <small class="form-text">
Text displayed when the logo cannot be loaded (accessibility). Text displayed when the logo cannot be loaded (accessibility).
</small> </small>
</div> </div>
@@ -75,7 +90,7 @@
<!-- Visual Assets --> <!-- Visual Assets -->
<div class="form-section"> <div class="form-section">
<h3>🖼️ Visual Assets</h3> <h3><i class="ti ti-photo"></i> Visual Assets</h3>
<div class="form-row"> <div class="form-row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label for="logo_file">Logo Image</label> <label for="logo_file">Logo Image</label>
@@ -90,7 +105,7 @@
<span class="asset-label">Current logo</span> <span class="asset-label">Current logo</span>
</div> </div>
{% endif %} {% endif %}
<small class="form-text text-muted"> <small class="form-text">
PNG, JPG, GIF, SVG. Recommended: 200x50px PNG, JPG, GIF, SVG. Recommended: 200x50px
</small> </small>
</div> </div>
@@ -108,7 +123,7 @@
<span class="asset-label">Current favicon</span> <span class="asset-label">Current favicon</span>
</div> </div>
{% endif %} {% endif %}
<small class="form-text text-muted"> <small class="form-text">
ICO, PNG. Recommended: 16x16px or 32x32px ICO, PNG. Recommended: 16x16px or 32x32px
</small> </small>
</div> </div>
@@ -117,7 +132,7 @@
<!-- Theme Settings --> <!-- Theme Settings -->
<div class="form-section"> <div class="form-section">
<h3>🎨 Theme Settings</h3> <h3><i class="ti ti-color-swatch"></i> Theme Settings</h3>
<div class="form-group"> <div class="form-group">
<label for="primary_color">Primary Color</label> <label for="primary_color">Primary Color</label>
<div class="color-picker-wrapper"> <div class="color-picker-wrapper">
@@ -130,7 +145,7 @@
pattern="^#[0-9A-Fa-f]{6}$" pattern="^#[0-9A-Fa-f]{6}$"
placeholder="#007bff"> placeholder="#007bff">
</div> </div>
<small class="form-text text-muted"> <small class="form-text">
This color will be used for buttons, links, and other UI elements. This color will be used for buttons, links, and other UI elements.
</small> </small>
</div> </div>
@@ -138,7 +153,7 @@
<!-- Imprint/Legal Page --> <!-- Imprint/Legal Page -->
<div class="form-section"> <div class="form-section">
<h3>⚖️ Imprint / Legal Page</h3> <h3><i class="ti ti-scale"></i> Imprint / Legal Page</h3>
<div class="form-group"> <div class="form-group">
<label class="toggle-label"> <label class="toggle-label">
<input type="checkbox" name="imprint_enabled" id="imprint_enabled" <input type="checkbox" name="imprint_enabled" id="imprint_enabled"
@@ -146,7 +161,7 @@
<span class="toggle-slider"></span> <span class="toggle-slider"></span>
<span class="toggle-text">Enable Imprint Page</span> <span class="toggle-text">Enable Imprint Page</span>
</label> </label>
<small class="form-text text-muted"> <small class="form-text">
When enabled, an "Imprint" link will appear in the footer linking to your custom legal page. When enabled, an "Imprint" link will appear in the footer linking to your custom legal page.
</small> </small>
</div> </div>
@@ -158,7 +173,7 @@
value="{{ branding.imprint_title or 'Imprint' }}" value="{{ branding.imprint_title or 'Imprint' }}"
class="form-control" class="form-control"
placeholder="Imprint"> placeholder="Imprint">
<small class="form-text text-muted"> <small class="form-text">
The title that will be displayed on the imprint page. The title that will be displayed on the imprint page.
</small> </small>
</div> </div>
@@ -169,7 +184,7 @@
class="form-control content-editor" class="form-control content-editor"
rows="15" rows="15"
placeholder="Enter your imprint/legal information here...">{{ branding.imprint_content or '' }}</textarea> placeholder="Enter your imprint/legal information here...">{{ branding.imprint_content or '' }}</textarea>
<small class="form-text text-muted"> <small class="form-text">
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; 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> </small>
</div> </div>
@@ -178,7 +193,10 @@
<!-- Save Button --> <!-- Save Button -->
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-primary">💾 Save Branding Settings</button> <button type="submit" class="btn btn-primary">
<i class="ti ti-device-floppy"></i>
Save Branding Settings
</button>
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">Cancel</a> <a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">Cancel</a>
</div> </div>
</form> </form>
@@ -187,8 +205,99 @@
</div> </div>
<style> <style>
/* Branding-specific styles */ /* Container */
.branding-preview-card { .branding-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* Page Header */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-icon {
font-size: 2.5rem;
display: inline-block;
animation: rotate 8s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
/* Cards */
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
margin-bottom: 1.5rem;
overflow: hidden;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card-header {
background: #f8f9fa;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
color: #1f2937;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title .icon {
font-size: 1.5rem;
}
.card-body {
padding: 1.5rem;
}
/* Preview Card */
.preview-card {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@@ -202,7 +311,7 @@
.demo-header { .demo-header {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
padding-bottom: 1rem; padding-bottom: 1rem;
border-bottom: 1px solid #dee2e6; border-bottom: 1px solid #e5e7eb;
} }
.demo-logo { .demo-logo {
@@ -213,8 +322,8 @@
.demo-text-logo { .demo-text-logo {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; font-weight: 700;
color: #333; color: #1f2937;
} }
.demo-content { .demo-content {
@@ -222,7 +331,7 @@
} }
.demo-content p { .demo-content p {
color: #6c757d; color: #6b7280;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@@ -230,12 +339,80 @@
margin-right: 1rem; margin-right: 1rem;
} }
/* Current assets display */ /* Form Sections */
.form-section {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid #e5e7eb;
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
.form-section h3 {
margin-bottom: 1rem;
color: #1f2937;
font-size: 1.1rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-section h3 i {
font-size: 1.25rem;
}
/* Form Controls */
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
color: #374151;
}
.form-control {
width: 100%;
padding: 0.625rem 1rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 1rem;
transition: all 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-text {
display: block;
margin-top: 0.25rem;
font-size: 0.875rem;
color: #6b7280;
}
/* File Input */
.form-control-file {
display: block;
width: 100%;
padding: 0.375rem 0;
font-size: 0.875rem;
}
/* Current Assets */
.current-asset { .current-asset {
margin-top: 0.5rem; margin-top: 0.5rem;
padding: 0.75rem; padding: 0.75rem;
background: #f8f9fa; background: #f8f9fa;
border-radius: 4px; border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
@@ -255,10 +432,10 @@
.asset-label { .asset-label {
font-size: 0.875rem; font-size: 0.875rem;
color: #6c757d; color: #6b7280;
} }
/* Color picker styling */ /* Color Picker */
.color-picker-wrapper { .color-picker-wrapper {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
@@ -270,8 +447,8 @@
width: 60px; width: 60px;
height: 38px; height: 38px;
padding: 0.25rem; padding: 0.25rem;
border: 1px solid #ced4da; border: 1px solid #e5e7eb;
border-radius: 4px; border-radius: 8px;
cursor: pointer; cursor: pointer;
} }
@@ -280,58 +457,8 @@
font-family: monospace; font-family: monospace;
} }
/* Form sections */ /* Toggle Switch */
.form-section { .toggle-label {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid #dee2e6;
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
}
.form-section h3 {
margin-bottom: 1rem;
color: #495057;
font-size: 1.1rem;
font-weight: 600;
}
/* File input styling */
.form-control-file {
display: block;
width: 100%;
padding: 0.375rem 0;
}
/* Form row for two-column layout */
.form-row {
display: flex;
flex-wrap: wrap;
margin-right: -0.5rem;
margin-left: -0.5rem;
}
.form-row > .col-md-6 {
flex: 0 0 50%;
max-width: 50%;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
@media (max-width: 768px) {
.form-row > .col-md-6 {
flex: 0 0 100%;
max-width: 100%;
}
}
/* Sync color inputs */
/* Toggle label styling - ensuring proper alignment */
.form-group .toggle-label {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
@@ -349,7 +476,7 @@
display: inline-block; display: inline-block;
width: 50px; width: 50px;
height: 24px; height: 24px;
background: #ccc; background: #e5e7eb;
border-radius: 24px; border-radius: 24px;
transition: background 0.3s; transition: background 0.3s;
flex-shrink: 0; flex-shrink: 0;
@@ -365,10 +492,11 @@
top: 2px; top: 2px;
left: 2px; left: 2px;
transition: transform 0.3s; transition: transform 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
} }
.toggle-label input[type="checkbox"]:checked + .toggle-slider { .toggle-label input[type="checkbox"]:checked + .toggle-slider {
background: var(--primary-color); background: #667eea;
} }
.toggle-label input[type="checkbox"]:checked + .toggle-slider::before { .toggle-label input[type="checkbox"]:checked + .toggle-slider::before {
@@ -377,18 +505,18 @@
.toggle-text { .toggle-text {
font-weight: 500; font-weight: 500;
color: #495057; color: #1f2937;
line-height: 1; line-height: 1;
} }
/* Content editor styling */ /* Content Editor */
.content-editor { .content-editor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.5; line-height: 1.5;
background: #f8f9fa; background: #f8f9fa;
border: 1px solid #ced4da; border: 1px solid #e5e7eb;
border-radius: 4px; border-radius: 8px;
padding: 0.75rem; padding: 0.75rem;
resize: vertical; resize: vertical;
} }
@@ -400,6 +528,108 @@
border-radius: 8px; border-radius: 8px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* Form Row */
.form-row {
display: flex;
flex-wrap: wrap;
margin-right: -0.5rem;
margin-left: -0.5rem;
}
.form-row > .col-md-6 {
flex: 0 0 50%;
max-width: 50%;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
/* Form Actions */
.form-actions {
display: flex;
gap: 1rem;
padding-top: 1.5rem;
border-top: 1px solid #e5e7eb;
margin-top: 2rem;
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
/* Responsive Design */
@media (max-width: 768px) {
.branding-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.form-row > .col-md-6 {
flex: 0 0 100%;
max-width: 100%;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
</style> </style>
<script> <script>

View File

@@ -1,23 +1,59 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="container"> <div class="companies-admin-container">
<div class="header-section"> <!-- Header Section -->
<h1><i class="ti ti-building"></i> System Admin - All Companies</h1> <div class="page-header">
<p class="subtitle">Manage companies across the entire system</p> <div class="header-content">
<div class="header-left">
<h1 class="page-title">
<span class="page-icon"><i class="ti ti-building"></i></span>
All Companies
</h1>
<p class="page-subtitle">Manage companies across the entire system</p>
</div>
<div class="header-actions"> <div class="header-actions">
<a href="/setup" class="btn btn-md btn-success">+ Add New Company</a> <a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-md btn-secondary"><i class="ti ti-arrow-left"></i> Back to Dashboard</a> <i class="ti ti-arrow-left"></i>
Back to Dashboard
</a>
<a href="/setup" class="btn btn-primary">
<i class="ti ti-plus"></i>
Add New Company
</a>
</div>
</div> </div>
</div> </div>
<!-- Companies Table --> <!-- Summary Statistics -->
<div class="stats-section">
<div class="stat-card">
<div class="stat-value">{{ companies.total }}</div>
<div class="stat-label">Total Companies</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ companies.items | selectattr('is_personal') | list | length }}</div>
<div class="stat-label">Personal Companies</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ companies.items | rejectattr('is_personal') | list | length }}</div>
<div class="stat-label">Business Companies</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ companies.items | selectattr('is_active') | list | length }}</div>
<div class="stat-label">Active Companies</div>
</div>
</div>
<!-- Main Content -->
<div class="content-section">
{% if companies.items %} {% if companies.items %}
<div class="table-section"> <!-- Companies Table -->
<table class="table"> <div class="table-container">
<table class="data-table">
<thead> <thead>
<tr> <tr>
<th>Company Name</th> <th>Company</th>
<th>Type</th> <th>Type</th>
<th>Users</th> <th>Users</th>
<th>Admins</th> <th>Admins</th>
@@ -28,12 +64,14 @@
</thead> </thead>
<tbody> <tbody>
{% for company in companies.items %} {% for company in companies.items %}
<tr class="{% if not company.is_active %}inactive-company{% endif %}"> <tr class="{% if not company.is_active %}inactive-row{% endif %}">
<td> <td>
<strong>{{ company.name }}</strong> <div class="company-cell">
<div class="company-name">{{ company.name }}</div>
{% if company.slug %} {% if company.slug %}
<br><small class="text-muted">{{ company.slug }}</small> <div class="company-slug">{{ company.slug }}</div>
{% endif %} {% endif %}
</div>
</td> </td>
<td> <td>
{% if company.is_personal %} {% if company.is_personal %}
@@ -43,12 +81,16 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<div class="stat-cell">
<span class="stat-number">{{ company_stats[company.id]['user_count'] }}</span> <span class="stat-number">{{ company_stats[company.id]['user_count'] }}</span>
<small>users</small> <span class="stat-label">users</span>
</div>
</td> </td>
<td> <td>
<div class="stat-cell">
<span class="stat-number">{{ company_stats[company.id]['admin_count'] }}</span> <span class="stat-number">{{ company_stats[company.id]['admin_count'] }}</span>
<small>admins</small> <span class="stat-label">admins</span>
</div>
</td> </td>
<td> <td>
{% if company.is_active %} {% if company.is_active %}
@@ -57,11 +99,15 @@
<span class="status-badge status-inactive">Inactive</span> <span class="status-badge status-inactive">Inactive</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ company.created_at.strftime('%Y-%m-%d') }}</td>
<td> <td>
<div class="action-buttons"> <span class="date-text">{{ company.created_at.strftime('%Y-%m-%d') }}</span>
</td>
<td>
<div class="table-actions">
<a href="{{ url_for('system_admin.system_admin_company_detail', company_id=company.id) }}" <a href="{{ url_for('system_admin.system_admin_company_detail', company_id=company.id) }}"
class="btn btn-sm btn-primary">View Details</a> class="btn-icon" title="View Details">
<i class="ti ti-eye"></i>
</a>
</div> </div>
</td> </td>
</tr> </tr>
@@ -72,242 +118,474 @@
<!-- Pagination --> <!-- Pagination -->
{% if companies.pages > 1 %} {% if companies.pages > 1 %}
<div class="pagination-section"> <div class="pagination-container">
<div class="pagination"> <div class="pagination">
{% if companies.has_prev %} {% if companies.has_prev %}
<a href="{{ url_for('system_admin.system_admin_companies', page=companies.prev_num) }}" class="page-link"><i class="ti ti-arrow-left"></i> Previous</a> <a href="{{ url_for('system_admin.system_admin_companies', page=companies.prev_num) }}"
class="page-link">
<i class="ti ti-chevron-left"></i>
Previous
</a>
{% endif %} {% endif %}
<div class="page-numbers">
{% for page_num in companies.iter_pages() %} {% for page_num in companies.iter_pages() %}
{% if page_num %} {% if page_num %}
{% if page_num != companies.page %} {% if page_num != companies.page %}
<a href="{{ url_for('system_admin.system_admin_companies', page=page_num) }}" class="page-link">{{ page_num }}</a> <a href="{{ url_for('system_admin.system_admin_companies', page=page_num) }}"
class="page-link">{{ page_num }}</a>
{% else %} {% else %}
<span class="page-link current">{{ page_num }}</span> <span class="page-link current">{{ page_num }}</span>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="page-link"></span> <span class="page-dots">...</span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div>
{% if companies.has_next %} {% if companies.has_next %}
<a href="{{ url_for('system_admin.system_admin_companies', page=companies.next_num) }}" class="page-link">Next <i class="ti ti-arrow-right"></i></a> <a href="{{ url_for('system_admin.system_admin_companies', page=companies.next_num) }}"
class="page-link">
Next
<i class="ti ti-chevron-right"></i>
</a>
{% endif %} {% endif %}
</div> </div>
<p class="pagination-info"> <div class="pagination-info">
Showing {{ companies.per_page * (companies.page - 1) + 1 }} - Showing {{ companies.per_page * (companies.page - 1) + 1 }} -
{{ companies.per_page * (companies.page - 1) + companies.items|length }} of {{ companies.total }} companies {{ companies.per_page * (companies.page - 1) + companies.items|length }} of {{ companies.total }} companies
</p> </div>
</div> </div>
{% endif %} {% endif %}
{% else %} {% else %}
<!-- Empty State -->
<div class="empty-state"> <div class="empty-state">
<h3>No companies found</h3> <div class="empty-icon"><i class="ti ti-building-community"></i></div>
<p>No companies exist in the system yet.</p> <h3 class="empty-title">No Companies Yet</h3>
<p class="empty-message">No companies exist in the system.</p>
<a href="/setup" class="btn btn-primary">
<i class="ti ti-plus"></i>
Create First Company
</a>
</div> </div>
{% endif %} {% endif %}
<!-- Company Statistics Summary -->
<div class="summary-section">
<h3><i class="ti ti-chart-bar"></i> Company Summary</h3>
<div class="summary-grid">
<div class="summary-card">
<h4>Total Companies</h4>
<p class="summary-number">{{ companies.total }}</p>
</div>
<div class="summary-card">
<h4>Personal Companies</h4>
<p class="summary-number">{{ companies.items | selectattr('is_personal') | list | length }}</p>
</div>
<div class="summary-card">
<h4>Business Companies</h4>
<p class="summary-number">{{ companies.items | rejectattr('is_personal') | list | length }}</p>
</div>
<div class="summary-card">
<h4>Active Companies</h4>
<p class="summary-number">{{ companies.items | selectattr('is_active') | list | length }}</p>
</div>
</div>
</div> </div>
</div> </div>
<style> <style>
.header-section { /* Container */
.companies-admin-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Page Header */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-icon {
font-size: 2.5rem;
display: inline-block;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
.header-actions {
display: flex;
gap: 1rem;
}
/* Stats Section */
.stats-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.subtitle { .stat-card {
color: #6c757d;
margin-bottom: 1rem;
}
.table-section {
background: white; background: white;
border-radius: 8px; padding: 1.5rem;
overflow: hidden; border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center;
margin-bottom: 2rem; border: 1px solid #e5e7eb;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
} }
.table { .stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: #667eea;
}
.stat-label {
font-size: 0.9rem;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
/* Content Section */
.content-section {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
overflow: hidden;
}
/* Table Container */
.table-container {
overflow-x: auto;
}
.data-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin: 0;
} }
.table th, .data-table th,
.table td { .data-table td {
padding: 1rem; padding: 1rem 1.5rem;
text-align: left; text-align: left;
border-bottom: 1px solid #dee2e6; border-bottom: 1px solid #e5e7eb;
} }
.table th { .data-table th {
background: #f8f9fa; background: #f8f9fa;
font-weight: 600; font-weight: 600;
color: #495057; color: #374151;
}
.inactive-company {
background-color: #f8f9fa !important;
opacity: 0.7;
}
.text-muted {
color: #6c757d;
font-size: 0.875rem; font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
} }
.data-table tr:hover {
background: #f8f9fa;
}
.inactive-row {
opacity: 0.6;
}
/* Company Cell */
.company-cell {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.company-name {
font-weight: 600;
color: #1f2937;
}
.company-slug {
font-size: 0.875rem;
color: #6b7280;
}
/* Stat Cell */
.stat-cell {
display: flex;
align-items: baseline;
gap: 0.25rem;
}
.stat-cell .stat-number {
font-weight: 700;
color: #667eea;
font-size: 1.1rem;
}
.stat-cell .stat-label {
font-size: 0.875rem;
color: #6b7280;
}
/* Date Text */
.date-text {
color: #6b7280;
font-size: 0.95rem;
}
/* Badges */
.badge { .badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.75rem;
border-radius: 4px; border-radius: 20px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 600;
text-transform: uppercase;
} }
.badge-company { .badge-company {
background: #d1ecf1; background: #dbeafe;
color: #0c5460; color: #1e40af;
} }
.badge-freelancer { .badge-freelancer {
background: #d4edda; background: #d1fae5;
color: #155724; color: #065f46;
}
.stat-number {
font-weight: 600;
color: #007bff;
} }
/* Status Badges */
.status-badge { .status-badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.75rem;
border-radius: 4px; border-radius: 20px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 600;
text-transform: uppercase;
} }
.status-active { .status-active {
background: #d4edda; background: #d1fae5;
color: #155724; color: #065f46;
} }
.status-inactive { .status-inactive {
background: #f8d7da; background: #fee2e2;
color: #721c24; color: #991b1b;
} }
.action-buttons { /* Table Actions */
.table-actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
} }
/* Button styles now centralized in main style.css */ .btn-icon {
color: #6b7280;
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
background: #f3f4f6;
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
cursor: pointer;
}
.pagination-section { .btn-icon:hover {
margin: 2rem 0; background: #667eea;
color: white;
transform: translateY(-1px);
}
/* Pagination */
.pagination-container {
padding: 1.5rem;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem;
border-top: 1px solid #e5e7eb;
} }
.pagination { .pagination {
display: flex;
align-items: center;
gap: 0.5rem;
}
.page-numbers {
display: flex; display: flex;
gap: 0.25rem; gap: 0.25rem;
} }
.page-link { .page-link {
padding: 0.5rem 0.75rem; padding: 0.5rem 1rem;
border: 1px solid #dee2e6; border: 1px solid #e5e7eb;
color: #007bff; color: #667eea;
text-decoration: none; text-decoration: none;
border-radius: 4px; border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.25rem;
} }
.page-link:hover { .page-link:hover {
background: #e9ecef; background: #f3f4f6;
border-color: #667eea;
} }
.page-link.current { .page-link.current {
background: #007bff; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border-color: #007bff; border-color: transparent;
}
.page-dots {
padding: 0.5rem;
color: #6b7280;
} }
.pagination-info { .pagination-info {
color: #6c757d; color: #6b7280;
margin: 0; font-size: 0.875rem;
font-size: 0.9rem;
} }
/* Empty State */
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 3rem; padding: 4rem 2rem;
color: #6c757d;
} }
.summary-section { .empty-icon {
background: #f8f9fa; font-size: 4rem;
border-radius: 8px;
padding: 2rem;
}
.summary-section h3 {
margin-top: 0;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
color: #495057; opacity: 0.3;
} }
.summary-grid { .empty-title {
display: grid; font-size: 1.75rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); font-weight: 700;
gap: 1rem; color: #1f2937;
margin-bottom: 0.5rem;
} }
.summary-card { .empty-message {
background: white; font-size: 1.1rem;
color: #6b7280;
margin-bottom: 2rem;
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px; border-radius: 8px;
padding: 1.5rem;
text-align: center;
border: 1px solid #dee2e6;
}
.summary-card h4 {
margin: 0 0 0.5rem 0;
color: #6c757d;
font-size: 0.875rem;
font-weight: 500;
}
.summary-number {
font-size: 2rem;
font-weight: 600; font-weight: 600;
color: #007bff; font-size: 1rem;
margin: 0; cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
/* Responsive Design */
@media (max-width: 768px) {
.companies-admin-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.header-actions {
width: 100%;
flex-direction: column;
}
.table-container {
margin: 0 -1rem;
}
.data-table {
font-size: 0.875rem;
}
.data-table th,
.data-table td {
padding: 0.75rem;
}
.pagination-container {
flex-direction: column;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.content-section {
animation: slideIn 0.3s ease-out;
} }
</style> </style>
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,38 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="container"> <div class="edit-user-container">
<div class="header-section"> <!-- Header Section -->
<h1>✏️ Edit User: {{ user.username }}</h1> <div class="page-header">
<p class="subtitle">System Administrator - Edit user across companies</p> <div class="header-content">
<a href="{{ url_for('users.system_admin_users') }}" class="btn btn-secondary"><i class="ti ti-arrow-left"></i> Back to Users</a> <div class="header-left">
<h1 class="page-title">
<span class="page-icon"><i class="ti ti-user-edit"></i></span>
Edit User: {{ user.username }}
</h1>
<p class="page-subtitle">System Administrator - Edit user across companies</p>
</div>
<div class="header-actions">
<a href="{{ url_for('users.system_admin_users') }}" class="btn btn-secondary">
<i class="ti ti-arrow-left"></i>
Back to Users
</a>
</div>
</div>
</div> </div>
<div class="form-container"> <!-- Main Form -->
<form method="POST"> <form method="POST" class="user-edit-form">
<div class="form-grid"> <div class="form-grid">
<!-- Basic Information --> <!-- Basic Information -->
<div class="form-section"> <div class="card">
<h3>Basic Information</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-user"></i></span>
Basic Information
</h2>
</div>
<div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
@@ -29,10 +48,17 @@
class="form-control"> class="form-control">
</div> </div>
</div> </div>
</div>
<!-- Company & Team Assignment --> <!-- Company & Team Assignment -->
<div class="form-section"> <div class="card">
<h3>Company & Team</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-building"></i></span>
Company & Team
</h2>
</div>
<div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="company_id">Company</label> <label for="company_id">Company</label>
@@ -60,10 +86,17 @@
</select> </select>
</div> </div>
</div> </div>
</div>
<!-- Role & Permissions --> <!-- Role & Permissions -->
<div class="form-section"> <div class="card">
<h3>Role & Permissions</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-shield-check"></i></span>
Role & Permissions
</h2>
</div>
<div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="role">Role</label> <label for="role">Role</label>
@@ -76,40 +109,54 @@
{% endfor %} {% endfor %}
</select> </select>
{% if user.role == Role.SYSTEM_ADMIN %} {% if user.role == Role.SYSTEM_ADMIN %}
<small class="form-text">⚠️ Warning: This user is a System Administrator</small> <small class="form-text warning-text"><i class="ti ti-alert-triangle"></i> Warning: This user is a System Administrator</small>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<!-- Account Status --> <!-- Account Status -->
<div class="form-section"> <div class="card">
<h3>Account Status</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-settings"></i></span>
Account Status
</h2>
</div>
<div class="card-body">
<div class="form-group"> <div class="form-group">
<label class="checkbox-label"> <label class="toggle-label">
<input type="checkbox" name="is_verified" <input type="checkbox" name="is_verified"
{% if user.is_verified %}checked{% endif %}> {% if user.is_verified %}checked{% endif %}>
<span class="checkmark"></span> <span class="toggle-slider"></span>
Email Verified <span class="toggle-text">Email Verified</span>
</label> </label>
<small class="form-text">Whether the user's email address has been verified</small> <small class="form-text">Whether the user's email address has been verified</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="checkbox-label"> <label class="toggle-label">
<input type="checkbox" name="is_blocked" <input type="checkbox" name="is_blocked"
{% if user.is_blocked %}checked{% endif %}> {% if user.is_blocked %}checked{% endif %}>
<span class="checkmark"></span> <span class="toggle-slider"></span>
Account Blocked <span class="toggle-text">Account Blocked</span>
</label> </label>
<small class="form-text">Blocked users cannot log in to the system</small> <small class="form-text">Blocked users cannot log in to the system</small>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- User Information Display --> <!-- User Information Display -->
<div class="info-section"> <div class="card full-width">
<h3>User Information</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-info-circle"></i></span>
User Information
</h2>
</div>
<div class="card-body">
<div class="info-grid"> <div class="info-grid">
<div class="info-item"> <div class="info-item">
<label>Account Type:</label> <label>Account Type:</label>
@@ -138,27 +185,47 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Form Actions --> <!-- Form Actions -->
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button> <button type="submit" class="btn btn-primary">
<i class="ti ti-device-floppy"></i>
Save Changes
</button>
<a href="{{ url_for('users.system_admin_users') }}" class="btn btn-secondary">Cancel</a> <a href="{{ url_for('users.system_admin_users') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
<!-- Danger Zone -->
{% if user.id != g.user.id and not (user.role == Role.SYSTEM_ADMIN and user.id == g.user.id) %} {% if user.id != g.user.id and not (user.role == Role.SYSTEM_ADMIN and user.id == g.user.id) %}
<div class="danger-zone"> <div class="danger-zone">
<h4>Danger Zone</h4> <div class="danger-header">
<p>Permanently delete this user account. This action cannot be undone.</p> <h2 class="danger-title">
<i class="ti ti-alert-triangle"></i>
Danger Zone
</h2>
</div>
<div class="danger-content">
<div class="danger-item">
<div class="danger-info">
<h4>Delete User Account</h4>
<p>Permanently delete this user account. This will also delete all their time entries and cannot be undone.</p>
</div>
<div class="danger-actions">
<form method="POST" action="{{ url_for('users.system_admin_delete_user', user_id=user.id) }}" <form method="POST" action="{{ url_for('users.system_admin_delete_user', user_id=user.id) }}"
style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete user \'{{ user.username }}\'? This will also delete all their time entries and cannot be undone.')"> onsubmit="return confirm('Are you sure you want to delete user \'{{ user.username }}\'? This will also delete all their time entries and cannot be undone.')">
<button type="submit" class="btn btn-danger">Delete User</button> <button type="submit" class="btn btn-danger">
<i class="ti ti-trash"></i>
Delete User
</button>
</form> </form>
</div> </div>
</div>
</div>
</div>
{% endif %} {% endif %}
</div> </div>
</form>
</div>
</div>
<script> <script>
// Dynamic team loading when company changes // Dynamic team loading when company changes
@@ -191,175 +258,402 @@ document.getElementById('company_id').addEventListener('change', function() {
</script> </script>
<style> <style>
.header-section { /* Container */
margin-bottom: 2rem; .edit-user-container {
} max-width: 1200px;
margin: 0 auto;
.subtitle {
color: #6c757d;
margin-bottom: 1rem;
}
.form-container {
max-width: 800px;
background: white;
border-radius: 8px;
padding: 2rem; padding: 2rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
} }
/* Page Header */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-icon {
font-size: 2.5rem;
display: inline-block;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
/* Form Grid */
.form-grid { .form-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 2rem; gap: 1.5rem;
margin-bottom: 2rem; margin-bottom: 1.5rem;
} }
.form-section { /* Cards */
border: 1px solid #dee2e6; .card {
border-radius: 8px; background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
overflow: hidden;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card.full-width {
grid-column: 1 / -1;
}
.card-header {
background: #f8f9fa;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
color: #1f2937;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title .icon {
font-size: 1.5rem;
}
.card-body {
padding: 1.5rem; padding: 1.5rem;
} }
.form-section h3 { /* Form Elements */
margin-top: 0;
margin-bottom: 1.5rem;
color: #495057;
border-bottom: 2px solid #e9ecef;
padding-bottom: 0.5rem;
}
.form-group { .form-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.form-group label { .form-group label {
display: block; display: block;
font-weight: 600;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-weight: 500; color: #374151;
color: #495057;
} }
.form-control { .form-control {
width: 100%; width: 100%;
padding: 0.75rem; padding: 0.625rem 1rem;
border: 1px solid #ced4da; border: 1px solid #e5e7eb;
border-radius: 4px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; transition: all 0.2s ease;
} }
.form-control:focus { .form-control:focus {
border-color: #007bff; outline: none;
outline: 0; border-color: #667eea;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
} }
.form-text { .form-text {
display: block; display: block;
margin-top: 0.25rem; margin-top: 0.25rem;
font-size: 0.875rem; font-size: 0.875rem;
color: #6c757d; color: #6b7280;
} }
.checkbox-label { .warning-text {
display: flex; color: #dc2626 !important;
font-weight: 500;
}
/* Toggle Switches */
.toggle-label {
display: inline-flex;
align-items: center; align-items: center;
gap: 0.75rem;
cursor: pointer; cursor: pointer;
font-weight: normal; margin-bottom: 0.5rem;
} }
.checkbox-label input[type="checkbox"] { .toggle-label input[type="checkbox"] {
margin-right: 0.5rem; display: none;
margin-bottom: 0;
} }
.info-section { .toggle-slider {
background: #f8f9fa; position: relative;
border-radius: 8px; display: inline-block;
padding: 1.5rem; width: 50px;
margin-bottom: 2rem; height: 24px;
background: #e5e7eb;
border-radius: 24px;
transition: background 0.3s;
flex-shrink: 0;
} }
.info-section h3 { .toggle-slider::before {
margin-top: 0; content: '';
margin-bottom: 1rem; position: absolute;
color: #495057; width: 20px;
height: 20px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: transform 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
} }
.toggle-label input[type="checkbox"]:checked + .toggle-slider {
background: #667eea;
}
.toggle-label input[type="checkbox"]:checked + .toggle-slider::before {
transform: translateX(26px);
}
.toggle-text {
font-weight: 500;
color: #1f2937;
}
/* Info Grid */
.info-grid { .info-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem; gap: 1.5rem;
} }
.info-item { .info-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.25rem; gap: 0.5rem;
} }
.info-item label { .info-item label {
font-weight: 600; font-weight: 600;
color: #6c757d; color: #6b7280;
font-size: 0.875rem; font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
} }
/* Badges */
.badge { .badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.75rem;
border-radius: 4px; border-radius: 20px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 600;
text-transform: uppercase;
display: inline-block; display: inline-block;
width: fit-content;
} }
.badge-company { .badge-company {
background: #d1ecf1; background: #dbeafe;
color: #0c5460; color: #1e40af;
} }
.badge-freelancer { .badge-freelancer {
background: #d4edda; background: #d1fae5;
color: #155724; color: #065f46;
} }
.text-success { .text-success {
color: #28a745; color: #10b981;
font-weight: 500;
} }
.text-muted { .text-muted {
color: #6c757d; color: #6b7280;
} }
/* Form Actions */
.form-actions { .form-actions {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
align-items: center; padding: 2rem;
flex-wrap: wrap; background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
margin-bottom: 2rem;
} }
/* Button styles now centralized in main style.css */ /* Danger Zone */
.danger-zone { .danger-zone {
margin-left: auto; background: #fef2f2;
padding: 1rem; border: 2px solid #fecaca;
border: 2px solid #dc3545; border-radius: 12px;
overflow: hidden;
}
.danger-header {
background: #fee2e2;
padding: 1.5rem;
border-bottom: 1px solid #fecaca;
}
.danger-title {
font-size: 1.25rem;
font-weight: 600;
color: #991b1b;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.danger-content {
padding: 2rem;
}
.danger-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.danger-info h4 {
margin: 0 0 0.5rem 0;
color: #991b1b;
font-weight: 600;
}
.danger-info p {
margin: 0;
color: #7f1d1d;
font-size: 0.9rem;
line-height: 1.5;
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px; border-radius: 8px;
background: #f8d7da; font-weight: 600;
max-width: 300px; font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
} }
.danger-zone h4 { .btn-primary {
color: #721c24; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin-top: 0; color: white;
margin-bottom: 0.5rem;
} }
.danger-zone p { .btn-primary:hover {
color: #721c24; transform: translateY(-2px);
font-size: 0.875rem; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
margin-bottom: 1rem;
} }
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
.btn-danger {
background: #dc2626;
color: white;
}
.btn-danger:hover {
background: #b91c1c;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
/* Responsive Design */
@media (max-width: 768px) {
.edit-user-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.form-grid {
grid-template-columns: 1fr;
}
.danger-item {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
.card:nth-child(4) { animation-delay: 0.4s; }
</style> </style>
<script> <script>

View File

@@ -1,98 +1,117 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="container"> <div class="health-container">
<div class="header-section"> <!-- Header Section -->
<h1><i class="ti ti-heart-rate-monitor"></i> System Health Check</h1> <div class="page-header">
<p class="subtitle">System diagnostics and event monitoring</p> <div class="header-content">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary"><i class="ti ti-arrow-left"></i> Back to Dashboard</a> <div class="header-left">
<h1 class="page-title">
<span class="page-icon"><i class="ti ti-heart-rate-monitor"></i></span>
System Health
</h1>
<p class="page-subtitle">System diagnostics and event monitoring</p>
</div>
<div class="header-actions">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">
<i class="ti ti-arrow-left"></i>
Back to Dashboard
</a>
</div>
</div>
</div> </div>
<!-- System Health Status --> <!-- Health Status Cards -->
<div class="health-status-section">
<h2><i class="ti ti-search"></i> System Status</h2>
<div class="health-cards"> <div class="health-cards">
<div class="health-card {% if health_summary.health_status == 'healthy' %}healthy{% elif health_summary.health_status == 'issues' %}warning{% else %}critical{% endif %}"> <div class="health-card {% if health_status == 'healthy' %}healthy{% elif health_status == 'warning' %}warning{% else %}critical{% endif %}">
<div class="health-icon"> <div class="health-icon">
{% if health_summary.health_status == 'healthy' %} {% if health_status == 'healthy' %}
<i class="ti ti-circle-check" style="font-size: 2rem; color: #10b981;"></i> <i class="ti ti-circle-check"></i>
{% elif health_summary.health_status == 'issues' %} {% elif health_status == 'warning' %}
<i class="ti ti-alert-triangle" style="font-size: 2rem; color: #f59e0b;"></i> <i class="ti ti-alert-triangle"></i>
{% else %} {% else %}
<i class="ti ti-circle-x" style="font-size: 2rem; color: #ef4444;"></i> <i class="ti ti-circle-x"></i>
{% endif %} {% endif %}
</div> </div>
<div class="health-info"> <div class="health-content">
<h3>Overall Health</h3> <div class="health-label">Overall Health</div>
<p class="health-status">{{ health_summary.health_status|title }}</p> <div class="health-value">{{ health_status|title }}</div>
<small> <div class="health-detail">
{% if health_summary.health_status == 'healthy' %} {% if health_status == 'healthy' %}
All systems running normally All systems running normally
{% elif health_summary.health_status == 'issues' %} {% elif health_status == 'warning' %}
Minor issues detected Minor issues detected
{% else %} {% else %}
Critical issues require attention Critical issues require attention
{% endif %} {% endif %}
</small> </div>
</div> </div>
</div> </div>
<div class="health-card {% if db_healthy %}healthy{% else %}critical{% endif %}"> <div class="health-card {% if db_healthy %}healthy{% else %}critical{% endif %}">
<div class="health-icon"> <div class="health-icon">
{% if db_healthy %}<i class="ti ti-circle-check" style="font-size: 2rem; color: #10b981;"></i>{% else %}<i class="ti ti-circle-x" style="font-size: 2rem; color: #ef4444;"></i>{% endif %}
</div>
<div class="health-info">
<h3>Database</h3>
<p class="health-status">{% if db_healthy %}Connected{% else %}Error{% endif %}</p>
<small>
{% if db_healthy %} {% if db_healthy %}
PostgreSQL connection active <i class="ti ti-database"></i>
{% else %}
<i class="ti ti-database-off"></i>
{% endif %}
</div>
<div class="health-content">
<div class="health-label">Database</div>
<div class="health-value">{% if db_healthy %}Connected{% else %}Error{% endif %}</div>
<div class="health-detail">
{% if db_healthy %}
Database connection active
{% else %} {% else %}
{{ db_error }} {{ db_error }}
{% endif %} {% endif %}
</small> </div>
</div> </div>
</div> </div>
<div class="health-card info"> <div class="health-card info">
<div class="health-icon"><i class="ti ti-clock" style="font-size: 2rem;"></i></div> <div class="health-icon">
<div class="health-info"> <i class="ti ti-clock"></i>
<h3>Uptime</h3>
<p class="health-status">{{ uptime_duration.days }}d {{ uptime_duration.seconds//3600 }}h {{ (uptime_duration.seconds//60)%60 }}m</p>
<small>Since first recorded event</small>
</div> </div>
<div class="health-content">
<div class="health-label">Uptime</div>
<div class="health-value">{{ uptime_duration.days }}d {{ uptime_duration.seconds//3600 }}h</div>
<div class="health-detail">Since first recorded event</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Quick Stats --> <!-- Event Statistics -->
<div class="stats-section"> <div class="stats-section">
<h2><i class="ti ti-chart-bar"></i> Event Statistics</h2> <div class="stat-card {% if error_count_24h > 0 %}error{% endif %}">
<div class="stats-grid"> <div class="stat-value">{{ error_count_24h }}</div>
<div class="stat-card {% if health_summary.errors_24h > 0 %}error{% endif %}"> <div class="stat-label">Errors (24h)</div>
<h3>{{ health_summary.errors_24h }}</h3>
<p>Errors (24h)</p>
</div> </div>
<div class="stat-card {% if health_summary.warnings_24h > 0 %}warning{% endif %}"> <div class="stat-card {% if warning_count_24h > 0 %}warning{% endif %}">
<h3>{{ health_summary.warnings_24h }}</h3> <div class="stat-value">{{ warning_count_24h }}</div>
<p>Warnings (24h)</p> <div class="stat-label">Warnings (24h)</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>{{ today_events }}</h3> <div class="stat-value">{{ today_events }}</div>
<p>Events Today</p> <div class="stat-label">Events Today</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>{{ health_summary.total_events_week }}</h3> <div class="stat-value">{{ total_events_week }}</div>
<p>Events This Week</p> <div class="stat-label">Events This Week</div>
</div>
</div> </div>
</div> </div>
<!-- Recent Errors --> <!-- Recent Errors -->
{% if errors %} {% if errors %}
<div class="events-section error-section"> <div class="card error-card">
<h2><i class="ti ti-alert-circle"></i> Recent Errors</h2> <div class="card-header">
<div class="events-list"> <h2 class="card-title">
<span class="icon"><i class="ti ti-alert-circle"></i></span>
Recent Errors
</h2>
</div>
<div class="card-body">
<div class="event-list">
{% for error in errors %} {% for error in errors %}
<div class="event-item error"> <div class="event-item error">
<div class="event-header"> <div class="event-header">
@@ -100,23 +119,35 @@
<span class="event-time">{{ error.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span> <span class="event-time">{{ error.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</div> </div>
<div class="event-description">{{ error.description }}</div> <div class="event-description">{{ error.description }}</div>
<div class="event-meta">
{% if error.user %} {% if error.user %}
<div class="event-meta">User: {{ error.user.username }}</div> <span><i class="ti ti-user"></i> {{ error.user.username }}</span>
{% endif %} {% endif %}
{% if error.company %} {% if error.company %}
<div class="event-meta">Company: {{ error.company.name }}</div> <span><i class="ti ti-building"></i> {{ error.company.name }}</span>
{% endif %}
{% if error.ip_address %}
<span><i class="ti ti-world"></i> {{ error.ip_address }}</span>
{% endif %} {% endif %}
</div> </div>
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div>
{% endif %} {% endif %}
<!-- Recent Warnings --> <!-- Recent Warnings -->
{% if warnings %} {% if warnings %}
<div class="events-section warning-section"> <div class="card warning-card">
<h2><i class="ti ti-alert-triangle"></i> Recent Warnings</h2> <div class="card-header">
<div class="events-list"> <h2 class="card-title">
<span class="icon"><i class="ti ti-alert-triangle"></i></span>
Recent Warnings
</h2>
</div>
<div class="card-body">
<div class="event-list">
{% for warning in warnings %} {% for warning in warnings %}
<div class="event-item warning"> <div class="event-item warning">
<div class="event-header"> <div class="event-header">
@@ -124,236 +155,346 @@
<span class="event-time">{{ warning.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span> <span class="event-time">{{ warning.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</div> </div>
<div class="event-description">{{ warning.description }}</div> <div class="event-description">{{ warning.description }}</div>
<div class="event-meta">
{% if warning.user %} {% if warning.user %}
<div class="event-meta">User: {{ warning.user.username }}</div> <span><i class="ti ti-user"></i> {{ warning.user.username }}</span>
{% endif %} {% endif %}
{% if warning.company %} {% if warning.company %}
<div class="event-meta">Company: {{ warning.company.name }}</div> <span><i class="ti ti-building"></i> {{ warning.company.name }}</span>
{% endif %} {% endif %}
</div> </div>
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div>
{% endif %} {% endif %}
<!-- System Event Log --> <!-- System Event Log -->
<div class="events-section"> <div class="card">
<h2><i class="ti ti-clipboard-list"></i> System Event Log (Last 7 Days)</h2> <div class="card-header">
<div class="events-controls"> <h2 class="card-title">
<span class="icon"><i class="ti ti-clipboard-list"></i></span>
System Event Log
<span class="card-subtitle">Last 7 days</span>
</h2>
</div>
<div class="card-body">
<div class="event-controls">
<button class="filter-btn active" data-filter="all">All Events</button> <button class="filter-btn active" data-filter="all">All Events</button>
<button class="filter-btn" data-filter="auth">Authentication</button> <button class="filter-btn" data-filter="auth">Authentication</button>
<button class="filter-btn" data-filter="user_management">User Management</button> <button class="filter-btn" data-filter="user_management">User Management</button>
<button class="filter-btn" data-filter="system">System</button> <button class="filter-btn" data-filter="system">System</button>
<button class="filter-btn" data-filter="error">Errors</button> <button class="filter-btn" data-filter="error">Errors</button>
</div> </div>
<div class="events-list" id="eventsList"> <div class="event-list" id="eventsList">
{% for event in recent_events %} {% for event in recent_events %}
<div class="event-item {{ event.severity }} {{ event.event_category }}" data-category="{{ event.event_category }}" data-severity="{{ event.severity }}"> <div class="event-item {{ event.severity }}" data-category="{{ event.event_category }}" data-severity="{{ event.severity }}">
<div class="event-header"> <div class="event-header">
<div class="event-info">
<span class="event-type">{{ event.event_type }}</span> <span class="event-type">{{ event.event_type }}</span>
<span class="event-category-badge">{{ event.event_category }}</span> <span class="event-category">{{ event.event_category }}</span>
{% if event.severity != 'info' %}
<span class="severity-badge severity-{{ event.severity }}">{{ event.severity|upper }}</span>
{% endif %}
</div>
<span class="event-time">{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span> <span class="event-time">{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</div> </div>
<div class="event-description">{{ event.description }}</div> <div class="event-description">{{ event.description }}</div>
{% if event.user or event.company or event.ip_address %}
<div class="event-meta">
{% if event.user %} {% if event.user %}
<div class="event-meta">User: {{ event.user.username }}</div> <span><i class="ti ti-user"></i> {{ event.user.username }}</span>
{% endif %} {% endif %}
{% if event.company %} {% if event.company %}
<div class="event-meta">Company: {{ event.company.name }}</div> <span><i class="ti ti-building"></i> {{ event.company.name }}</span>
{% endif %} {% endif %}
{% if event.ip_address %} {% if event.ip_address %}
<div class="event-meta">IP: {{ event.ip_address }}</div> <span><i class="ti ti-world"></i> {{ event.ip_address }}</span>
{% endif %}
</div>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<!-- Last Error Details -->
{% if health_summary.last_error %}
<div class="events-section error-section">
<h2>🔍 Last Critical Error</h2>
<div class="last-error-details">
<div class="error-card">
<div class="error-header">
<h3>{{ health_summary.last_error.event_type }}</h3>
<span class="error-time">{{ health_summary.last_error.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</span>
</div> </div>
<div class="error-description">{{ health_summary.last_error.description }}</div>
{% if health_summary.last_error.event_metadata %}
<div class="error-metadata">
<strong>Additional Details:</strong>
<pre>{{ health_summary.last_error.event_metadata }}</pre>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div> </div>
<style> <style>
.header-section { /* Container */
.health-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Page Header */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
} }
.subtitle { .header-content {
color: #6c757d; display: flex;
margin-bottom: 1rem; justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 2rem;
} }
.health-status-section { .page-title {
margin-bottom: 2rem; font-size: 2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
} }
.page-icon {
font-size: 2.5rem;
display: inline-block;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
/* Health Cards */
.health-cards { .health-cards {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem; gap: 1.5rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.health-card { .health-card {
background: white; background: white;
border-radius: 8px; border-radius: 12px;
padding: 1.5rem; padding: 1.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-left: 4px solid #dee2e6; border: 1px solid #e5e7eb;
border-left: 4px solid #e5e7eb;
transition: all 0.3s ease;
}
.health-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
} }
.health-card.healthy { .health-card.healthy {
border-left-color: #28a745; border-left-color: #10b981;
background: #f8fff9; background: #f0fdf4;
} }
.health-card.warning { .health-card.warning {
border-left-color: #ffc107; border-left-color: #f59e0b;
background: #fffdf7; background: #fffbeb;
} }
.health-card.critical { .health-card.critical {
border-left-color: #dc3545; border-left-color: #ef4444;
background: #fff8f8; background: #fef2f2;
} }
.health-card.info { .health-card.info {
border-left-color: #17a2b8; border-left-color: #3b82f6;
background: #f8fcfd; background: #eff6ff;
} }
.health-icon { .health-icon {
font-size: 2.5rem; font-size: 3rem;
line-height: 1; flex-shrink: 0;
} }
.health-info h3 { .health-card.healthy .health-icon {
margin: 0 0 0.25rem 0; color: #10b981;
color: #495057;
} }
.health-status { .health-card.warning .health-icon {
font-size: 1.1rem; color: #f59e0b;
font-weight: 600;
margin: 0 0 0.25rem 0;
} }
.health-card.healthy .health-status { .health-card.critical .health-icon {
color: #28a745; color: #ef4444;
} }
.health-card.warning .health-status { .health-card.info .health-icon {
color: #ffc107; color: #3b82f6;
} }
.health-card.critical .health-status { .health-content {
color: #dc3545; flex: 1;
} }
.health-card.info .health-status { .health-label {
color: #17a2b8;
}
.health-info small {
color: #6c757d;
font-size: 0.875rem; font-size: 0.875rem;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
margin-bottom: 0.25rem;
} }
.health-value {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.health-card.healthy .health-value {
color: #059669;
}
.health-card.warning .health-value {
color: #d97706;
}
.health-card.critical .health-value {
color: #dc2626;
}
.health-card.info .health-value {
color: #2563eb;
}
.health-detail {
font-size: 0.875rem;
color: #6b7280;
}
/* Stats Section */
.stats-section { .stats-section {
background: #f8f9fa;
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
}
.stats-section h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #495057;
}
.stats-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; gap: 1.5rem;
margin-bottom: 2rem;
} }
.stat-card { .stat-card {
background: white; background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1.5rem; padding: 1.5rem;
border-radius: 12px;
text-align: center; text-align: center;
border-left: 4px solid #dee2e6; border: 1px solid #e5e7eb;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
border-left: 4px solid #e5e7eb;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
} }
.stat-card.error { .stat-card.error {
border-left-color: #dc3545; border-left-color: #ef4444;
background: #fff8f8; background: #fef2f2;
} }
.stat-card.warning { .stat-card.warning {
border-left-color: #ffc107; border-left-color: #f59e0b;
background: #fffdf7; background: #fffbeb;
} }
.stat-card h3 { .stat-value {
font-size: 2rem; font-size: 2.5rem;
margin: 0 0 0.5rem 0; font-weight: 700;
color: #007bff; margin-bottom: 0.5rem;
color: #667eea;
} }
.stat-card.error h3 { .stat-card.error .stat-value {
color: #dc3545; color: #dc2626;
} }
.stat-card.warning h3 { .stat-card.warning .stat-value {
color: #ffc107; color: #d97706;
} }
.stat-card p { .stat-label {
margin: 0; font-size: 0.9rem;
color: #6c757d; color: #6b7280;
font-weight: 500; text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
} }
.events-section { /* Cards */
.card {
background: white; background: white;
border: 1px solid #dee2e6; border-radius: 12px;
border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
padding: 2rem; border: 1px solid #e5e7eb;
margin-bottom: 2rem;
}
.events-section h2 {
margin-top: 0;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
color: #495057; overflow: hidden;
transition: all 0.3s ease;
} }
.events-controls { .card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card.error-card {
border-left: 4px solid #ef4444;
}
.card.warning-card {
border-left: 4px solid #f59e0b;
}
.card-header {
background: #f8f9fa;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
color: #1f2937;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title .icon {
font-size: 1.5rem;
}
.card-subtitle {
font-size: 0.875rem;
color: #6b7280;
font-weight: 400;
margin-left: auto;
}
.card-body {
padding: 1.5rem;
}
/* Event Controls */
.event-controls {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
@@ -362,53 +503,62 @@
.filter-btn { .filter-btn {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border: 1px solid #dee2e6; border: 1px solid #e5e7eb;
background: white; background: white;
color: #495057; color: #6b7280;
border-radius: 4px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s ease;
font-weight: 500; font-weight: 500;
font-size: 0.875rem;
} }
.filter-btn:hover { .filter-btn:hover {
background: #f8f9fa; background: #f3f4f6;
border-color: #adb5bd; border-color: #667eea;
color: #212529; color: #667eea;
} }
.filter-btn.active { .filter-btn.active {
background: #007bff; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border-color: #007bff; border-color: transparent;
} }
.events-list { /* Event List */
.event-list {
max-height: 600px; max-height: 600px;
overflow-y: auto; overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.75rem;
} }
.event-item { .event-item {
border: 1px solid #dee2e6; background: #f8f9fa;
border-radius: 6px; border-radius: 8px;
padding: 1rem; padding: 1rem;
margin-bottom: 0.5rem; border: 1px solid #e5e7eb;
background: white; transition: all 0.2s ease;
}
.event-item:hover {
background: #f3f4f6;
} }
.event-item.error { .event-item.error {
border-left: 4px solid #dc3545; border-left: 4px solid #ef4444;
background: #fff8f8; background: #fef2f2;
} }
.event-item.warning { .event-item.warning {
border-left: 4px solid #ffc107; border-left: 4px solid #f59e0b;
background: #fffdf7; background: #fffbeb;
} }
.event-item.critical { .event-item.critical {
border-left: 4px solid #dc3545; border-left: 4px solid #dc2626;
background: #fff5f5; background: #fee2e2;
} }
.event-header { .event-header {
@@ -420,112 +570,155 @@
gap: 0.5rem; gap: 0.5rem;
} }
.event-type { .event-info {
font-weight: 600; display: flex;
color: #495057; align-items: center;
gap: 0.5rem;
} }
.event-category-badge { .event-type {
background: #e9ecef; font-weight: 600;
color: #495057; color: #1f2937;
}
.event-category {
background: #e5e7eb;
color: #6b7280;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
border-radius: 4px; border-radius: 4px;
font-size: 0.75rem; font-size: 0.75rem;
text-transform: uppercase; text-transform: uppercase;
} }
.severity-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.severity-warning {
background: #fef3c7;
color: #92400e;
}
.severity-error {
background: #fee2e2;
color: #991b1b;
}
.severity-critical {
background: #dc2626;
color: white;
}
.event-time { .event-time {
color: #6c757d; color: #6b7280;
font-size: 0.875rem; font-size: 0.875rem;
} }
.event-description { .event-description {
color: #4b5563;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
color: #495057;
} }
.event-meta { .event-meta {
font-size: 0.875rem;
color: #6c757d;
margin-bottom: 0.25rem;
}
.error-section {
border-left: 4px solid #dc3545;
}
.warning-section {
border-left: 4px solid #ffc107;
}
.last-error-details {
background: #fff8f8;
border: 1px solid #f5c6cb;
border-radius: 6px;
padding: 1rem;
}
.error-card {
background: white;
border-radius: 6px;
padding: 1rem;
}
.error-header {
display: flex; display: flex;
justify-content: space-between; gap: 1rem;
font-size: 0.875rem;
color: #6b7280;
}
.event-meta span {
display: flex;
align-items: center; align-items: center;
margin-bottom: 0.5rem; gap: 0.25rem;
} }
.error-header h3 { .event-meta i {
margin: 0;
color: #dc3545;
}
.error-time {
color: #6c757d;
font-size: 0.875rem; font-size: 0.875rem;
} }
.error-description { /* Buttons */
margin-bottom: 0.5rem; .btn {
color: #495057; padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
} }
.error-metadata { .btn-secondary {
background: #f8f9fa; background: white;
border: 1px solid #dee2e6; color: #667eea;
border-radius: 4px; border: 2px solid rgba(255, 255, 255, 0.3);
padding: 0.5rem;
} }
.error-metadata pre { .btn-secondary:hover {
margin: 0.5rem 0 0 0; background: rgba(255, 255, 255, 0.1);
font-size: 0.875rem; border-color: rgba(255, 255, 255, 0.5);
white-space: pre-wrap;
} }
/* Button styles now centralized in main style.css */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.health-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.health-cards { .health-cards {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.event-header { .event-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
.events-controls { .event-controls {
flex-direction: column; flex-direction: column;
} }
.filter-btn {
width: 100%;
} }
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
</style> </style>
<script> <script>
@@ -547,7 +740,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (filter === 'all') { if (filter === 'all') {
item.style.display = 'block'; item.style.display = 'block';
} else if (filter === 'error') { } else if (filter === 'error') {
item.style.display = item.classList.contains('error') || item.classList.contains('critical') ? 'block' : 'none'; item.style.display = (item.classList.contains('error') || item.classList.contains('critical')) ? 'block' : 'none';
} else { } else {
item.style.display = item.dataset.category === filter ? 'block' : 'none'; item.style.display = item.dataset.category === filter ? 'block' : 'none';
} }

View File

@@ -1,35 +1,51 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="container"> <div class="settings-container">
<div class="header-section"> <!-- Header Section -->
<h1>⚙️ System Administrator Settings</h1> <div class="page-header">
<p class="subtitle">Global system configuration and management</p> <div class="header-content">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary"><i class="ti ti-arrow-left"></i> Back to Dashboard</a> <div class="header-left">
<h1 class="page-title">
<span class="page-icon"><i class="ti ti-settings"></i></span>
System Administrator Settings
</h1>
<p class="page-subtitle">Global system configuration and management</p>
</div>
<div class="header-actions">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">
<i class="ti ti-arrow-left"></i>
Back to Dashboard
</a>
</div>
</div>
</div> </div>
<!-- System Statistics --> <!-- System Statistics -->
<div class="stats-section"> <div class="stats-section">
<h3>📊 System Overview</h3>
<div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
<h4>{{ total_companies }}</h4> <div class="stat-value">{{ total_companies }}</div>
<p>Total Companies</p> <div class="stat-label">Total Companies</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h4>{{ total_users }}</h4> <div class="stat-value">{{ total_users }}</div>
<p>Total Users</p> <div class="stat-label">Total Users</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h4>{{ total_system_admins }}</h4> <div class="stat-value">{{ total_system_admins }}</div>
<p>System Administrators</p> <div class="stat-label">System Administrators</div>
</div>
</div> </div>
</div> </div>
<!-- System Settings Form --> <!-- System Settings Form -->
<div class="settings-section"> <div class="card">
<h3>🔧 System Configuration</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-adjustments"></i></span>
System Configuration
</h2>
</div>
<div class="card-body">
<form method="POST" class="settings-form"> <form method="POST" class="settings-form">
<div class="setting-group"> <div class="setting-group">
<div class="setting-header"> <div class="setting-header">
@@ -112,10 +128,17 @@
</div> </div>
</form> </form>
</div> </div>
</div>
<!-- System Information --> <!-- System Information -->
<div class="info-section"> <div class="card">
<h3> System Information</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-info-circle"></i></span>
System Information
</h2>
</div>
<div class="card-body">
<div class="info-grid"> <div class="info-grid">
<div class="info-card"> <div class="info-card">
<h4>Application Version</h4> <h4>Application Version</h4>
@@ -131,10 +154,16 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Danger Zone --> <!-- Danger Zone -->
<div class="danger-section"> <div class="danger-zone">
<h3>⚠️ Danger Zone</h3> <div class="danger-header">
<h2 class="danger-title">
<i class="ti ti-alert-triangle"></i>
Danger Zone
</h2>
</div>
<div class="danger-content"> <div class="danger-content">
<div class="danger-item"> <div class="danger-item">
<div class="danger-info"> <div class="danger-info">
@@ -164,8 +193,14 @@
</div> </div>
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="actions-section"> <div class="card">
<h3>🚀 Quick Actions</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-rocket"></i></span>
Quick Actions
</h2>
</div>
<div class="card-body">
<div class="actions-grid"> <div class="actions-grid">
<a href="{{ url_for('users.system_admin_users') }}" class="action-card"> <a href="{{ url_for('users.system_admin_users') }}" class="action-card">
<div class="action-icon"><i class="ti ti-users"></i></div> <div class="action-icon"><i class="ti ti-users"></i></div>
@@ -176,7 +211,7 @@
</a> </a>
<a href="{{ url_for('system_admin.system_admin_companies') }}" class="action-card"> <a href="{{ url_for('system_admin.system_admin_companies') }}" class="action-card">
<div class="action-icon">🏢</div> <div class="action-icon"><i class="ti ti-building"></i></div>
<div class="action-content"> <div class="action-content">
<h4>Manage Companies</h4> <h4>Manage Companies</h4>
<p>View and manage all companies in the system</p> <p>View and manage all companies in the system</p>
@@ -184,7 +219,7 @@
</a> </a>
<a href="{{ url_for('system_admin.system_admin_time_entries') }}" class="action-card"> <a href="{{ url_for('system_admin.system_admin_time_entries') }}" class="action-card">
<div class="action-icon">⏱️</div> <div class="action-icon"><i class="ti ti-clock"></i></div>
<div class="action-content"> <div class="action-content">
<h4>View Time Entries</h4> <h4>View Time Entries</h4>
<p>Browse time tracking data across all companies</p> <p>Browse time tracking data across all companies</p>
@@ -193,68 +228,136 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<style> <style>
.header-section { /* Container */
margin-bottom: 2rem; .settings-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
} }
.subtitle { /* Page Header */
color: #6c757d; .page-header {
margin-bottom: 1rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
} border-radius: 16px;
.stats-section {
background: #f8f9fa;
border-radius: 8px;
padding: 2rem; padding: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
} }
.stats-section h3 { .header-content {
margin-top: 0; display: flex;
margin-bottom: 1.5rem; justify-content: space-between;
color: #495057; align-items: center;
flex-wrap: wrap;
gap: 2rem;
} }
.stats-grid { .page-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-icon {
font-size: 2.5rem;
display: inline-block;
animation: rotate 8s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
/* Stats Section */
.stats-section {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; gap: 1.5rem;
margin-bottom: 2rem;
} }
.stat-card { .stat-card {
background: white; background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1.5rem; padding: 1.5rem;
border-radius: 12px;
text-align: center; text-align: center;
border: 1px solid #e5e7eb;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
} }
.stat-card h4 { .stat-card:hover {
font-size: 2rem; transform: translateY(-2px);
margin: 0 0 0.5rem 0; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
color: #007bff;
} }
.stat-card p { .stat-value {
margin: 0; font-size: 2.5rem;
color: #6c757d; font-weight: 700;
font-weight: 500; margin-bottom: 0.5rem;
color: #667eea;
} }
.settings-section { .stat-label {
font-size: 0.9rem;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
/* Cards */
.card {
background: white; background: white;
border: 1px solid #dee2e6; border-radius: 12px;
border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
padding: 2rem; border: 1px solid #e5e7eb;
margin-bottom: 2rem; margin-bottom: 1.5rem;
overflow: hidden;
transition: all 0.3s ease;
} }
.settings-section h3 { .card:hover {
margin-top: 0; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
margin-bottom: 1.5rem; transform: translateY(-2px);
color: #495057; }
.card-header {
background: #f8f9fa;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
color: #1f2937;
display: flex;
align-items: center;
gap: 0.5rem;
}
.card-title .icon {
font-size: 1.5rem;
}
.card-body {
padding: 1.5rem;
} }
.settings-form { .settings-form {
@@ -268,8 +371,9 @@
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 2rem; gap: 2rem;
padding: 1.5rem; padding: 1.5rem;
border: 1px solid #e9ecef; border: 1px solid #e5e7eb;
border-radius: 8px; border-radius: 8px;
background: #f8f9fa;
} }
.setting-group.full-width { .setting-group.full-width {
@@ -295,7 +399,8 @@
.setting-header h4 { .setting-header h4 {
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
color: #495057; color: #1f2937;
font-weight: 600;
} }
.setting-header p { .setting-header p {
@@ -320,7 +425,7 @@
position: relative; position: relative;
width: 50px; width: 50px;
height: 24px; height: 24px;
background: #ccc; background: #e5e7eb;
border-radius: 24px; border-radius: 24px;
transition: background 0.3s; transition: background 0.3s;
} }
@@ -335,10 +440,11 @@
top: 2px; top: 2px;
left: 2px; left: 2px;
transition: transform 0.3s; transition: transform 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
} }
.toggle-label input[type="checkbox"]:checked + .toggle-slider { .toggle-label input[type="checkbox"]:checked + .toggle-slider {
background: #007bff; background: #667eea;
} }
.toggle-label input[type="checkbox"]:checked + .toggle-slider::before { .toggle-label input[type="checkbox"]:checked + .toggle-slider::before {
@@ -347,7 +453,7 @@
.toggle-text { .toggle-text {
font-weight: 500; font-weight: 500;
color: #495057; color: #1f2937;
} }
.setting-description { .setting-description {
@@ -359,21 +465,9 @@
.form-actions { .form-actions {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
padding-top: 1rem; padding-top: 1.5rem;
border-top: 1px solid #e9ecef; border-top: 1px solid #e5e7eb;
} margin-top: 2rem;
.info-section {
background: #f8f9fa;
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
}
.info-section h3 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #495057;
} }
.info-grid { .info-grid {
@@ -383,41 +477,62 @@
} }
.info-card { .info-card {
background: white; background: #f8f9fa;
border: 1px solid #dee2e6; border: 1px solid #e5e7eb;
border-radius: 8px; border-radius: 8px;
padding: 1.5rem; padding: 1.5rem;
} }
.info-card h4 { .info-card h4 {
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
color: #495057; color: #374151;
font-size: 1rem; font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
} }
.info-card p { .info-card p {
margin: 0; margin: 0;
color: #6c757d; color: #1f2937;
font-size: 1.1rem;
font-weight: 500;
} }
.danger-section { /* Danger Zone */
background: #f8d7da; .danger-zone {
border: 2px solid #dc3545; margin-top: 3rem;
border-radius: 8px; background: #fef2f2;
padding: 2rem; border: 2px solid #fecaca;
margin-bottom: 2rem; border-radius: 12px;
overflow: hidden;
} }
.danger-section h3 { .danger-header {
margin-top: 0; background: #fee2e2;
margin-bottom: 1.5rem; padding: 1.5rem;
color: #721c24; border-bottom: 1px solid #fecaca;
}
.danger-title {
font-size: 1.25rem;
font-weight: 600;
color: #991b1b;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.danger-title i {
font-size: 1.5rem;
} }
.danger-content { .danger-content {
padding: 2rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1.5rem;
} }
.danger-item { .danger-item {
@@ -427,82 +542,145 @@
background: white; background: white;
padding: 1.5rem; padding: 1.5rem;
border-radius: 8px; border-radius: 8px;
border: 1px solid #dc3545; border: 1px solid #fecaca;
gap: 2rem;
} }
.danger-info h4 { .danger-info h4 {
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
color: #721c24; color: #991b1b;
font-weight: 600;
} }
.danger-info p { .danger-info p {
margin: 0; margin: 0;
color: #721c24; color: #7f1d1d;
font-size: 0.9rem; font-size: 0.9rem;
} line-height: 1.5;
.actions-section {
background: #f8f9fa;
border-radius: 8px;
padding: 2rem;
}
.actions-section h3 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #495057;
} }
.actions-grid { .actions-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem; gap: 1.5rem;
} }
.action-card { .action-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1.5rem;
display: flex; display: flex;
align-items: center; gap: 1.5rem;
gap: 1rem; padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
text-decoration: none; text-decoration: none;
color: inherit; transition: all 0.2s ease;
transition: all 0.2s; border: 2px solid transparent;
align-items: center;
} }
.action-card:hover { .action-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1); background: white;
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
text-decoration: none; text-decoration: none;
color: inherit;
} }
.action-icon { .action-icon {
font-size: 2rem; font-size: 2.5rem;
width: 60px; flex-shrink: 0;
height: 60px; }
display: flex;
align-items: center; .action-icon i {
justify-content: center; font-size: 2.5rem;
background: #f8f9fa; color: #667eea;
border-radius: 8px;
} }
.action-content h4 { .action-content h4 {
margin: 0 0 0.5rem 0; margin: 0 0 0.25rem 0;
color: #495057; color: #1f2937;
font-weight: 600;
font-size: 1.1rem;
} }
.action-content p { .action-content p {
margin: 0; margin: 0;
color: #6c757d; color: #6b7280;
font-size: 0.875rem; font-size: 0.9rem;
} }
/* Button styles now centralized in main style.css */ /* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
.btn-danger {
background: #dc2626;
color: white;
}
.btn-danger:hover {
background: #b91c1c;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
.btn-warning {
background: #f59e0b;
color: white;
}
.btn-warning:hover {
background: #d97706;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
}
/* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.settings-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.setting-group { .setting-group {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -513,5 +691,26 @@
gap: 1rem; gap: 1rem;
} }
} }
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -1,16 +1,35 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="container"> <div class="time-entries-container">
<div class="header-section"> <!-- Header Section -->
<h1>⏱️ System Admin - Time Entries</h1> <div class="page-header">
<p class="subtitle">View time entries across all companies</p> <div class="header-content">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary"><i class="ti ti-arrow-left"></i> Back to Dashboard</a> <div class="header-left">
<h1 class="page-title">
<span class="page-icon"><i class="ti ti-clock"></i></span>
System Admin - Time Entries
</h1>
<p class="page-subtitle">View time entries across all companies</p>
</div>
<div class="header-actions">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">
<i class="ti ti-arrow-left"></i>
Back to Dashboard
</a>
</div>
</div>
</div> </div>
<!-- Filter Section --> <!-- Filter Section -->
<div class="filter-section"> <div class="card">
<h3>Filter Time Entries</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-filter"></i></span>
Filter Time Entries
</h2>
</div>
<div class="card-body">
<form method="GET" class="filter-form"> <form method="GET" class="filter-form">
<div class="filter-group"> <div class="filter-group">
<label for="company">Company:</label> <label for="company">Company:</label>
@@ -29,10 +48,12 @@
{% endif %} {% endif %}
</form> </form>
</div> </div>
</div>
<!-- Time Entries Table --> <!-- Time Entries Table -->
{% if entries.items %} {% if entries.items %}
<div class="table-section"> <div class="card">
<div class="card-body no-padding">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@@ -106,6 +127,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
<!-- Pagination --> <!-- Pagination -->
{% if entries.pages > 1 %} {% if entries.pages > 1 %}
@@ -152,55 +174,127 @@
<!-- Summary Statistics --> <!-- Summary Statistics -->
{% if entries.items %} {% if entries.items %}
<div class="summary-section"> <div class="stats-section">
<h3>📊 Summary Statistics</h3> <div class="stat-card">
<div class="summary-grid"> <div class="stat-value">{{ entries.total }}</div>
<div class="summary-card"> <div class="stat-label">Total Entries</div>
<h4>Total Entries</h4>
<p class="summary-number">{{ entries.total }}</p>
</div> </div>
<div class="summary-card"> <div class="stat-card">
<h4>Active Sessions</h4> <div class="stat-value">{{ entries.items | selectattr('0.departure_time', 'equalto', None) | list | length }}</div>
<p class="summary-number">{{ entries.items | selectattr('0.departure_time', 'equalto', None) | list | length }}</p> <div class="stat-label">Active Sessions</div>
</div> </div>
<div class="summary-card"> <div class="stat-card">
<h4>Paused Sessions</h4> <div class="stat-value">{{ entries.items | selectattr('0.is_paused', 'equalto', True) | list | length }}</div>
<p class="summary-number">{{ entries.items | selectattr('0.is_paused', 'equalto', True) | list | length }}</p> <div class="stat-label">Paused Sessions</div>
</div> </div>
<div class="summary-card"> <div class="stat-card">
<h4>Completed Today</h4> <div class="stat-value">
<p class="summary-number">
{{ entries.items | selectattr('0.arrival_time') | selectattr('0.departure_time', 'defined') | {{ entries.items | selectattr('0.arrival_time') | selectattr('0.departure_time', 'defined') |
list | length }} list | length }}
</p>
</div> </div>
<div class="stat-label">Completed Today</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<style> <style>
.header-section { /* Container */
.time-entries-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Page Header */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
} }
.subtitle { .header-content {
color: #6c757d; display: flex;
margin-bottom: 1rem; justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 2rem;
} }
.filter-section { .page-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-icon {
font-size: 2.5rem;
display: inline-block;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
/* Cards */
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
margin-bottom: 1.5rem;
overflow: hidden;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card-header {
background: #f8f9fa; background: #f8f9fa;
padding: 1.5rem; padding: 1.5rem;
border-radius: 8px; border-bottom: 1px solid #e5e7eb;
margin-bottom: 2rem;
} }
.filter-section h3 { .card-title {
margin-top: 0; font-size: 1.25rem;
margin-bottom: 1rem; font-weight: 600;
margin: 0;
color: #1f2937;
display: flex;
align-items: center;
gap: 0.5rem;
} }
.card-title .icon {
font-size: 1.5rem;
}
.card-body {
padding: 1.5rem;
}
.card-body.no-padding {
padding: 0;
}
/* Filter Form */
.filter-form { .filter-form {
display: flex; display: flex;
align-items: end; align-items: end;
@@ -215,24 +309,24 @@
} }
.filter-group label { .filter-group label {
font-weight: 500; font-weight: 600;
color: #495057; color: #374151;
font-size: 0.875rem;
} }
.form-control { .form-control {
padding: 0.5rem; padding: 0.625rem 1rem;
border: 1px solid #ced4da; border: 1px solid #e5e7eb;
border-radius: 4px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
min-width: 200px; min-width: 250px;
transition: all 0.2s ease;
} }
.table-section { .form-control:focus {
background: white; outline: none;
border-radius: 8px; border-color: #667eea;
overflow: hidden; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
margin-bottom: 2rem;
} }
.table { .table {
@@ -252,7 +346,10 @@
.table th { .table th {
background: #f8f9fa; background: #f8f9fa;
font-weight: 600; font-weight: 600;
color: #495057; color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.8rem;
} }
.paused-entry { .paused-entry {
@@ -260,18 +357,18 @@
} }
.company-name { .company-name {
font-weight: 500; font-weight: 600;
color: #495057; color: #1f2937;
} }
.project-name { .project-name {
color: #007bff; color: #667eea;
font-weight: 500; font-weight: 600;
} }
.duration { .duration {
font-weight: 600; font-weight: 700;
color: #28a745; color: #10b981;
} }
.notes { .notes {
@@ -284,25 +381,26 @@
} }
.status-badge { .status-badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.75rem;
border-radius: 4px; border-radius: 20px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 600;
text-transform: uppercase;
} }
.status-active { .status-active {
background: #d4edda; background: #d1fae5;
color: #155724; color: #065f46;
} }
.status-paused { .status-paused {
background: #fff3cd; background: #fef3c7;
color: #856404; color: #92400e;
} }
.status-completed { .status-completed {
background: #d1ecf1; background: #dbeafe;
color: #0c5460; color: #1e40af;
} }
.pagination-section { .pagination-section {
@@ -320,21 +418,27 @@
} }
.page-link { .page-link {
padding: 0.5rem 0.75rem; padding: 0.5rem 1rem;
border: 1px solid #dee2e6; border: 1px solid #e5e7eb;
color: #007bff; color: #667eea;
text-decoration: none; text-decoration: none;
border-radius: 4px; border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.25rem;
} }
.page-link:hover { .page-link:hover {
background: #e9ecef; background: #f3f4f6;
border-color: #667eea;
} }
.page-link.current { .page-link.current {
background: #007bff; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border-color: #007bff; border-color: transparent;
} }
.pagination-info { .pagination-info {
@@ -349,57 +453,130 @@
color: #6c757d; color: #6c757d;
} }
.summary-section { /* Stats Section */
background: #f8f9fa; .stats-section {
border-radius: 8px;
padding: 2rem;
}
.summary-section h3 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #495057;
}
.summary-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; gap: 1.5rem;
margin-top: 2rem;
} }
.summary-card { .stat-card {
background: white; background: white;
border-radius: 8px;
padding: 1.5rem; padding: 1.5rem;
border-radius: 12px;
text-align: center; text-align: center;
border: 1px solid #dee2e6; border: 1px solid #e5e7eb;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
} }
.summary-card h4 { .stat-card:hover {
margin: 0 0 0.5rem 0; transform: translateY(-2px);
color: #6c757d; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
font-size: 0.875rem;
font-weight: 500;
} }
.summary-number { .stat-value {
font-size: 2rem; font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: #667eea;
}
.stat-label {
font-size: 0.9rem;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600; font-weight: 600;
color: #007bff;
margin: 0;
} }
/* Button styles now centralized in main style.css */ /* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
.btn-sm {
font-size: 0.875rem;
padding: 0.5rem 1rem;
}
.btn-outline { .btn-outline {
background: transparent; background: transparent;
color: #007bff; color: #667eea;
border: 1px solid #007bff; border: 2px solid #667eea;
} }
.btn-outline:hover { .btn-outline:hover {
background: #007bff; background: #667eea;
color: white; color: white;
} }
/* Responsive Design */
@media (max-width: 768px) {
.time-entries-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.table {
font-size: 0.8rem;
}
.table th,
.table td {
padding: 0.5rem;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -1,16 +1,35 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="container"> <div class="users-admin-container">
<div class="header-section"> <!-- Header Section -->
<h1><i class="ti ti-users"></i> System Admin - All Users</h1> <div class="page-header">
<p class="subtitle">Manage users across all companies</p> <div class="header-content">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-md btn-secondary"><i class="ti ti-arrow-left"></i> Back to Dashboard</a> <div class="header-left">
<h1 class="page-title">
<span class="page-icon"><i class="ti ti-users"></i></span>
System Admin - All Users
</h1>
<p class="page-subtitle">Manage users across all companies</p>
</div>
<div class="header-actions">
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">
<i class="ti ti-arrow-left"></i>
Back to Dashboard
</a>
</div>
</div>
</div> </div>
<!-- Filter Options --> <!-- Filter Options -->
<div class="filter-section"> <div class="card">
<h3>Filter Users</h3> <div class="card-header">
<h2 class="card-title">
<span class="icon"><i class="ti ti-filter"></i></span>
Filter Users
</h2>
</div>
<div class="card-body">
<div class="filter-buttons"> <div class="filter-buttons">
<a href="{{ url_for('users.system_admin_users') }}" <a href="{{ url_for('users.system_admin_users') }}"
class="btn btn-filter {% if not current_filter %}active{% endif %}"> class="btn btn-filter {% if not current_filter %}active{% endif %}">
@@ -38,10 +57,12 @@
</a> </a>
</div> </div>
</div> </div>
</div>
<!-- Users Table --> <!-- Users Table -->
{% if users.items %} {% if users.items %}
<div class="table-section"> <div class="card">
<div class="card-body no-padding">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@@ -118,6 +139,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
<!-- Pagination --> <!-- Pagination -->
{% if users.pages > 1 %} {% if users.pages > 1 %}
@@ -160,60 +182,132 @@
</div> </div>
<style> <style>
.header-section { /* Container */
.users-admin-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Page Header */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
} }
.subtitle { .header-content {
color: #6c757d; display: flex;
margin-bottom: 1rem; justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 2rem;
} }
.filter-section { .page-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.page-icon {
font-size: 2.5rem;
display: inline-block;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.5rem 0 0 0;
}
/* Cards */
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
margin-bottom: 1.5rem;
overflow: hidden;
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.card-header {
background: #f8f9fa; background: #f8f9fa;
padding: 1.5rem; padding: 1.5rem;
border-radius: 8px; border-bottom: 1px solid #e5e7eb;
margin-bottom: 2rem;
} }
.filter-section h3 { .card-title {
margin-top: 0; font-size: 1.25rem;
margin-bottom: 1rem; font-weight: 600;
margin: 0;
color: #1f2937;
display: flex;
align-items: center;
gap: 0.5rem;
} }
.card-title .icon {
font-size: 1.5rem;
}
.card-body {
padding: 1.5rem;
}
.card-body.no-padding {
padding: 0;
}
/* Filter Buttons */
.filter-buttons { .filter-buttons {
display: flex; display: flex;
gap: 0.5rem; gap: 0.75rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
.btn-filter { .btn-filter {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border: 1px solid #dee2e6; border: 1px solid #e5e7eb;
background: white; background: white;
color: #495057; color: #6b7280;
text-decoration: none; text-decoration: none;
border-radius: 4px; border-radius: 8px;
transition: all 0.2s; font-weight: 500;
transition: all 0.2s ease;
} }
.btn-filter:hover { .btn-filter:hover {
background: #e9ecef; background: #f3f4f6;
border-color: #667eea;
color: #667eea;
} }
.btn-filter.active { .btn-filter.active {
background: #007bff; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border-color: #007bff; border-color: transparent;
}
.table-section {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
} }
/* Table */
.table { .table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -224,120 +318,127 @@
.table td { .table td {
padding: 1rem; padding: 1rem;
text-align: left; text-align: left;
border-bottom: 1px solid #dee2e6; border-bottom: 1px solid #e5e7eb;
} }
.table th { .table th {
background: #f8f9fa; background: #f8f9fa;
font-weight: 600; font-weight: 600;
color: #495057; color: #374151;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.8rem;
}
.table tr:hover {
background: #f8f9fa;
} }
.blocked-user { .blocked-user {
background-color: #f8d7da !important; background-color: #fef2f2 !important;
} }
.company-name {
font-weight: 600;
color: #1f2937;
}
/* Badges */
.badge { .badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.75rem;
border-radius: 4px; border-radius: 20px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 600;
text-transform: uppercase;
} }
.badge-self { .badge-self {
background: #d1ecf1; background: #dbeafe;
color: #0c5460; color: #1e40af;
} }
.badge-personal { .badge-personal {
background: #fff3cd; background: #fef3c7;
color: #856404; color: #92400e;
} }
.badge-company { .badge-company {
background: #d1ecf1; background: #dbeafe;
color: #0c5460; color: #1e40af;
} }
.badge-freelancer { .badge-freelancer {
background: #d4edda; background: #d1fae5;
color: #155724; color: #065f46;
} }
/* Role Badges */
.role-badge { .role-badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.75rem;
border-radius: 4px; border-radius: 20px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 600;
text-transform: uppercase;
} }
.role-team_member { .role-team_member {
background: #e2e3e5; background: #e5e7eb;
color: #495057; color: #374151;
} }
.role-team_leader { .role-team_leader {
background: #d4edda; background: #d1fae5;
color: #155724; color: #065f46;
} }
.role-supervisor { .role-supervisor {
background: #d1ecf1; background: #dbeafe;
color: #0c5460; color: #1e40af;
} }
.role-admin { .role-admin {
background: #fff3cd; background: #fef3c7;
color: #856404; color: #92400e;
} }
.role-system_admin { .role-system_admin {
background: #f1c0e8; background: #ede9fe;
color: #6a1b99; color: #5b21b6;
} }
/* Status Badges */
.status-badge { .status-badge {
padding: 0.25rem 0.5rem; padding: 0.25rem 0.75rem;
border-radius: 4px; border-radius: 20px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 600;
text-transform: uppercase;
} }
.status-active { .status-active {
background: #d4edda; background: #d1fae5;
color: #155724; color: #065f46;
} }
.status-blocked { .status-blocked {
background: #f8d7da; background: #fee2e2;
color: #721c24; color: #991b1b;
} }
.status-unverified { .status-unverified {
background: #fff3cd; background: #fef3c7;
color: #856404; color: #92400e;
} }
/* Action Buttons */
.action-buttons { .action-buttons {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
} }
.btn { /* Pagination */
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;
}
/* Button styles now centralized in main style.css */
.pagination-section { .pagination-section {
margin-top: 2rem; margin: 2rem 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -351,37 +452,156 @@
} }
.page-link { .page-link {
padding: 0.5rem 0.75rem; padding: 0.5rem 1rem;
border: 1px solid #dee2e6; border: 1px solid #e5e7eb;
color: #007bff; color: #667eea;
text-decoration: none; text-decoration: none;
border-radius: 4px; border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.25rem;
} }
.page-link:hover { .page-link:hover {
background: #e9ecef; background: #f3f4f6;
border-color: #667eea;
} }
.page-link.current { .page-link.current {
background: #007bff; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border-color: #007bff; border-color: transparent;
} }
.pagination-info { .pagination-info {
color: #6c757d; color: #6b7280;
margin: 0; margin: 0;
font-size: 0.9rem; font-size: 0.9rem;
} }
/* Empty State */
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 3rem; padding: 4rem 2rem;
color: #6c757d; background: white;
border-radius: 12px;
border: 1px solid #e5e7eb;
} }
.company-name { .empty-state h3 {
font-weight: 500; font-size: 1.5rem;
color: #1f2937;
margin-bottom: 0.5rem;
} }
.empty-state p {
color: #6b7280;
font-size: 1.1rem;
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
.btn-danger {
background: #dc2626;
color: white;
}
.btn-danger:hover {
background: #b91c1c;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
.btn-sm {
font-size: 0.875rem;
padding: 0.5rem 1rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.users-admin-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.table {
font-size: 0.8rem;
}
.table th,
.table td {
padding: 0.5rem;
}
.action-buttons {
flex-direction: column;
gap: 0.25rem;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
</style> </style>
{% endblock %} {% endblock %}