Squashed commit of the following:

commit 1eeea9f83ad9230a5c1f7a75662770eaab0df837
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 21:15:41 2025 +0200

    Disable resuming of old time entries.

commit 3e3ec2f01cb7943622b819a19179388078ae1315
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 20:59:19 2025 +0200

    Refactor db migrations.

commit 15a51a569da36c6b7c9e01ab17b6fdbdee6ad994
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 19:58:04 2025 +0200

    Apply new style for Time Tracking view.

commit 77e5278b303e060d2b03853b06277f8aa567ae68
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 18:06:04 2025 +0200

    Allow direct registrations as a Company.

commit 188a8772757cbef374243d3a5f29e4440ddecabe
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 18:04:45 2025 +0200

    Add email invitation feature.

commit d9ebaa02aa01b518960a20dccdd5a327d82f30c6
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 17:12:32 2025 +0200

    Apply common style for Company, User, Team management pages.

commit 81149caf4d8fc6317e2ab1b4f022b32fc5aa6d22
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 16:44:32 2025 +0200

    Move export functions to own module.

commit 1a26e19338e73f8849c671471dd15cc3c1b1fe82
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 15:51:15 2025 +0200

    Split up models.py.

commit 61f1ccd10f721b0ff4dc1eccf30c7a1ee13f204d
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 12:05:28 2025 +0200

    Move utility function into own modules.

commit 84b341ed35e2c5387819a8b9f9d41eca900ae79f
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 11:44:24 2025 +0200

    Refactor auth functions use.

commit 923e311e3da5b26d85845c2832b73b7b17c48adb
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 11:35:52 2025 +0200

    Refactor route nameing and fix bugs along the way.

commit f0a5c4419c340e62a2615c60b2a9de28204d2995
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 10:34:33 2025 +0200

    Fix URL endpoints in announcement template.

commit b74d74542a1c8dc350749e4788a9464d067a88b5
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 09:25:53 2025 +0200

    Move announcements to own module.

commit 9563a28021ac46c82c04fe4649b394dbf96f92c7
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 09:16:30 2025 +0200

    Combine Company view and edit templates.

commit 6687c373e681d54e4deab6b2582fed5cea9aadf6
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 08:17:42 2025 +0200

    Move Users, Company and System Administration to own modules.

commit 8b7894a2e3eb84bb059f546648b6b9536fea724e
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 07:40:57 2025 +0200

    Move Teams and Projects to own modules.

commit d11bf059d99839ecf1f5d7020b8c8c8a2454c00b
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 07:09:33 2025 +0200

    Move Tasks and Sprints to own modules.
This commit is contained in:
2025-07-07 21:16:36 +02:00
parent 4214e88d18
commit 9a79778ad6
116 changed files with 21063 additions and 5653 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,602 @@
{% extends 'layout.html' %}
{% block content %}
<div class="admin-container">
<div class="admin-header">
<h1>Team Management</h1>
<a href="{{ url_for('create_team') }}" class="btn btn-md btn-success">Create New Team</a>
<div class="teams-admin-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">
<span class="page-icon">👥</span>
Team Management
</h1>
<p class="page-subtitle">Manage teams and their members across your organization</p>
</div>
<div class="header-actions">
<a href="{{ url_for('teams.create_team') }}" class="btn btn-primary">
<span class="icon">+</span>
Create New Team
</a>
</div>
</div>
</div>
<!-- Team Statistics -->
{% if teams %}
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Members</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>{{ team.name }}</td>
<td>{{ team.description }}</td>
<td>{{ team.users|length }}</td>
<td>{{ team.created_at.strftime('%Y-%m-%d') }}</td>
<td class="actions">
<a href="{{ url_for('manage_team', team_id=team.id) }}" class="button btn btn-sm btn-primary">Manage</a>
<form method="POST" action="{{ url_for('delete_team', team_id=team.id) }}" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this team?');">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No teams found. Create a team to get started.</p>
<div class="stats-section">
<div class="stat-card">
<div class="stat-value">{{ teams|length if teams else 0 }}</div>
<div class="stat-label">Total Teams</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ teams|map(attribute='users')|map('length')|sum if teams else 0 }}</div>
<div class="stat-label">Total Members</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ (teams|map(attribute='users')|map('length')|sum / teams|length)|round(1) if teams else 0 }}</div>
<div class="stat-label">Avg Team Size</div>
</div>
</div>
{% endif %}
<!-- Main Content -->
<div class="teams-content">
{% if teams %}
<!-- Search Bar -->
<div class="search-section">
<div class="search-container">
<span class="search-icon">🔍</span>
<input type="text"
class="search-input"
id="teamSearch"
placeholder="Search teams by name or description...">
</div>
</div>
<!-- Teams Grid -->
<div class="teams-grid" id="teamsGrid">
{% for team in teams %}
<div class="team-card" data-team-name="{{ team.name.lower() }}" data-team-desc="{{ team.description.lower() if team.description else '' }}">
<div class="team-header">
<div class="team-icon-wrapper">
<span class="team-icon">👥</span>
</div>
<div class="team-meta">
<span class="member-count">{{ team.users|length }} members</span>
</div>
</div>
<div class="team-body">
<h3 class="team-name">{{ team.name }}</h3>
<p class="team-description">
{{ team.description if team.description else 'No description provided' }}
</p>
<div class="team-info">
<div class="info-item">
<span class="info-icon">📅</span>
<span class="info-text">Created {{ team.created_at.strftime('%b %d, %Y') }}</span>
</div>
{% if team.users %}
<div class="info-item">
<span class="info-icon">👤</span>
<span class="info-text">Led by {{ team.users[0].username }}</span>
</div>
{% endif %}
</div>
<!-- Member Avatars -->
{% if team.users %}
<div class="member-avatars">
{% for member in team.users[:5] %}
<div class="member-avatar" title="{{ member.username }}">
{{ member.username[:2].upper() }}
</div>
{% endfor %}
{% if team.users|length > 5 %}
<div class="member-avatar more" title="{{ team.users|length - 5 }} more members">
+{{ team.users|length - 5 }}
</div>
{% endif %}
</div>
{% endif %}
</div>
<div class="team-actions">
<a href="{{ url_for('teams.manage_team', team_id=team.id) }}" class="btn btn-manage">
<span class="icon">⚙️</span>
Manage Team
</a>
<form method="POST"
action="{{ url_for('teams.delete_team', team_id=team.id) }}"
class="delete-form"
onsubmit="return confirm('Are you sure you want to delete the team \"{{ team.name }}\"? This action cannot be undone.');">
<button type="submit" class="btn btn-delete" title="Delete Team">
<span class="icon">🗑️</span>
</button>
</form>
</div>
</div>
{% endfor %}
</div>
<!-- No Results Message -->
<div class="no-results" id="noResults" style="display: none;">
<div class="empty-icon">🔍</div>
<p class="empty-message">No teams found matching your search</p>
<p class="empty-hint">Try searching with different keywords</p>
</div>
{% else %}
<!-- Empty State -->
<div class="empty-state">
<div class="empty-icon">👥</div>
<h2 class="empty-title">No Teams Yet</h2>
<p class="empty-message">Create your first team to start organizing your workforce</p>
<a href="{{ url_for('teams.create_team') }}" class="btn btn-primary btn-lg">
<span class="icon">+</span>
Create First Team
</a>
</div>
{% endif %}
</div>
</div>
<style>
/* Container */
.teams-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;
}
/* Stats Section */
.stats-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 12px;
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: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;
}
/* Search Section */
.search-section {
margin-bottom: 2rem;
}
.search-container {
position: relative;
max-width: 500px;
}
.search-icon {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
font-size: 1.25rem;
opacity: 0.6;
}
.search-input {
width: 100%;
padding: 1rem 1rem 1rem 3rem;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 1rem;
transition: all 0.3s ease;
background: white;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Teams Grid */
.teams-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* Team Card */
.team-card {
background: white;
border-radius: 12px;
border: 1px solid #e5e7eb;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
overflow: hidden;
display: flex;
flex-direction: column;
}
.team-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.team-header {
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.team-icon-wrapper {
width: 60px;
height: 60px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.team-icon {
font-size: 2rem;
}
.member-count {
background: white;
color: #6b7280;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
}
.team-body {
padding: 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
}
.team-name {
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
margin: 0 0 0.5rem 0;
}
.team-description {
color: #6b7280;
line-height: 1.6;
margin-bottom: 1.5rem;
flex: 1;
}
.team-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.info-item {
display: flex;
align-items: center;
gap: 0.5rem;
color: #6b7280;
font-size: 0.875rem;
}
.info-icon {
font-size: 1rem;
}
/* Member Avatars */
.member-avatars {
display: flex;
margin-bottom: 1rem;
}
.member-avatar {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 600;
margin-right: -8px;
border: 2px solid white;
position: relative;
z-index: 1;
transition: all 0.2s ease;
}
.member-avatar:hover {
transform: scale(1.1);
z-index: 10;
}
.member-avatar.more {
background: #6b7280;
font-size: 0.75rem;
}
/* Team Actions */
.team-actions {
padding: 1.5rem;
background: #f8f9fa;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 0.75rem;
align-items: center;
}
.delete-form {
margin: 0;
}
/* 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-manage {
background: white;
color: #667eea;
border: 2px solid #e5e7eb;
flex: 1;
justify-content: center;
}
.btn-manage:hover {
background: #f3f4f6;
border-color: #667eea;
}
.btn-delete {
background: #fee2e2;
color: #dc2626;
padding: 0.75rem;
min-width: auto;
}
.btn-delete:hover {
background: #dc2626;
color: white;
}
.btn-lg {
padding: 1rem 2rem;
font-size: 1.1rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: white;
border-radius: 12px;
border: 2px dashed #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;
}
.empty-hint {
color: #9ca3af;
font-size: 0.875rem;
}
/* No Results */
.no-results {
text-align: center;
padding: 3rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.teams-admin-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.page-title {
font-size: 2rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.header-stats {
grid-template-columns: 1fr;
}
.teams-grid {
grid-template-columns: 1fr;
}
.team-actions {
flex-direction: column;
}
.btn-manage {
width: 100%;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.team-card {
animation: slideIn 0.4s ease-out;
animation-fill-mode: both;
}
.team-card:nth-child(1) { animation-delay: 0.05s; }
.team-card:nth-child(2) { animation-delay: 0.1s; }
.team-card:nth-child(3) { animation-delay: 0.15s; }
.team-card:nth-child(4) { animation-delay: 0.2s; }
.team-card:nth-child(5) { animation-delay: 0.25s; }
.team-card:nth-child(6) { animation-delay: 0.3s; }
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('teamSearch');
const teamsGrid = document.getElementById('teamsGrid');
const noResults = document.getElementById('noResults');
if (searchInput && teamsGrid) {
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase().trim();
const teamCards = teamsGrid.querySelectorAll('.team-card');
let visibleCount = 0;
teamCards.forEach(card => {
const teamName = card.getAttribute('data-team-name');
const teamDesc = card.getAttribute('data-team-desc');
if (teamName.includes(searchTerm) || teamDesc.includes(searchTerm)) {
card.style.display = '';
visibleCount++;
} else {
card.style.display = 'none';
}
});
// Show/hide no results message
if (noResults) {
noResults.style.display = visibleCount === 0 ? 'block' : 'none';
}
});
}
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -45,9 +45,9 @@
<div class="form-group">
<label for="work_hours_per_day">Standard Work Hours Per Day:</label>
<input type="number"
id="work_hours_per_day"
name="work_hours_per_day"
value="{{ work_config.work_hours_per_day }}"
id="standard_hours_per_day"
name="standard_hours_per_day"
value="{{ work_config.standard_hours_per_day }}"
min="1"
max="24"
step="0.5"
@@ -61,9 +61,9 @@
<div class="form-group">
<label for="mandatory_break_minutes">Mandatory Break Duration (minutes):</label>
<input type="number"
id="mandatory_break_minutes"
name="mandatory_break_minutes"
value="{{ work_config.mandatory_break_minutes }}"
id="break_duration_minutes"
name="break_duration_minutes"
value="{{ work_config.break_duration_minutes }}"
min="0"
max="240"
required>
@@ -73,9 +73,9 @@
<div class="form-group">
<label for="break_threshold_hours">Break Threshold (hours):</label>
<input type="number"
id="break_threshold_hours"
name="break_threshold_hours"
value="{{ work_config.break_threshold_hours }}"
id="break_after_hours"
name="break_after_hours"
value="{{ work_config.break_after_hours }}"
min="0"
max="24"
step="0.5"
@@ -116,11 +116,11 @@
<div class="current-config">
<h4>Current Configuration Summary</h4>
<div class="config-summary">
<strong>Region:</strong> {{ work_config.region_name }}<br>
<strong>Work Day:</strong> {{ work_config.work_hours_per_day }} hours<br>
<strong>Region:</strong> {{ work_config.work_region.value }}<br>
<strong>Work Day:</strong> {{ work_config.standard_hours_per_day }} hours<br>
<strong>Break Policy:</strong>
{% if work_config.mandatory_break_minutes > 0 %}
{{ work_config.mandatory_break_minutes }} minutes after {{ work_config.break_threshold_hours }} hours
{{ work_config.break_duration_minutes }} minutes after {{ work_config.break_after_hours }} hours
{% else %}
No mandatory breaks
{% endif %}
@@ -135,7 +135,7 @@
</div>
<button type="submit" class="btn btn-primary">Save Custom Configuration</button>
<a href="{{ url_for('admin_company') }}" class="btn btn-secondary">Back to Company Settings</a>
<a href="{{ url_for('companies.admin_company') }}" class="btn btn-secondary">Back to Company Settings</a>
</form>
</div>
</div>

View File

@@ -120,16 +120,16 @@
</div>
<div class="chart-stats">
<div class="stat-card">
<h4>Total Hours</h4>
<span id="total-hours">0</span>
<h4 id="stat-label-1">Total Hours</h4>
</div>
<div class="stat-card">
<h4>Total Days</h4>
<span id="total-days">0</span>
<h4 id="stat-label-2">Total Days</h4>
</div>
<div class="stat-card">
<h4>Average Hours/Day</h4>
<span id="avg-hours">0</span>
<h4 id="stat-label-3">Average Hours/Day</h4>
</div>
</div>
</div>
@@ -417,9 +417,9 @@ class TimeAnalyticsController {
document.getElementById('avg-hours').textContent = data.burndown.tasks_completed || '0';
// Update stat labels for burndown
document.querySelector('.stat-card:nth-child(1) h4').textContent = 'Total Tasks';
document.querySelector('.stat-card:nth-child(2) h4').textContent = 'Timeline Days';
document.querySelector('.stat-card:nth-child(3) h4').textContent = 'Completed Tasks';
document.getElementById('stat-label-1').textContent = 'Total Tasks';
document.getElementById('stat-label-2').textContent = 'Timeline Days';
document.getElementById('stat-label-3').textContent = 'Completed Tasks';
} else {
document.getElementById('total-hours').textContent = data.totalHours?.toFixed(1) || '0';
document.getElementById('total-days').textContent = data.totalDays || '0';
@@ -427,9 +427,9 @@ class TimeAnalyticsController {
data.totalDays > 0 ? (data.totalHours / data.totalDays).toFixed(1) : '0';
// Restore original stat labels
document.querySelector('.stat-card:nth-child(1) h4').textContent = 'Total Hours';
document.querySelector('.stat-card:nth-child(2) h4').textContent = 'Total Days';
document.querySelector('.stat-card:nth-child(3) h4').textContent = 'Average Hours/Day';
document.getElementById('stat-label-1').textContent = 'Total Hours';
document.getElementById('stat-label-2').textContent = 'Total Days';
document.getElementById('stat-label-3').textContent = 'Average Hours/Day';
}
this.updateChart();

View File

@@ -4,7 +4,7 @@
<div class="admin-container">
<div class="admin-header">
<h1>Company Users - {{ company.name }}</h1>
<a href="{{ url_for('create_user') }}" class="btn btn-success">Create New User</a>
<a href="{{ url_for('users.create_user') }}" class="btn btn-success">Create New User</a>
</div>
<!-- User Statistics -->
@@ -84,13 +84,15 @@
</td>
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-sm btn-primary">Edit</a>
<a href="{{ url_for('users.edit_user', user_id=user.id) }}" class="btn btn-sm btn-primary">Edit</a>
{% if user.id != g.user.id %}
{% if user.is_blocked %}
<a href="{{ url_for('toggle_user_status', user_id=user.id) }}" class="btn btn-sm btn-success">Unblock</a>
{% else %}
<a href="{{ url_for('toggle_user_status', user_id=user.id) }}" class="btn btn-sm btn-warning">Block</a>
{% endif %}
<form method="POST" action="{{ url_for('users.toggle_user_status', user_id=user.id) }}" style="display: inline;">
{% if user.is_blocked %}
<button type="submit" class="btn btn-sm btn-success">Unblock</button>
{% else %}
<button type="submit" class="btn btn-sm btn-warning">Block</button>
{% endif %}
</form>
<button class="btn btn-sm btn-danger" onclick="confirmDelete({{ user.id }}, '{{ user.username }}')">Delete</button>
{% endif %}
</td>
@@ -103,14 +105,14 @@
<div class="empty-state">
<h3>No Users Found</h3>
<p>There are no users in this company yet.</p>
<a href="{{ url_for('create_user') }}" class="btn btn-primary">Add First User</a>
<a href="{{ url_for('users.create_user') }}" class="btn btn-primary">Add First User</a>
</div>
{% endif %}
</div>
<!-- Navigation -->
<div class="admin-section">
<a href="{{ url_for('admin_company') }}" class="btn btn-secondary">← Back to Company Management</a>
<a href="{{ url_for('companies.admin_company') }}" class="btn btn-secondary">← Back to Company Management</a>
</div>
</div>

View File

@@ -18,15 +18,15 @@
<div class="policy-info">
<div class="policy-item">
<strong>Region:</strong> {{ company_config.region_name }}
<strong>Region:</strong> {{ company_config.work_region.value }}
</div>
<div class="policy-item">
<strong>Standard Work Day:</strong> {{ company_config.work_hours_per_day }} hours
<strong>Standard Work Day:</strong> {{ company_config.standard_hours_per_day }} hours
</div>
<div class="policy-item">
<strong>Break Policy:</strong>
{% if company_config.mandatory_break_minutes > 0 %}
{{ company_config.mandatory_break_minutes }} minutes after {{ company_config.break_threshold_hours }} hours
{{ company_config.break_duration_minutes }} minutes after {{ company_config.break_after_hours }} hours
{% else %}
No mandatory breaks
{% endif %}

View File

@@ -5,7 +5,7 @@
<div class="header-section">
<h1>⚠️ Confirm Company Deletion</h1>
<p class="subtitle">Critical Action Required - Review All Data Before Proceeding</p>
<a href="{{ url_for('admin_users') if g.user.role != Role.SYSTEM_ADMIN else url_for('system_admin_users') }}"
<a href="{{ url_for('users.admin_users') if g.user.role != Role.SYSTEM_ADMIN else url_for('users.system_admin_users') }}"
class="btn btn-md btn-secondary">← Back to User Management</a>
</div>
@@ -231,7 +231,7 @@
</div>
<div class="form-group">
<a href="{{ url_for('admin_users') if g.user.role != Role.SYSTEM_ADMIN else url_for('system_admin_users') }}"
<a href="{{ url_for('users.admin_users') if g.user.role != Role.SYSTEM_ADMIN else url_for('users.system_admin_users') }}"
class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-danger">
Delete Company and All Data

View File

@@ -4,7 +4,7 @@
<div class="timetrack-container">
<h2>Create New Project</h2>
<form method="POST" action="{{ url_for('create_project') }}" class="project-form">
<form method="POST" action="{{ url_for('projects.create_project') }}" class="project-form">
<div class="form-row">
<div class="form-group">
<label for="name">Project Name *</label>
@@ -71,7 +71,7 @@
<div class="form-actions">
<button type="submit" class="btn">Create Project</button>
<a href="{{ url_for('admin_projects') }}" class="btn btn-secondary">Cancel</a>
<a href="{{ url_for('projects.admin_projects') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>

View File

@@ -1,24 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<h1>Create New Team</h1>
<form method="POST" action="{{ url_for('create_team') }}" class="team-form">
<div class="form-group">
<label for="name">Team Name</label>
<input type="text" id="name" name="name" class="form-control" required>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" class="form-control" rows="3"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Create Team</button>
<a href="{{ url_for('admin_teams') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
{% endblock %}

View File

@@ -4,7 +4,7 @@
<div class="admin-container">
<h1>Create New User</h1>
<form method="POST" action="{{ url_for('create_user') }}" class="user-form">
<form method="POST" action="{{ url_for('users.create_user') }}" class="user-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" required autofocus>
@@ -48,7 +48,7 @@
<div class="form-group">
<button type="submit" class="btn btn-success">Create User</button>
<a href="{{ url_for('admin_users') }}" class="btn btn-secondary">Cancel</a>
<a href="{{ url_for('users.admin_users') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>

View File

@@ -918,6 +918,7 @@ function loadDashboard() {
}
function renderDashboard() {
console.log('Rendering dashboard with widgets:', widgets);
const grid = document.getElementById('dashboard-grid');
const emptyMessage = document.getElementById('empty-dashboard');
@@ -930,6 +931,10 @@ function renderDashboard() {
grid.style.display = 'grid';
emptyMessage.style.display = 'none';
// Clear timer intervals before clearing widgets
Object.values(timerIntervals).forEach(interval => clearInterval(interval));
timerIntervals = {};
// Clear existing widgets
grid.innerHTML = '';
@@ -939,8 +944,11 @@ function renderDashboard() {
return a.grid_x - b.grid_x;
});
console.log('Sorted widgets:', widgets);
// Render each widget
widgets.forEach(widget => {
console.log('Creating widget element for:', widget);
const widgetElement = createWidgetElement(widget);
grid.appendChild(widgetElement);
});
@@ -949,6 +957,9 @@ function renderDashboard() {
if (isCustomizing) {
initializeDragAndDrop();
}
// Reset global timer state to force refresh
globalTimerState = null;
}
function createWidgetElement(widget) {
@@ -1397,6 +1408,7 @@ function configureWidget(widgetId) {
}
function removeWidget(widgetId) {
console.log('Removing widget with ID:', widgetId);
if (!confirm('Are you sure you want to remove this widget?')) return;
fetch(`/api/dashboard/widgets/${widgetId}`, {
@@ -1404,6 +1416,7 @@ function removeWidget(widgetId) {
})
.then(response => response.json())
.then(data => {
console.log('Remove widget response:', data);
if (data.success) {
loadDashboard();
} else {

View File

@@ -1,73 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<h1>Edit Company</h1>
<form method="POST" class="user-form">
<div class="form-group">
<label for="name">Company Name</label>
<input type="text" id="name" name="name" class="form-control"
value="{{ company.name }}" required autofocus>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" class="form-control"
rows="3">{{ company.description or '' }}</textarea>
<small class="form-help">Optional description of your company</small>
</div>
<div class="form-group">
<label for="max_users">Maximum Users</label>
<input type="number" id="max_users" name="max_users" class="form-control"
value="{{ company.max_users or '' }}" min="1">
<small class="form-help">Leave empty for unlimited users</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="is_active" name="is_active"
{{ 'checked' if company.is_active else '' }}>
Company is active
</label>
<small class="form-help">Inactive companies cannot be accessed by users</small>
</div>
<div class="info-box">
<h3>Company Code</h3>
<p><strong>{{ company.slug }}</strong></p>
<p>This code cannot be changed and is used by new users to register for your company.</p>
</div>
<div class="form-actions">
<a href="{{ url_for('admin_company') }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
<style>
.info-box {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.info-box h3 {
margin-top: 0;
color: #0066cc;
}
.info-box p {
margin-bottom: 10px;
}
.info-box p:last-child {
margin-bottom: 0;
color: #666;
}
</style>
{% endblock %}

View File

@@ -4,7 +4,7 @@
<div class="timetrack-container">
<h2>Edit Project: {{ project.name }}</h2>
<form method="POST" action="{{ url_for('edit_project', project_id=project.id) }}" class="project-form">
<form method="POST" action="{{ url_for('projects.edit_project', project_id=project.id) }}" class="project-form">
<div class="form-row">
<div class="form-group">
<label for="name">Project Name *</label>
@@ -96,9 +96,29 @@
<div class="form-actions">
<button type="submit" class="btn">Update Project</button>
<a href="{{ url_for('admin_projects') }}" class="btn btn-secondary">Cancel</a>
<a href="{{ url_for('projects.admin_projects') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
<!-- Danger Zone (only for admins) -->
{% if g.user.role in [Role.ADMIN, Role.SYSTEM_ADMIN] %}
<div class="danger-zone">
<h3>⚠️ Danger Zone</h3>
<div class="danger-content">
<p><strong>Delete Project</strong></p>
<p>Once you delete a project, there is no going back. This will permanently delete:</p>
<ul>
<li>All tasks and subtasks in this project</li>
<li>All time entries logged to this project</li>
<li>All sprints associated with this project</li>
<li>All comments and activity history</li>
</ul>
<form method="POST" action="{{ url_for('projects.delete_project', project_id=project.id) }}" onsubmit="return confirm('Are you absolutely sure you want to delete {{ project.name }}? This action cannot be undone!');">
<button type="submit" class="btn btn-danger">Delete This Project</button>
</form>
</div>
</div>
{% endif %}
</div>
<style>
@@ -194,6 +214,47 @@
#code {
text-transform: uppercase;
}
/* Danger Zone */
.danger-zone {
margin-top: 3rem;
padding: 1.5rem;
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.danger-zone h3 {
color: #dc2626;
margin-top: 0;
margin-bottom: 1rem;
}
.danger-content {
color: #7f1d1d;
}
.danger-content p {
margin-bottom: 0.5rem;
}
.danger-content ul {
margin: 1rem 0 1.5rem 2rem;
}
.danger-content .btn-danger {
background-color: #dc2626;
color: white;
border: none;
padding: 0.5rem 1.5rem;
}
.danger-content .btn-danger:hover {
background-color: #b91c1c;
}
</style>
<script>

View File

@@ -4,7 +4,7 @@
<div class="admin-container">
<h1>Edit User: {{ user.username }}</h1>
<form method="POST" action="{{ url_for('edit_user', user_id=user.id) }}" class="user-form">
<form method="POST" action="{{ url_for('users.edit_user', user_id=user.id) }}" class="user-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" value="{{ user.username }}" required>
@@ -46,7 +46,7 @@
<div class="form-group">
<button type="submit" class="btn btn-primary">Update User</button>
<a href="{{ url_for('admin_users') }}" class="btn btn-secondary">Cancel</a>
<a href="{{ url_for('users.admin_users') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>

View File

@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invitation to {{ invitation.company.name }}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
border-radius: 8px;
padding: 40px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
font-size: 48px;
margin-bottom: 10px;
}
h1 {
color: #667eea;
font-size: 24px;
margin: 0;
}
.content {
margin-bottom: 30px;
}
.invitation-box {
background-color: #f8f9fa;
border-left: 4px solid #667eea;
padding: 20px;
margin: 20px 0;
}
.cta-button {
display: inline-block;
padding: 14px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
margin: 20px 0;
}
.details {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.details-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.details-label {
font-weight: 600;
color: #6b7280;
}
.footer {
text-align: center;
color: #6b7280;
font-size: 14px;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
}
.custom-message {
background-color: #ede9fe;
border-left: 4px solid #5b21b6;
padding: 15px;
margin: 20px 0;
font-style: italic;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">📨</div>
<h1>You're Invited to Join {{ invitation.company.name }}!</h1>
</div>
<div class="content">
<p>Hello,</p>
<p><strong>{{ sender.username }}</strong> has invited you to join <strong>{{ invitation.company.name }}</strong> on {{ g.branding.app_name }}.</p>
{% if custom_message %}
<div class="custom-message">
<strong>Personal message from {{ sender.username }}:</strong><br>
{{ custom_message }}
</div>
{% endif %}
<div class="invitation-box">
<h3 style="margin-top: 0;">Your Invitation Details:</h3>
<div class="details-item">
<span class="details-label">Company:</span>
<span>{{ invitation.company.name }}</span>
</div>
<div class="details-item">
<span class="details-label">Role:</span>
<span>{{ invitation.role }}</span>
</div>
<div class="details-item">
<span class="details-label">Invited by:</span>
<span>{{ sender.username }}</span>
</div>
<div class="details-item">
<span class="details-label">Expires:</span>
<span>{{ invitation.expires_at.strftime('%B %d, %Y') }}</span>
</div>
</div>
<div style="text-align: center;">
<a href="{{ invitation_url }}" class="cta-button">Accept Invitation</a>
<p style="font-size: 14px; color: #6b7280;">
Or copy and paste this link:<br>
<code style="background: #f3f4f6; padding: 5px; border-radius: 4px;">{{ invitation_url }}</code>
</p>
</div>
<div class="details">
<h3 style="margin-top: 0;">What happens next?</h3>
<ul style="margin: 0; padding-left: 20px;">
<li>Click the link above to accept the invitation</li>
<li>Create your account with a username and password</li>
<li>You'll automatically join {{ invitation.company.name }}</li>
<li>Start tracking your time and collaborating with your team!</li>
</ul>
</div>
</div>
<div class="footer">
<p>This invitation will expire on <strong>{{ invitation.expires_at.strftime('%B %d, %Y') }}</strong>.</p>
<p>If you didn't expect this invitation, you can safely ignore this email.</p>
<p>&copy; {{ g.branding.app_name }} - Time Tracking Made Simple</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reminder: Invitation to {{ invitation.company.name }}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
border-radius: 8px;
padding: 40px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.reminder-badge {
display: inline-block;
background: #f59e0b;
color: white;
padding: 5px 15px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
}
.logo {
font-size: 48px;
margin-bottom: 10px;
}
h1 {
color: #667eea;
font-size: 24px;
margin: 0;
}
.cta-button {
display: inline-block;
padding: 14px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
margin: 20px 0;
}
.expiry-warning {
background-color: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.footer {
text-align: center;
color: #6b7280;
font-size: 14px;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="reminder-badge">REMINDER</div>
<div class="logo">📨</div>
<h1>Your Invitation is Still Waiting!</h1>
</div>
<div class="content">
<p>Hello,</p>
<p>This is a friendly reminder that you still have a pending invitation to join <strong>{{ invitation.company.name }}</strong> on {{ g.branding.app_name }}.</p>
<div class="expiry-warning">
<strong>⏰ Time is running out!</strong><br>
This invitation will expire on <strong>{{ invitation.expires_at.strftime('%B %d, %Y') }}</strong>.
</div>
<p>Don't miss out on joining your team! Click the button below to accept your invitation and create your account:</p>
<div style="text-align: center;">
<a href="{{ invitation_url }}" class="cta-button">Accept Invitation Now</a>
<p style="font-size: 14px; color: #6b7280;">
Or copy and paste this link:<br>
<code style="background: #f3f4f6; padding: 5px; border-radius: 4px;">{{ invitation_url }}</code>
</p>
</div>
</div>
<div class="footer">
<p>If you're no longer interested in joining {{ invitation.company.name }}, you can safely ignore this email.</p>
<p>&copy; {{ g.branding.app_name }} - Time Tracking Made Simple</p>
</div>
</div>
</body>
</html>

View File

@@ -7,7 +7,7 @@
<div class="export-options">
<div class="export-section">
<h3>Date Range</h3>
<form action="{{ url_for('download_export') }}" method="get">
<form action="{{ url_for('export.download_export') }}" method="get">
<div class="form-group">
<label for="start_date">Start Date:</label>
<input type="date" id="start_date" name="start_date" required>
@@ -33,14 +33,14 @@
<div class="export-section">
<h3>Quick Export</h3>
<div class="quick-export-buttons">
<a href="{{ url_for('download_export', period='today', format='csv') }}" class="btn">Today (CSV)</a>
<a href="{{ url_for('download_export', period='today', format='excel') }}" class="btn">Today (Excel)</a>
<a href="{{ url_for('download_export', period='week', format='csv') }}" class="btn">This Week (CSV)</a>
<a href="{{ url_for('download_export', period='week', format='excel') }}" class="btn">This Week (Excel)</a>
<a href="{{ url_for('download_export', period='month', format='csv') }}" class="btn">This Month (CSV)</a>
<a href="{{ url_for('download_export', period='month', format='excel') }}" class="btn">This Month (Excel)</a>
<a href="{{ url_for('download_export', period='all', format='csv') }}" class="btn">All Time (CSV)</a>
<a href="{{ url_for('download_export', period='all', format='excel') }}" class="btn">All Time (Excel)</a>
<a href="{{ url_for('export.download_export', period='today', format='csv') }}" class="btn">Today (CSV)</a>
<a href="{{ url_for('export.download_export', period='today', format='excel') }}" class="btn">Today (Excel)</a>
<a href="{{ url_for('export.download_export', period='week', format='csv') }}" class="btn">This Week (CSV)</a>
<a href="{{ url_for('export.download_export', period='week', format='excel') }}" class="btn">This Week (Excel)</a>
<a href="{{ url_for('export.download_export', period='month', format='csv') }}" class="btn">This Month (CSV)</a>
<a href="{{ url_for('export.download_export', period='month', format='excel') }}" class="btn">This Month (Excel)</a>
<a href="{{ url_for('export.download_export', period='all', format='csv') }}" class="btn">All Time (CSV)</a>
<a href="{{ url_for('export.download_export', period='all', format='excel') }}" class="btn">All Time (Excel)</a>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

938
templates/index_old.html Normal file
View File

@@ -0,0 +1,938 @@
{% extends "layout.html" %}
{% block content %}
{% if not g.user %}
<!-- Decadent Splash Page -->
<div class="splash-container">
<!-- Hero Section -->
<section class="splash-hero">
<div class="hero-content">
<h1 class="hero-title">Transform Your Productivity</h1>
<p class="hero-subtitle">Experience the future of time management with {{ g.branding.app_name if g.branding else 'TimeTrack' }}'s intelligent tracking system</p>
<div class="cta-buttons">
<a href="{{ url_for('register') }}" class="btn-primary">Get Started Free</a>
<a href="{{ url_for('login') }}" class="btn-secondary">Sign In</a>
</div>
</div>
<div class="hero-visual">
<div class="floating-clock">
<div class="clock-face">
<div class="hour-hand"></div>
<div class="minute-hand"></div>
<div class="second-hand"></div>
</div>
</div>
</div>
</section>
<!-- Features Grid -->
<section class="features-grid">
<h2 class="section-title">Powerful Features for Modern Teams</h2>
<div class="feature-cards">
<div class="feature-card">
<div class="feature-icon"></div>
<h3>Lightning Fast</h3>
<p>Start tracking in seconds with our intuitive one-click interface</p>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<h3>Advanced Analytics</h3>
<p>Gain insights with comprehensive reports and visual dashboards</p>
</div>
<div class="feature-card">
<div class="feature-icon">🏃‍♂️</div>
<h3>Sprint Management</h3>
<p>Organize work into sprints with agile project tracking</p>
</div>
<div class="feature-card">
<div class="feature-icon">👥</div>
<h3>Team Collaboration</h3>
<p>Manage teams, projects, and resources all in one place</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔒</div>
<h3>Enterprise Security</h3>
<p>Bank-level encryption with role-based access control</p>
</div>
<div class="feature-card">
<div class="feature-icon">🌐</div>
<h3>Multi-Company Support</h3>
<p>Perfect for agencies managing multiple client accounts</p>
</div>
</div>
</section>
<!-- Why Choose Section -->
<section class="statistics">
<h2 class="section-title">Why Choose {{ g.branding.app_name if g.branding else 'TimeTrack' }}?</h2>
<div class="stat-item">
<div class="stat-number">100%</div>
<div class="stat-label">Free & Open Source</div>
</div>
<div class="stat-item">
<div class="stat-number"></div>
<div class="stat-label">Unlimited Tracking</div>
</div>
<div class="stat-item">
<div class="stat-number">0</div>
<div class="stat-label">Hidden Fees</div>
</div>
<div class="stat-item">
<div class="stat-number">24/7</div>
<div class="stat-label">Always Available</div>
</div>
</section>
<!-- Getting Started Section -->
<section class="testimonials">
<h2 class="section-title">Get Started in Minutes</h2>
<div class="testimonial-grid">
<div class="testimonial-card">
<div class="feature-icon">1</div>
<h3>Sign Up</h3>
<p>Create your free account in seconds. No credit card required.</p>
</div>
<div class="testimonial-card">
<div class="feature-icon">2</div>
<h3>Set Up Your Workspace</h3>
<p>Add your company, teams, and projects to organize your time tracking.</p>
</div>
<div class="testimonial-card">
<div class="feature-icon">3</div>
<h3>Start Tracking</h3>
<p>Click "Arrive" to start tracking, "Leave" when done. It's that simple!</p>
</div>
</div>
</section>
<!-- Open Source Section -->
<section class="pricing">
<h2 class="section-title">Forever Free, Forever Open</h2>
<div class="pricing-cards">
<div class="pricing-card featured">
<div class="badge">100% Free</div>
<h3>{{ g.branding.app_name if g.branding else 'TimeTrack' }} Community</h3>
<div class="price">$0<span>/forever</span></div>
<ul class="pricing-features">
<li>✓ Unlimited users</li>
<li>✓ All features included</li>
<li>✓ Time tracking & analytics</li>
<li>✓ Sprint management</li>
<li>✓ Team collaboration</li>
<li>✓ Project management</li>
<li>✓ Self-hosted option</li>
<li>✓ No restrictions</li>
</ul>
<a href="{{ url_for('register') }}" class="btn-pricing">Get Started Free</a>
</div>
</div>
<p style="text-align: center; margin-top: 2rem; color: #666;">
The software {{ g.branding.app_name if g.branding else 'TimeTrack' }} runs is open source software.<br />
Host it yourself or use our free hosted version.<br />
The source is available on GitHub:
<a href="https://github.com/nullmedium/TimeTrack" target="_blank">https://github.com/nullmedium/TimeTrack</a>
</p>
</section>
<!-- Final CTA -->
<section class="final-cta">
<h2>Ready to Take Control of Your Time?</h2>
<p>Start tracking your time effectively today - no strings attached</p>
<a href="{{ url_for('register') }}" class="btn-primary large">Create Free Account</a>
</section>
</div>
{% else %}
<!-- Include the modern time tracking interface from time_tracking.html -->
<div class="time-tracking-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">
<span class="page-icon">⏱️</span>
Time Tracking
</h1>
<p class="page-subtitle">Track your work hours efficiently</p>
</div>
<div class="header-actions">
<button id="manual-entry-btn" class="btn btn-secondary">
<span class="icon">📝</span>
Manual Entry
</button>
<a href="{{ url_for('analytics') }}" class="btn btn-secondary">
<span class="icon">📊</span>
View Analytics
</a>
</div>
</div>
</div>
<!-- Timer Section -->
<div class="timer-section">
{% if active_entry %}
<!-- Active Timer -->
<div class="timer-card active">
<div class="timer-display">
<div class="timer-value" id="timer"
data-start="{{ active_entry.arrival_time.timestamp() }}"
data-breaks="{{ active_entry.total_break_duration }}"
data-paused="{{ 'true' if active_entry.is_paused else 'false' }}">
00:00:00
</div>
<div class="timer-status">
{% if active_entry.is_paused %}
<span class="status-badge paused">On Break</span>
{% else %}
<span class="status-badge active">Working</span>
{% endif %}
</div>
</div>
<div class="timer-info">
<div class="info-row">
<span class="info-label">Started:</span>
<span class="info-value">{{ active_entry.arrival_time|format_datetime }}</span>
</div>
{% if active_entry.project %}
<div class="info-row">
<span class="info-label">Project:</span>
<span class="info-value project-badge" style="background-color: {{ active_entry.project.color or '#667eea' }}">
{{ active_entry.project.code }} - {{ active_entry.project.name }}
</span>
</div>
{% endif %}
{% if active_entry.task %}
<div class="info-row">
<span class="info-label">Task:</span>
<span class="info-value task-badge">
{{ active_entry.task.title }}
</span>
</div>
{% endif %}
{% if active_entry.notes %}
<div class="info-row">
<span class="info-label">Notes:</span>
<span class="info-value">{{ active_entry.notes }}</span>
</div>
{% endif %}
{% if active_entry.is_paused %}
<div class="info-row">
<span class="info-label">Break started:</span>
<span class="info-value">{{ active_entry.pause_start_time|format_time }}</span>
</div>
{% endif %}
{% if active_entry.total_break_duration > 0 %}
<div class="info-row">
<span class="info-label">Total breaks:</span>
<span class="info-value">{{ active_entry.total_break_duration|format_duration }}</span>
</div>
{% endif %}
</div>
<div class="timer-actions">
<button id="pause-btn" class="btn {% if active_entry.is_paused %}btn-success{% else %}btn-warning{% endif %}"
data-id="{{ active_entry.id }}">
{% if active_entry.is_paused %}
<span class="icon">▶️</span>
Resume Work
{% else %}
<span class="icon">⏸️</span>
Take Break
{% endif %}
</button>
<button id="leave-btn" class="btn btn-danger" data-id="{{ active_entry.id }}">
<span class="icon">⏹️</span>
Stop Working
</button>
</div>
</div>
{% else %}
<!-- Inactive Timer -->
<div class="timer-card inactive">
<div class="start-work-container">
<h2>Start Tracking Time</h2>
<p>Select a project and task to begin tracking your work</p>
<form id="start-work-form" class="modern-form">
<div class="form-row">
<div class="form-group">
<label for="project-select" class="form-label">
Project <span class="optional-badge">Optional</span>
</label>
<select id="project-select" name="project_id" class="form-control">
<option value="">No specific project</option>
{% for project in available_projects %}
<option value="{{ project.id }}" data-color="{{ project.color or '#667eea' }}">
{{ project.code }} - {{ project.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="task-select" class="form-label">
Task <span class="optional-badge">Optional</span>
</label>
<select id="task-select" name="task_id" class="form-control" disabled>
<option value="">Select a project first</option>
</select>
</div>
</div>
<div class="form-group">
<label for="work-notes" class="form-label">
Notes <span class="optional-badge">Optional</span>
</label>
<textarea id="work-notes" name="notes" class="form-control"
rows="2" placeholder="What are you working on?"></textarea>
</div>
<div class="form-actions">
<button type="submit" id="arrive-btn" class="btn btn-primary btn-large">
<span class="icon">▶️</span>
Start Working
</button>
</div>
</form>
</div>
</div>
{% endif %}
</div>
<!-- Quick Stats -->
{% if today_hours is defined %}
<div class="stats-section">
<div class="stat-card">
<div class="stat-value">{{ today_hours|format_duration }}</div>
<div class="stat-label">Today</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ week_hours|format_duration }}</div>
<div class="stat-label">This Week</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ month_hours|format_duration }}</div>
<div class="stat-label">This Month</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ active_projects|length if active_projects else 0 }}</div>
<div class="stat-label">Active Projects</div>
</div>
</div>
{% endif %}
<!-- Recent Entries -->
<div class="entries-section">
<div class="section-header">
<h2 class="section-title">
<span class="icon">📋</span>
Recent Time Entries
</h2>
<div class="view-toggle">
<button class="toggle-btn active" data-view="list">
<span class="icon">📝</span>
List
</button>
<button class="toggle-btn" data-view="grid">
<span class="icon">📊</span>
Grid
</button>
</div>
</div>
<!-- List View -->
<div id="list-view" class="view-container active">
{% if history %}
<div class="entries-table-container">
<table class="entries-table">
<thead>
<tr>
<th>Date</th>
<th>Time</th>
<th>Project / Task</th>
<th>Duration</th>
<th>Break</th>
<th>Notes</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for entry in history %}
<tr data-entry-id="{{ entry.id }}" class="entry-row">
<td>
<div class="date-cell">
<span class="date-day">{{ entry.arrival_time.strftime('%d') }}</span>
<span class="date-month">{{ entry.arrival_time.strftime('%b') }}</span>
</div>
</td>
<td>
<div class="time-cell">
<span class="time-start">{{ entry.arrival_time|format_time }}</span>
<span class="time-separator"></span>
<span class="time-end">{{ entry.departure_time|format_time if entry.departure_time else 'Active' }}</span>
</div>
</td>
<td>
<div class="project-task-cell">
{% if entry.project %}
<span class="project-tag" style="background-color: {{ entry.project.color or '#667eea' }}">
{{ entry.project.code }}
</span>
{% endif %}
{% if entry.task %}
<span class="task-name">{{ entry.task.title }}</span>
{% elif entry.project %}
<span class="project-name">{{ entry.project.name }}</span>
{% else %}
<span class="no-project">No project</span>
{% endif %}
</div>
</td>
<td>
<span class="duration-badge">
{{ entry.duration|format_duration if entry.duration is not none else 'In progress' }}
</span>
</td>
<td>
<span class="break-duration">
{{ entry.total_break_duration|format_duration if entry.total_break_duration else '-' }}
</span>
</td>
<td>
<span class="notes-preview" title="{{ entry.notes or '' }}">
{{ entry.notes[:30] + '...' if entry.notes and entry.notes|length > 30 else entry.notes or '-' }}
</span>
</td>
<td>
<div class="actions-cell">
{% if entry.departure_time and not active_entry %}
<button class="btn-icon resume-work-btn" data-id="{{ entry.id }}" title="Resume">
<span class="icon">🔄</span>
</button>
{% endif %}
<button class="btn-icon edit-entry-btn" data-id="{{ entry.id }}" title="Edit">
<span class="icon">✏️</span>
</button>
<button class="btn-icon delete-entry-btn" data-id="{{ entry.id }}" title="Delete">
<span class="icon">🗑️</span>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">📭</div>
<h3>No time entries yet</h3>
<p>Start tracking your time to see entries here</p>
</div>
{% endif %}
</div>
<!-- Grid View -->
<div id="grid-view" class="view-container">
<div class="entries-grid">
{% for entry in history %}
<div class="entry-card" data-entry-id="{{ entry.id }}">
<div class="entry-header">
<div class="entry-date">
{{ entry.arrival_time.strftime('%d %b %Y') }}
</div>
<div class="entry-duration">
{{ entry.duration|format_duration if entry.duration is not none else 'Active' }}
</div>
</div>
<div class="entry-body">
{% if entry.project %}
<div class="entry-project" style="border-left-color: {{ entry.project.color or '#667eea' }}">
<strong>{{ entry.project.code }}</strong> - {{ entry.project.name }}
</div>
{% endif %}
{% if entry.task %}
<div class="entry-task">
<span class="icon">📋</span> {{ entry.task.title }}
</div>
{% endif %}
<div class="entry-time">
<span class="icon">🕐</span>
{{ entry.arrival_time|format_time }} - {{ entry.departure_time|format_time if entry.departure_time else 'Active' }}
</div>
{% if entry.notes %}
<div class="entry-notes">
<span class="icon">📝</span>
{{ entry.notes }}
</div>
{% endif %}
</div>
<div class="entry-footer">
<button class="btn-sm edit-entry-btn" data-id="{{ entry.id }}">Edit</button>
<button class="btn-sm btn-danger delete-entry-btn" data-id="{{ entry.id }}">Delete</button>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Edit Entry Modal -->
<div id="edit-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Edit Time Entry</h3>
<form id="edit-entry-form">
<input type="hidden" id="edit-entry-id">
<div class="form-group">
<label for="edit-arrival-date">Arrival Date:</label>
<input type="date" id="edit-arrival-date" required>
<small>Format: YYYY-MM-DD</small>
</div>
<div class="form-group">
<label for="edit-arrival-time">Arrival Time (24h):</label>
<input type="time" id="edit-arrival-time" required step="1">
<small>Format: HH:MM (24-hour)</small>
</div>
<div class="form-group">
<label for="edit-departure-date">Departure Date:</label>
<input type="date" id="edit-departure-date">
<small>Format: YYYY-MM-DD</small>
</div>
<div class="form-group">
<label for="edit-departure-time">Departure Time (24h):</label>
<input type="time" id="edit-departure-time" step="1">
<small>Format: HH:MM (24-hour)</small>
</div>
<button type="submit" class="btn">Save Changes</button>
</form>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Confirm Deletion</h3>
<p>Are you sure you want to delete this time entry? This action cannot be undone.</p>
<input type="hidden" id="delete-entry-id">
<div class="modal-actions">
<button id="confirm-delete" class="btn btn-danger">Delete</button>
<button id="cancel-delete" class="btn">Cancel</button>
</div>
</div>
</div>
<!-- Manual Time Entry Modal -->
<div id="manual-entry-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Add Manual Time Entry</h3>
<form id="manual-entry-form">
<div class="form-group">
<label for="manual-project-select">Project (Optional):</label>
<select id="manual-project-select" name="project_id">
<option value="">No specific project</option>
{% for project in available_projects %}
<option value="{{ project.id }}">{{ project.code }} - {{ project.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="manual-start-date">Start Date:</label>
<input type="date" id="manual-start-date" required>
</div>
<div class="form-group">
<label for="manual-start-time">Start Time:</label>
<input type="time" id="manual-start-time" required step="1">
</div>
<div class="form-group">
<label for="manual-end-date">End Date:</label>
<input type="date" id="manual-end-date" required>
</div>
<div class="form-group">
<label for="manual-end-time">End Time:</label>
<input type="time" id="manual-end-time" required step="1">
</div>
<div class="form-group">
<label for="manual-break-minutes">Break Duration (minutes):</label>
<input type="number" id="manual-break-minutes" min="0" value="0" placeholder="Break time in minutes">
</div>
<div class="form-group">
<label for="manual-notes">Notes (Optional):</label>
<textarea id="manual-notes" name="notes" rows="3" placeholder="Description of work performed"></textarea>
</div>
<button type="submit" class="btn">Add Entry</button>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Manual entry functionality
document.getElementById('manual-entry-btn').addEventListener('click', function() {
// Set default dates to today
const today = new Date().toISOString().split('T')[0];
document.getElementById('manual-start-date').value = today;
document.getElementById('manual-end-date').value = today;
document.getElementById('manual-entry-modal').style.display = 'block';
});
// Manual entry form submission
document.getElementById('manual-entry-form').addEventListener('submit', function(e) {
e.preventDefault();
const projectId = document.getElementById('manual-project-select').value || null;
const startDate = document.getElementById('manual-start-date').value;
const startTime = document.getElementById('manual-start-time').value;
const endDate = document.getElementById('manual-end-date').value;
const endTime = document.getElementById('manual-end-time').value;
const breakMinutes = parseInt(document.getElementById('manual-break-minutes').value) || 0;
const notes = document.getElementById('manual-notes').value;
// Validate end time is after start time
const startDateTime = new Date(`${startDate}T${startTime}`);
const endDateTime = new Date(`${endDate}T${endTime}`);
if (endDateTime <= startDateTime) {
alert('End time must be after start time');
return;
}
// Send request to create manual entry
fetch('/api/manual-entry', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
project_id: projectId,
start_date: startDate,
start_time: startTime,
end_date: endDate,
end_time: endTime,
break_minutes: breakMinutes,
notes: notes
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('manual-entry-modal').style.display = 'none';
location.reload(); // Refresh to show new entry
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while adding the manual entry');
});
});
// Edit entry functionality
document.querySelectorAll('.edit-entry-btn').forEach(button => {
button.addEventListener('click', function() {
const entryId = this.getAttribute('data-id');
const row = document.querySelector(`tr[data-entry-id="${entryId}"]`);
const cells = row.querySelectorAll('td');
// Get date and time from the row
const dateStr = cells[0].textContent.trim();
const arrivalTimeStr = cells[2].textContent.trim(); // arrival time is now in column 2
const departureTimeStr = cells[3].textContent.trim(); // departure time is now in column 3
// Set values in the form
document.getElementById('edit-entry-id').value = entryId;
document.getElementById('edit-arrival-date').value = dateStr;
// Format time for input (HH:MM format)
document.getElementById('edit-arrival-time').value = arrivalTimeStr.substring(0, 5);
if (departureTimeStr && departureTimeStr !== 'Active') {
document.getElementById('edit-departure-date').value = dateStr;
document.getElementById('edit-departure-time').value = departureTimeStr.substring(0, 5);
} else {
document.getElementById('edit-departure-date').value = '';
document.getElementById('edit-departure-time').value = '';
}
// Show the modal
document.getElementById('edit-modal').style.display = 'block';
});
});
// Delete entry functionality
document.querySelectorAll('.delete-entry-btn').forEach(button => {
button.addEventListener('click', function() {
const entryId = this.getAttribute('data-id');
document.getElementById('delete-entry-id').value = entryId;
document.getElementById('delete-modal').style.display = 'block';
});
});
// Close modals when clicking the X
document.querySelectorAll('.close').forEach(closeBtn => {
closeBtn.addEventListener('click', function() {
this.closest('.modal').style.display = 'none';
});
});
// Close modals when clicking outside
window.addEventListener('click', function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
});
// Cancel delete
document.getElementById('cancel-delete').addEventListener('click', function() {
document.getElementById('delete-modal').style.display = 'none';
});
// Confirm delete
document.getElementById('confirm-delete').addEventListener('click', function() {
const entryId = document.getElementById('delete-entry-id').value;
fetch(`/api/delete/${entryId}`, {
method: 'DELETE',
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the row from the table
document.querySelector(`tr[data-entry-id="${entryId}"]`).remove();
// Close the modal
document.getElementById('delete-modal').style.display = 'none';
// Show success message
alert('Entry deleted successfully');
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the entry');
});
});
// Submit edit form
document.getElementById('edit-entry-form').addEventListener('submit', function(e) {
e.preventDefault();
const entryId = document.getElementById('edit-entry-id').value;
const arrivalDate = document.getElementById('edit-arrival-date').value;
const arrivalTime = document.getElementById('edit-arrival-time').value;
const departureDate = document.getElementById('edit-departure-date').value || '';
const departureTime = document.getElementById('edit-departure-time').value || '';
// Ensure we have seconds in the time strings
const arrivalTimeWithSeconds = arrivalTime.includes(':') ?
(arrivalTime.split(':').length === 2 ? arrivalTime + ':00' : arrivalTime) :
arrivalTime + ':00:00';
// Format datetime strings for the API (ISO 8601: YYYY-MM-DDTHH:MM:SS)
const arrivalDateTime = `${arrivalDate}T${arrivalTimeWithSeconds}`;
let departureDateTime = null;
if (departureDate && departureTime) {
const departureTimeWithSeconds = departureTime.includes(':') ?
(departureTime.split(':').length === 2 ? departureTime + ':00' : departureTime) :
departureTime + ':00:00';
departureDateTime = `${departureDate}T${departureTimeWithSeconds}`;
}
console.log('Sending update:', {
arrival_time: arrivalDateTime,
departure_time: departureDateTime
});
// Send update request
fetch(`/api/update/${entryId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
arrival_time: arrivalDateTime,
departure_time: departureDateTime
}),
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || 'Server error');
});
}
return response.json();
})
.then(data => {
if (data.success) {
// Close the modal
document.getElementById('edit-modal').style.display = 'none';
// Refresh the page to show updated data
location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while updating the entry: ' + error.message);
});
});
});
</script>
<style>
.start-work-form {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.start-work-form .form-group {
margin-bottom: 1rem;
}
.start-work-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.start-work-form select,
.start-work-form textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.start-work-form select:focus,
.start-work-form textarea:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
}
.project-info {
color: #4CAF50;
font-size: 0.9rem;
margin-top: 0.5rem;
}
.project-tag {
background: #4CAF50;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
margin-right: 0.5rem;
}
.project-tag + small {
color: #666;
font-size: 0.85rem;
}
.time-history td {
vertical-align: middle;
}
.time-history .project-tag + small {
display: block;
margin-top: 0.25rem;
}
.manual-entry-btn {
background: #17a2b8;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
margin-left: 1rem;
transition: background-color 0.2s ease;
}
.manual-entry-btn:hover {
background: #138496;
}
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
border-radius: 8px;
width: 500px;
max-width: 90%;
max-height: 80%;
overflow-y: auto;
}
.modal .form-group {
margin-bottom: 1rem;
}
.modal label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.modal input,
.modal select,
.modal textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 1rem;
box-sizing: border-box;
}
.modal input:focus,
.modal select:focus,
.modal textarea:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
}
</style>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,473 @@
{% extends "layout.html" %}
{% block content %}
<div class="invitations-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">
<span class="page-icon">📨</span>
Invitations
</h1>
<p class="page-subtitle">Manage team invitations for {{ g.user.company.name }}</p>
</div>
<div class="header-actions">
<a href="{{ url_for('invitations.send_invitation') }}" class="btn btn-primary">
<span class="icon">+</span>
Send New Invitation
</a>
</div>
</div>
</div>
<!-- Statistics -->
<div class="stats-section">
<div class="stat-card">
<div class="stat-value">{{ pending_invitations|length }}</div>
<div class="stat-label">Pending</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ accepted_invitations|length }}</div>
<div class="stat-label">Accepted</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ expired_invitations|length }}</div>
<div class="stat-label">Expired</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ (pending_invitations|length + accepted_invitations|length + expired_invitations|length) }}</div>
<div class="stat-label">Total Sent</div>
</div>
</div>
<!-- Pending Invitations -->
{% if pending_invitations %}
<div class="section">
<h2 class="section-title">
<span class="icon"></span>
Pending Invitations
</h2>
<div class="invitations-list">
{% for invitation in pending_invitations %}
<div class="invitation-card pending">
<div class="invitation-header">
<div class="invitation-info">
<h3 class="invitation-email">{{ invitation.email }}</h3>
<div class="invitation-meta">
<span class="meta-item">
<span class="icon">👤</span>
Role: {{ invitation.role }}
</span>
<span class="meta-item">
<span class="icon">📅</span>
Sent {{ invitation.created_at.strftime('%b %d, %Y') }}
</span>
<span class="meta-item">
<span class="icon"></span>
Expires {{ invitation.expires_at.strftime('%b %d, %Y') }}
</span>
</div>
</div>
<div class="invitation-actions">
<form method="POST" action="{{ url_for('invitations.resend_invitation', invitation_id=invitation.id) }}" style="display: inline;">
<button type="submit" class="btn btn-sm btn-secondary">
<span class="icon">🔄</span>
Resend
</button>
</form>
<form method="POST" action="{{ url_for('invitations.revoke_invitation', invitation_id=invitation.id) }}" style="display: inline;">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to revoke this invitation?');">
<span class="icon"></span>
Revoke
</button>
</form>
</div>
</div>
<div class="invitation-footer">
<span class="footer-text">Invited by {{ invitation.invited_by.username }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Accepted Invitations -->
{% if accepted_invitations %}
<div class="section">
<h2 class="section-title">
<span class="icon"></span>
Accepted Invitations
</h2>
<div class="invitations-list">
{% for invitation in accepted_invitations %}
<div class="invitation-card accepted">
<div class="invitation-header">
<div class="invitation-info">
<h3 class="invitation-email">{{ invitation.email }}</h3>
<div class="invitation-meta">
<span class="meta-item">
<span class="icon">👤</span>
Joined as: {{ invitation.accepted_by.username }} ({{ invitation.role }})
</span>
<span class="meta-item">
<span class="icon">📅</span>
Accepted {{ invitation.accepted_at.strftime('%b %d, %Y') }}
</span>
</div>
</div>
<div class="invitation-actions">
<a href="{{ url_for('users.view_user', user_id=invitation.accepted_by.id) }}" class="btn btn-sm btn-secondary">
<span class="icon">👁️</span>
View User
</a>
</div>
</div>
<div class="invitation-footer">
<span class="footer-text">Invited by {{ invitation.invited_by.username }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Expired Invitations -->
{% if expired_invitations %}
<div class="section">
<h2 class="section-title">
<span class="icon">⏱️</span>
Expired Invitations
</h2>
<div class="invitations-list">
{% for invitation in expired_invitations %}
<div class="invitation-card expired">
<div class="invitation-header">
<div class="invitation-info">
<h3 class="invitation-email">{{ invitation.email }}</h3>
<div class="invitation-meta">
<span class="meta-item">
<span class="icon">👤</span>
Role: {{ invitation.role }}
</span>
<span class="meta-item">
<span class="icon">📅</span>
Expired {{ invitation.expires_at.strftime('%b %d, %Y') }}
</span>
</div>
</div>
<div class="invitation-actions">
<form method="POST" action="{{ url_for('invitations.resend_invitation', invitation_id=invitation.id) }}" style="display: inline;">
<button type="submit" class="btn btn-sm btn-primary">
<span class="icon">📤</span>
Send New Invitation
</button>
</form>
</div>
</div>
<div class="invitation-footer">
<span class="footer-text">Originally invited by {{ invitation.invited_by.username }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Empty State -->
{% if not pending_invitations and not accepted_invitations and not expired_invitations %}
<div class="empty-state">
<div class="empty-icon">📨</div>
<h3>No invitations yet</h3>
<p>Start building your team by sending invitations</p>
<a href="{{ url_for('invitations.send_invitation') }}" class="btn btn-primary">
<span class="icon">+</span>
Send First Invitation
</a>
</div>
{% endif %}
</div>
<style>
.invitations-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* Header styles - reuse from other pages */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.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;
}
/* Stats section */
.stats-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 12px;
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: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;
}
/* Sections */
.section {
margin-bottom: 2rem;
}
.section-title {
font-size: 1.5rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Invitation cards */
.invitations-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.invitation-card {
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;
}
.invitation-card.pending {
border-left: 4px solid #f59e0b;
}
.invitation-card.accepted {
border-left: 4px solid #10b981;
}
.invitation-card.expired {
border-left: 4px solid #ef4444;
}
.invitation-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.invitation-header {
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.invitation-email {
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 0.5rem 0;
}
.invitation-meta {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
.meta-item {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
color: #6b7280;
}
.invitation-actions {
display: flex;
gap: 0.5rem;
}
.invitation-footer {
background: #f8f9fa;
padding: 0.75rem 1.5rem;
border-top: 1px solid #e5e7eb;
}
.footer-text {
font-size: 0.875rem;
color: #6b7280;
}
/* 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-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.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: #4b5563;
border: 2px solid #e5e7eb;
}
.btn-secondary:hover {
background: #f3f4f6;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
/* Empty state */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: white;
border-radius: 12px;
border: 1px solid #e5e7eb;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-state h3 {
font-size: 1.5rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 0.5rem;
}
.empty-state p {
color: #6b7280;
margin-bottom: 2rem;
}
/* Responsive */
@media (max-width: 768px) {
.invitations-container {
padding: 1rem;
}
.invitation-header {
flex-direction: column;
align-items: flex-start;
}
.invitation-actions {
width: 100%;
justify-content: flex-start;
}
.meta-item {
font-size: 0.8rem;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,378 @@
{% extends "layout.html" %}
{% block content %}
<div class="invitation-send-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">
<span class="page-icon">✉️</span>
Send Invitation
</h1>
<p class="page-subtitle">Invite team members to join {{ g.user.company.name }}</p>
</div>
<div class="header-actions">
<a href="{{ url_for('invitations.list_invitations') }}" class="btn btn-secondary">
<span class="icon"></span>
Back to Invitations
</a>
</div>
</div>
</div>
<!-- Main Content -->
<div class="content-wrapper">
<div class="card invitation-form-card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">👥</span>
Invitation Details
</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('invitations.send_invitation') }}" class="modern-form">
<div class="form-group">
<label for="email" class="form-label">Email Address</label>
<input type="email" id="email" name="email" class="form-control"
placeholder="colleague@example.com" required autofocus>
<span class="form-hint">The email address where the invitation will be sent</span>
</div>
<div class="form-group">
<label for="role" class="form-label">Role</label>
<select id="role" name="role" class="form-control">
{% for role in roles %}
<option value="{{ role }}" {% if role == 'Team Member' %}selected{% endif %}>
{{ role }}
</option>
{% endfor %}
</select>
<span class="form-hint">The role this user will have when they join</span>
</div>
<div class="form-group">
<label for="custom_message" class="form-label">Personal Message (Optional)</label>
<textarea id="custom_message" name="custom_message" class="form-control"
rows="4" placeholder="Add a personal message to the invitation..."></textarea>
<span class="form-hint">This message will be included in the invitation email</span>
</div>
<div class="info-panel">
<div class="info-item">
<span class="info-icon">📧</span>
<div class="info-content">
<h4>What happens next?</h4>
<ul>
<li>An email invitation will be sent immediately</li>
<li>The recipient will have 7 days to accept</li>
<li>They'll create their account using the invitation link</li>
<li>They'll automatically join {{ g.user.company.name }}</li>
</ul>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon">📤</span>
Send Invitation
</button>
<a href="{{ url_for('invitations.list_invitations') }}" class="btn btn-ghost">
Cancel
</a>
</div>
</form>
</div>
</div>
<!-- Preview Section -->
<div class="card preview-card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">👁️</span>
Email Preview
</h2>
</div>
<div class="card-body">
<div class="email-preview">
<div class="preview-from">
<strong>From:</strong> {{ g.branding.app_name }} &lt;{{ g.branding.email_from or 'noreply@timetrack.com' }}&gt;
</div>
<div class="preview-to">
<strong>To:</strong> <span id="preview-email">colleague@example.com</span>
</div>
<div class="preview-subject">
<strong>Subject:</strong> Invitation to join {{ g.user.company.name }} on {{ g.branding.app_name }}
</div>
<div class="preview-body">
<p>Hello,</p>
<p>{{ g.user.username }} has invited you to join {{ g.user.company.name }} on {{ g.branding.app_name }}.</p>
<p id="preview-message" style="display: none;"></p>
<p>Click the link below to accept the invitation and create your account:</p>
<p><a href="#" class="preview-link">[Invitation Link]</a></p>
<p>This invitation will expire in 7 days.</p>
<p>Best regards,<br>The {{ g.branding.app_name }} Team</p>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.invitation-send-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.content-wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-top: 2rem;
}
@media (max-width: 1024px) {
.content-wrapper {
grid-template-columns: 1fr;
}
}
.invitation-form-card,
.preview-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e5e7eb;
}
.email-preview {
background: #f8f9fa;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 1.5rem;
font-family: system-ui, -apple-system, sans-serif;
}
.email-preview > div {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #e5e7eb;
}
.preview-body {
border-bottom: none !important;
margin-top: 1rem;
}
.preview-link {
color: #667eea;
text-decoration: underline;
}
/* Reuse existing styles */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 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;
}
.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-body {
padding: 1.5rem;
}
.modern-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-label {
font-weight: 600;
color: #374151;
font-size: 0.95rem;
}
.form-control {
padding: 0.75rem 1rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1rem;
transition: all 0.2s ease;
background-color: #f9fafb;
}
.form-control:focus {
outline: none;
border-color: #667eea;
background-color: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-hint {
font-size: 0.875rem;
color: #6b7280;
}
.info-panel {
background: #f3f4f6;
border-radius: 8px;
padding: 1.5rem;
}
.info-item {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.info-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.info-content h4 {
font-size: 1rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 0.5rem 0;
}
.info-content ul {
margin: 0;
padding-left: 1.25rem;
}
.info-content li {
margin-bottom: 0.25rem;
color: #4b5563;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 0.5rem;
}
.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: #4b5563;
border: 2px solid #e5e7eb;
}
.btn-secondary:hover {
background: #f3f4f6;
}
.btn-ghost {
background: transparent;
color: #6b7280;
}
.btn-ghost:hover {
color: #374151;
background: #f3f4f6;
}
</style>
<script>
// Live preview updates
document.getElementById('email').addEventListener('input', function(e) {
document.getElementById('preview-email').textContent = e.target.value || 'colleague@example.com';
});
document.getElementById('custom_message').addEventListener('input', function(e) {
const previewMessage = document.getElementById('preview-message');
if (e.target.value.trim()) {
previewMessage.textContent = e.target.value;
previewMessage.style.display = 'block';
} else {
previewMessage.style.display = 'none';
}
});
</script>
{% endblock %}

View File

@@ -129,31 +129,30 @@
<ul>
{% if g.user %}
<li><a href="{{ url_for('home') }}" data-tooltip="Home"><i class="nav-icon">🏠</i><span class="nav-text">Home</span></a></li>
<li><a href="{{ url_for('time_tracking') }}" data-tooltip="Time Tracking"><i class="nav-icon">⏱️</i><span class="nav-text">Time Tracking</span></a></li>
<li><a href="{{ url_for('dashboard') }}" data-tooltip="Dashboard"><i class="nav-icon">📊</i><span class="nav-text">Dashboard</span></a></li>
<li><a href="{{ url_for('unified_task_management') }}" data-tooltip="Task Management"><i class="nav-icon">📋</i><span class="nav-text">Task Management</span></a></li>
<li><a href="{{ url_for('sprint_management') }}" data-tooltip="Sprint Management"><i class="nav-icon">🏃‍♂️</i><span class="nav-text">Sprints</span></a></li>
<li><a href="{{ url_for('tasks.unified_task_management') }}" data-tooltip="Task Management"><i class="nav-icon">📋</i><span class="nav-text">Task Management</span></a></li>
<li><a href="{{ url_for('sprints.sprint_management') }}" data-tooltip="Sprint Management"><i class="nav-icon">🏃‍♂️</i><span class="nav-text">Sprints</span></a></li>
<li><a href="{{ url_for('analytics') }}" data-tooltip="Time Analytics"><i class="nav-icon">📊</i><span class="nav-text">Analytics</span></a></li>
<!-- Role-based menu items -->
{% if g.user.role == Role.ADMIN or g.user.role == Role.SYSTEM_ADMIN %}
<li class="nav-divider">Admin</li>
<li><a href="{{ url_for('dashboard') }}" data-tooltip="Dashboard"><i class="nav-icon">📈</i><span class="nav-text">Dashboard</span></a></li>
<li><a href="{{ url_for('admin_company') }}" data-tooltip="Company"><i class="nav-icon">🏢</i><span class="nav-text">Company</span></a></li>
<li><a href="{{ url_for('admin_users') }}" data-tooltip="Manage Users"><i class="nav-icon">👥</i><span class="nav-text">Manage Users</span></a></li>
<li><a href="{{ url_for('admin_teams') }}" data-tooltip="Manage Teams"><i class="nav-icon">🏭</i><span class="nav-text">Manage Teams</span></a></li>
<li><a href="{{ url_for('admin_projects') }}" data-tooltip="Manage Projects"><i class="nav-icon">📝</i><span class="nav-text">Manage Projects</span></a></li>
<li><a href="{{ url_for('admin_work_policies') }}" data-tooltip="Work Policies"><i class="nav-icon">⚖️</i><span class="nav-text">Work Policies</span></a></li>
<li><a href="{{ url_for('admin_settings') }}" data-tooltip="System Settings"><i class="nav-icon">🔧</i><span class="nav-text">System Settings</span></a></li>
<li><a href="{{ url_for('companies.admin_company') }}" data-tooltip="Company Settings"><i class="nav-icon">🏢</i><span class="nav-text">Company Settings</span></a></li>
<li><a href="{{ url_for('users.admin_users') }}" data-tooltip="Manage Users"><i class="nav-icon">👥</i><span class="nav-text">Manage Users</span></a></li>
<li><a href="{{ url_for('invitations.list_invitations') }}" data-tooltip="Invitations"><i class="nav-icon">📨</i><span class="nav-text">Invitations</span></a></li>
<li><a href="{{ url_for('teams.admin_teams') }}" data-tooltip="Manage Teams"><i class="nav-icon">🏭</i><span class="nav-text">Manage Teams</span></a></li>
<li><a href="{{ url_for('projects.admin_projects') }}" data-tooltip="Manage Projects"><i class="nav-icon">📝</i><span class="nav-text">Manage Projects</span></a></li>
{% if g.user.role == Role.SYSTEM_ADMIN %}
<li class="nav-divider">System Admin</li>
<li><a href="{{ url_for('system_admin_dashboard') }}" data-tooltip="System Dashboard"><i class="nav-icon">🌐</i><span class="nav-text">System Dashboard</span></a></li>
<li><a href="{{ url_for('system_admin_announcements') }}" data-tooltip="Announcements"><i class="nav-icon">📢</i><span class="nav-text">Announcements</span></a></li>
<li><a href="{{ url_for('system_admin.system_admin_dashboard') }}" data-tooltip="System Dashboard"><i class="nav-icon">🌐</i><span class="nav-text">System Dashboard</span></a></li>
<li><a href="{{ url_for('announcements.index') }}" data-tooltip="Announcements"><i class="nav-icon">📢</i><span class="nav-text">Announcements</span></a></li>
{% endif %}
{% elif g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR] %}
<li class="nav-divider">Team</li>
<li><a href="{{ url_for('dashboard') }}" data-tooltip="Dashboard"><i class="nav-icon">📈</i><span class="nav-text">Dashboard</span></a></li>
{% if g.user.role == Role.SUPERVISOR %}
<li><a href="{{ url_for('admin_projects') }}" data-tooltip="Manage Projects"><i class="nav-icon">📝</i><span class="nav-text">Manage Projects</span></a></li>
<li><a href="{{ url_for('projects.admin_projects') }}" data-tooltip="Manage Projects"><i class="nav-icon">📝</i><span class="nav-text">Manage Projects</span></a></li>
{% endif %}
{% endif %}
{% else %}

View File

@@ -14,7 +14,7 @@
</div>
<div class="page-actions task-actions">
<button id="create-task-btn" class="btn btn-success">Create New Task</button>
<a href="{{ url_for('admin_projects') }}" class="btn btn-secondary">Back to Projects</a>
<a href="{{ url_for('projects.admin_projects') }}" class="btn btn-secondary">Back to Projects</a>
</div>
</div>

View File

@@ -1,96 +0,0 @@
{% extends 'layout.html' %}
{% block content %}
<div class="container">
<h1>Manage Team: {{ team.name }}</h1>
<div class="card mb-4">
<div class="card-header">
<h2>Team Details</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('manage_team', team_id=team.id) }}">
<input type="hidden" name="action" value="update_team">
<div class="mb-3">
<label for="name" class="form-label">Team Name</label>
<input type="text" class="form-control" id="name" name="name" value="{{ team.name }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ team.description }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Update Team</button>
</form>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2>Team Members</h2>
</div>
<div class="card-body">
{% if team_members %}
<table class="table table-striped">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for member in team_members %}
<tr>
<td>{{ member.username }}</td>
<td>{{ member.email }}</td>
<td>{{ member.role.value }}</td>
<td>
<form method="POST" action="{{ url_for('manage_team', team_id=team.id) }}" class="d-inline">
<input type="hidden" name="action" value="remove_member">
<input type="hidden" name="user_id" value="{{ member.id }}">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to remove this user from the team?')">
Remove
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No members in this team yet.</p>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-header">
<h2>Add Team Member</h2>
</div>
<div class="card-body">
{% if available_users %}
<form method="POST" action="{{ url_for('manage_team', team_id=team.id) }}">
<input type="hidden" name="action" value="add_member">
<div class="mb-3">
<label for="user_id" class="form-label">Select User</label>
<select class="form-select" id="user_id" name="user_id" required>
<option value="">-- Select User --</option>
{% for user in available_users %}
<option value="{{ user.id }}">{{ user.username }} ({{ user.email }})</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-success">Add to Team</button>
</form>
{% else %}
<p>No available users to add to this team.</p>
{% endif %}
</div>
</div>
<div class="mt-3">
<a href="{{ url_for('admin_teams') }}" class="btn btn-secondary">Back to Teams</a>
</div>
</div>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -7,12 +7,114 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
<style>
.registration-type {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}
.type-card {
flex: 1;
padding: 1.5rem;
border: 2px solid #e5e7eb;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.type-card:hover {
border-color: #667eea;
background: #f8f9fa;
}
.type-card.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
}
.type-card .icon {
font-size: 2.5rem;
margin-bottom: 0.5rem;
display: block;
}
.type-card h3 {
margin: 0 0 0.5rem 0;
color: #1f2937;
}
.type-card p {
margin: 0;
font-size: 0.875rem;
color: #6b7280;
}
.form-section {
display: none;
}
.form-section.active {
display: block;
}
.company-code-group {
background: #f8f9fa;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.optional-badge {
background: #667eea;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
margin-left: 0.5rem;
}
.benefits-list {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
margin-top: 1.5rem;
}
.benefits-list h4 {
margin: 0 0 1rem 0;
color: #1f2937;
}
.benefits-list ul {
list-style: none;
padding: 0;
margin: 0;
}
.benefits-list li {
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
}
.benefits-list li:before {
content: "✓";
position: absolute;
left: 0;
color: #10b981;
font-weight: bold;
}
</style>
</head>
<body class="auth-page">
<div class="auth-container">
<div class="auth-brand">
<h1>Welcome to {{ g.branding.app_name if g.branding else 'TimeTrack' }}</h1>
<p>Join your company team</p>
<p>Create your account to start tracking time</p>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
@@ -23,22 +125,51 @@
{% endif %}
{% endwith %}
<div class="registration-options mb-4">
<div class="alert alert-info">
<h5>Registration Options:</h5>
<p><strong>Company Employee:</strong> You're on the right page! Enter your company code below.</p>
<p><strong>Freelancer/Independent:</strong> <a href="{{ url_for('register_freelancer') }}" class="btn btn-outline-primary btn-sm">Register as Freelancer</a></p>
</div>
</div>
<form method="POST" action="{{ url_for('register') }}" class="auth-form">
<div class="form-group company-code-group">
<label for="company_code">Company Code</label>
<input type="text" id="company_code" name="company_code" class="form-control" required autofocus
placeholder="ENTER-CODE">
<small class="form-text text-muted">Get this code from your company administrator</small>
<form method="POST" action="{{ url_for('register') }}" class="auth-form" id="registrationForm">
<!-- Registration Type Selection -->
<div class="registration-type">
<div class="type-card active" data-type="company" onclick="selectRegistrationType('company')">
<span class="icon">🏢</span>
<h3>Company Employee</h3>
<p>Join an existing company</p>
</div>
<div class="type-card" data-type="freelancer" onclick="selectRegistrationType('freelancer')">
<span class="icon">💼</span>
<h3>Freelancer</h3>
<p>Create personal workspace</p>
</div>
</div>
<input type="hidden" name="registration_type" id="registration_type" value="company">
<!-- Company Registration Fields -->
<div class="form-section active" id="company-section">
<div class="company-code-group">
<label for="company_code">
Company Code
<span class="optional-badge">Optional</span>
</label>
<input type="text" id="company_code" name="company_code" class="form-control"
placeholder="Enter code or leave blank to create new company">
<small class="form-text text-muted">
Have a company code? Enter it here. No code? Leave blank to create your own company.
<br><strong>Tip:</strong> Ask your admin for an email invitation instead.
</small>
</div>
</div>
<!-- Freelancer Registration Fields -->
<div class="form-section" id="freelancer-section">
<div class="form-group input-icon">
<i>🏢</i>
<input type="text" id="business_name" name="business_name" class="form-control"
placeholder="Your Business Name (optional)">
<label for="business_name">Business Name</label>
<small class="form-text text-muted">Leave blank to use your username as workspace name</small>
</div>
</div>
<!-- Common Fields -->
<div class="form-group input-icon">
<i>👤</i>
<input type="text" id="username" name="username" class="form-control" placeholder="Choose a username" required>
@@ -79,13 +210,67 @@
<p>Already have an account? <a href="{{ url_for('login') }}">Login here</a></p>
</div>
<!-- Dynamic Benefits Section -->
<div class="benefits-list" id="company-benefits">
<h4>What you get:</h4>
<ul>
<li>Join an existing company team or create your own</li>
<li>Collaborate with team members</li>
<li>Track time on company projects</li>
<li>Team management tools (if admin)</li>
<li>Shared reports and analytics</li>
</ul>
</div>
<div class="benefits-list" id="freelancer-benefits" style="display: none;">
<h4>What you get as a freelancer:</h4>
<ul>
<li>Your own personal workspace</li>
<li>Time tracking for your projects</li>
<li>Project management tools</li>
<li>Export capabilities for invoicing</li>
<li>Complete control over your data</li>
</ul>
</div>
<div class="verification-notice">
<p>💡 You can register without an email, but we recommend adding one later for account recovery.</p>
<p>💡 You can register without an email, but we recommend adding one for account recovery.</p>
</div>
</form>
</div>
<script src="{{ url_for('static', filename='js/password-strength.js') }}"></script>
<script src="{{ url_for('static', filename='js/auth-animations.js') }}"></script>
<script>
function selectRegistrationType(type) {
// Update active card
document.querySelectorAll('.type-card').forEach(card => {
card.classList.remove('active');
});
document.querySelector(`[data-type="${type}"]`).classList.add('active');
// Update hidden field
document.getElementById('registration_type').value = type;
// Show/hide sections
if (type === 'company') {
document.getElementById('company-section').classList.add('active');
document.getElementById('freelancer-section').classList.remove('active');
document.getElementById('company-benefits').style.display = 'block';
document.getElementById('freelancer-benefits').style.display = 'none';
// Update form action
document.getElementById('registrationForm').action = "{{ url_for('register') }}";
} else {
document.getElementById('company-section').classList.remove('active');
document.getElementById('freelancer-section').classList.add('active');
document.getElementById('company-benefits').style.display = 'none';
document.getElementById('freelancer-benefits').style.display = 'block';
// Update form action
document.getElementById('registrationForm').action = "{{ url_for('register_freelancer') }}";
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accept Invitation - {{ g.branding.app_name if g.branding else 'TimeTrack' }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
<style>
.invitation-info {
background: linear-gradient(135deg, #667eea10 0%, #764ba210 100%);
border: 2px solid #667eea;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: center;
}
.invitation-company {
font-size: 1.5rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 0.5rem;
}
.invitation-details {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.detail-item {
display: flex;
align-items: center;
gap: 0.5rem;
color: #6b7280;
}
.detail-icon {
font-size: 1.25rem;
}
.welcome-message {
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
margin-bottom: 2rem;
text-align: center;
}
.welcome-message h3 {
margin: 0 0 0.5rem 0;
color: #1f2937;
}
.welcome-message p {
margin: 0;
color: #6b7280;
}
</style>
</head>
<body class="auth-page">
<div class="auth-container">
<div class="auth-brand">
<h1>Welcome to {{ g.branding.app_name if g.branding else 'TimeTrack' }}</h1>
<p>Complete your registration</p>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Invitation Info -->
<div class="invitation-info">
<div class="invitation-company">{{ invitation.company.name }}</div>
<p>You've been invited to join this company</p>
<div class="invitation-details">
<div class="detail-item">
<span class="detail-icon">👤</span>
<span>Role: <strong>{{ invitation.role }}</strong></span>
</div>
<div class="detail-item">
<span class="detail-icon">✉️</span>
<span>Email: <strong>{{ invitation.email }}</strong></span>
</div>
<div class="detail-item">
<span class="detail-icon">👥</span>
<span>Invited by: <strong>{{ invitation.invited_by.username }}</strong></span>
</div>
</div>
</div>
<div class="welcome-message">
<h3>Create Your Account</h3>
<p>Choose a username and password to complete your registration</p>
</div>
<form method="POST" action="{{ url_for('register_with_invitation', token=invitation.token) }}" class="auth-form">
<div class="form-group input-icon">
<i>👤</i>
<input type="text" id="username" name="username" class="form-control"
placeholder="Choose a username" required autofocus>
<label for="username">Username</label>
</div>
<div class="form-group">
<div class="input-icon readonly-field">
<i>📧</i>
<input type="email" value="{{ invitation.email }}" class="form-control" readonly disabled>
<label>Email Address</label>
</div>
<small class="form-text text-muted">This email was used for your invitation</small>
</div>
<div class="form-group input-icon">
<i>🔒</i>
<input type="password" id="password" name="password" class="form-control"
placeholder="Create a strong password" required>
<label for="password">Password</label>
<div id="password-strength" class="password-strength"></div>
</div>
<div class="form-group input-icon">
<i>🔒</i>
<input type="password" id="confirm_password" name="confirm_password" class="form-control"
placeholder="Confirm your password" required>
<label for="confirm_password">Confirm Password</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="terms" required>
<label class="form-check-label" for="terms">
I agree to the Terms of Service and Privacy Policy
</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Create Account & Join {{ invitation.company.name }}</button>
</div>
<div class="auth-links">
<p>Already have an account? <a href="{{ url_for('login') }}">Login here</a></p>
</div>
<div class="verification-notice">
<p>✅ Your email is pre-verified through this invitation</p>
</div>
</form>
</div>
<script src="{{ url_for('static', filename='js/password-strength.js') }}"></script>
<script src="{{ url_for('static', filename='js/auth-animations.js') }}"></script>
<style>
.readonly-field {
opacity: 0.7;
background-color: #f3f4f6;
}
.readonly-field input {
background-color: #f3f4f6 !important;
cursor: not-allowed;
}
</style>
</body>
</html>

View File

@@ -101,7 +101,7 @@
<!-- Form Actions -->
<div class="form-actions">
{% if is_super_admin %}
<a href="{{ url_for('admin_company') }}" class="btn btn-secondary">
<a href="{{ url_for('companies.admin_company') }}" class="btn btn-secondary">
← Back to Dashboard
</a>
{% endif %}

View File

@@ -5,7 +5,7 @@
<div class="header-section">
<h1>{{ "Edit" if announcement else "Create" }} Announcement</h1>
<p class="subtitle">{{ "Update" if announcement else "Create new" }} system announcement for users</p>
<a href="{{ url_for('system_admin_announcements') }}" class="btn btn-secondary">
<a href="{{ url_for('announcements.index') }}" class="btn btn-secondary">
← Back to Announcements
</a>
</div>
@@ -155,7 +155,7 @@
<button type="submit" class="btn btn-primary">
{{ "Update" if announcement else "Create" }} Announcement
</button>
<a href="{{ url_for('system_admin_announcements') }}" class="btn btn-secondary">Cancel</a>
<a href="{{ url_for('announcements.index') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>

View File

@@ -4,7 +4,7 @@
<div class="content-header">
<div class="header-row">
<h1>System Announcements</h1>
<a href="{{ url_for('system_admin_announcement_new') }}" class="btn btn-md btn-primary">
<a href="{{ url_for('announcements.create') }}" class="btn btn-md btn-primary">
<i class="icon"></i> New Announcement
</a>
</div>
@@ -75,11 +75,11 @@
<td>{{ announcement.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="action-buttons">
<a href="{{ url_for('system_admin_announcement_edit', id=announcement.id) }}"
<a href="{{ url_for('announcements.edit', id=announcement.id) }}"
class="btn btn-sm btn-outline-primary" title="Edit">
✏️
</a>
<form method="POST" action="{{ url_for('system_admin_announcement_delete', id=announcement.id) }}"
<form method="POST" action="{{ url_for('announcements.delete', id=announcement.id) }}"
style="display: inline-block;"
onsubmit="return confirm('Are you sure you want to delete this announcement?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
@@ -99,13 +99,13 @@
<div class="pagination-container">
<div class="pagination">
{% if announcements.has_prev %}
<a href="{{ url_for('system_admin_announcements', page=announcements.prev_num) }}" class="page-link">« Previous</a>
<a href="{{ url_for('announcements.index', page=announcements.prev_num) }}" class="page-link">« Previous</a>
{% endif %}
{% for page_num in announcements.iter_pages() %}
{% if page_num %}
{% if page_num != announcements.page %}
<a href="{{ url_for('system_admin_announcements', page=page_num) }}" class="page-link">{{ page_num }}</a>
<a href="{{ url_for('announcements.index', page=page_num) }}" class="page-link">{{ page_num }}</a>
{% else %}
<span class="page-link current">{{ page_num }}</span>
{% endif %}
@@ -115,7 +115,7 @@
{% endfor %}
{% if announcements.has_next %}
<a href="{{ url_for('system_admin_announcements', page=announcements.next_num) }}" class="page-link">Next »</a>
<a href="{{ url_for('announcements.index', page=announcements.next_num) }}" class="page-link">Next »</a>
{% endif %}
</div>
</div>
@@ -125,7 +125,7 @@
<div class="empty-state">
<h3>No announcements found</h3>
<p>Create your first announcement to communicate with users.</p>
<a href="{{ url_for('system_admin_announcement_new') }}" class="btn btn-primary">
<a href="{{ url_for('announcements.create') }}" class="btn btn-primary">
Create Announcement
</a>
</div>

View File

@@ -5,7 +5,7 @@
<div class="management-header">
<h1>🎨 Branding Settings</h1>
<div class="management-actions">
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
</div>
</div>
@@ -179,7 +179,7 @@
<!-- Save Button -->
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 Save Branding Settings</button>
<a href="{{ url_for('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>
</form>
</div>

View File

@@ -5,7 +5,10 @@
<div class="header-section">
<h1>🏢 System Admin - All Companies</h1>
<p class="subtitle">Manage companies across the entire system</p>
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-md btn-secondary">← Back to Dashboard</a>
<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-md btn-secondary">← Back to Dashboard</a>
</div>
</div>
<!-- Companies Table -->
@@ -57,7 +60,7 @@
<td>{{ company.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="action-buttons">
<a href="{{ url_for('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>
</div>
</td>
@@ -72,13 +75,13 @@
<div class="pagination-section">
<div class="pagination">
{% if companies.has_prev %}
<a href="{{ url_for('system_admin_companies', page=companies.prev_num) }}" class="page-link">← Previous</a>
<a href="{{ url_for('system_admin.system_admin_companies', page=companies.prev_num) }}" class="page-link">← Previous</a>
{% endif %}
{% for page_num in companies.iter_pages() %}
{% if page_num %}
{% if page_num != companies.page %}
<a href="{{ url_for('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 %}
<span class="page-link current">{{ page_num }}</span>
{% endif %}
@@ -88,7 +91,7 @@
{% endfor %}
{% if companies.has_next %}
<a href="{{ url_for('system_admin_companies', page=companies.next_num) }}" class="page-link">Next →</a>
<a href="{{ url_for('system_admin.system_admin_companies', page=companies.next_num) }}" class="page-link">Next →</a>
{% endif %}
</div>

View File

@@ -6,8 +6,8 @@
<h1>🏢 {{ company.name }}</h1>
<p class="subtitle">Company Details - System Administrator View</p>
<div class="header-actions">
<a href="{{ url_for('system_admin_companies') }}" class="btn btn-secondary">← Back to Companies</a>
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-secondary">Dashboard</a>
<a href="{{ url_for('system_admin.system_admin_companies') }}" class="btn btn-secondary">← Back to Companies</a>
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">Dashboard</a>
</div>
</div>
@@ -121,7 +121,7 @@
{% endfor %}
{% if users|length > 10 %}
<div class="list-more">
<a href="{{ url_for('system_admin_users', company=company.id) }}" class="btn btn-sm btn-outline">
<a href="{{ url_for('users.system_admin_users', company=company.id) }}" class="btn btn-sm btn-outline">
View All {{ users|length }} Users →
</a>
</div>
@@ -195,14 +195,14 @@
<div class="actions-section">
<h3>🛠️ Management Actions</h3>
<div class="actions-grid">
<a href="{{ url_for('system_admin_users', company=company.id) }}" class="action-card">
<a href="{{ url_for('users.system_admin_users', company=company.id) }}" class="action-card">
<div class="action-icon">👥</div>
<div class="action-content">
<h4>Manage Users</h4>
<p>View and edit all users in this company</p>
</div>
</a>
<a href="{{ url_for('system_admin_time_entries', company=company.id) }}" class="action-card">
<a href="{{ url_for('system_admin.system_admin_time_entries', company=company.id) }}" class="action-card">
<div class="action-icon">⏱️</div>
<div class="action-content">
<h4>View Time Entries</h4>
@@ -211,6 +211,23 @@
</a>
</div>
</div>
<!-- Danger Zone -->
<div class="danger-section">
<h3>⚠️ Danger Zone</h3>
<div class="danger-content">
<p><strong>Delete Company</strong></p>
<p>Once you delete a company, there is no going back. This will permanently delete:</p>
<ul>
<li>All users in the company</li>
<li>All projects, tasks, and time entries</li>
<li>All teams and settings</li>
</ul>
<form method="POST" action="{{ url_for('system_admin.delete_company', company_id=company.id) }}" onsubmit="return confirm('Are you absolutely sure you want to delete {{ company.name }}? This action cannot be undone!');">
<button type="submit" class="btn btn-danger">Delete This Company</button>
</form>
</div>
</div>
</div>
<style>
@@ -552,5 +569,43 @@
.text-muted {
color: #6c757d;
}
/* Danger Zone */
.danger-section {
margin-top: 3rem;
padding: 1.5rem;
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
}
.danger-section h3 {
color: #dc2626;
margin-top: 0;
margin-bottom: 1rem;
}
.danger-content {
color: #7f1d1d;
}
.danger-content p {
margin-bottom: 0.5rem;
}
.danger-content ul {
margin: 1rem 0 1.5rem 2rem;
}
.danger-content .btn-danger {
background-color: #dc2626;
color: white;
border: none;
padding: 0.5rem 1.5rem;
}
.danger-content .btn-danger:hover {
background-color: #b91c1c;
}
</style>
{% endblock %}

View File

@@ -12,12 +12,12 @@
<div class="stat-card">
<h3>{{ total_companies }}</h3>
<p>Total Companies</p>
<a href="{{ url_for('system_admin_companies') }}" class="stat-link">Manage →</a>
<a href="{{ url_for('system_admin.system_admin_companies') }}" class="stat-link">Manage →</a>
</div>
<div class="stat-card">
<h3>{{ total_users }}</h3>
<p>Total Users</p>
<a href="{{ url_for('system_admin_users') }}" class="stat-link">Manage →</a>
<a href="{{ url_for('users.system_admin_users') }}" class="stat-link">Manage →</a>
</div>
<div class="stat-card">
<h3>{{ total_teams }}</h3>
@@ -30,7 +30,7 @@
<div class="stat-card">
<h3>{{ total_time_entries }}</h3>
<p>Time Entries</p>
<a href="{{ url_for('system_admin_time_entries') }}" class="stat-link">View →</a>
<a href="{{ url_for('system_admin.system_admin_time_entries') }}" class="stat-link">View →</a>
</div>
</div>
</div>
@@ -51,7 +51,7 @@
<h3>{{ blocked_users }}</h3>
<p>Blocked Users</p>
{% if blocked_users > 0 %}
<a href="{{ url_for('system_admin_users', filter='blocked') }}" class="stat-link">Review →</a>
<a href="{{ url_for('users.system_admin_users', filter='blocked') }}" class="stat-link">Review →</a>
{% endif %}
</div>
</div>
@@ -118,7 +118,7 @@
<td>{{ company.name }}</td>
<td>{{ company.user_count }}</td>
<td>
<a href="{{ url_for('system_admin_company_detail', company_id=company.id) }}" class="btn btn-sm">View</a>
<a href="{{ url_for('system_admin.system_admin_company_detail', company_id=company.id) }}" class="btn btn-sm">View</a>
</td>
</tr>
{% endfor %}
@@ -155,7 +155,7 @@
{% endif %}
</td>
<td>
<a href="{{ url_for('system_admin_company_detail', company_id=company.id) }}" class="btn btn-sm">View</a>
<a href="{{ url_for('system_admin.system_admin_company_detail', company_id=company.id) }}" class="btn btn-sm">View</a>
</td>
</tr>
{% endfor %}
@@ -171,22 +171,22 @@
<div class="admin-panel">
<h2>🛠️ System Management</h2>
<div class="admin-actions">
<a href="{{ url_for('system_admin_users') }}" class="btn btn-primary">
<a href="{{ url_for('users.system_admin_users') }}" class="btn btn-primary">
👥 Manage All Users
</a>
<a href="{{ url_for('system_admin_companies') }}" class="btn btn-primary">
<a href="{{ url_for('system_admin.system_admin_companies') }}" class="btn btn-primary">
🏢 Manage Companies
</a>
<a href="{{ url_for('system_admin_time_entries') }}" class="btn btn-primary">
<a href="{{ url_for('system_admin.system_admin_time_entries') }}" class="btn btn-primary">
⏱️ View Time Entries
</a>
<a href="{{ url_for('system_admin_settings') }}" class="btn btn-primary">
<a href="{{ url_for('system_admin.system_admin_settings') }}" class="btn btn-primary">
⚙️ System Settings
</a>
<a href="{{ url_for('system_admin_branding') }}" class="btn btn-primary">
<a href="{{ url_for('system_admin.branding') }}" class="btn btn-primary">
🎨 Branding Settings
</a>
<a href="{{ url_for('system_admin_health') }}" class="btn btn-primary">
<a href="{{ url_for('system_admin.system_admin_health') }}" class="btn btn-primary">
🏥 System Health
</a>
</div>

View File

@@ -5,7 +5,7 @@
<div class="header-section">
<h1>✏️ Edit User: {{ user.username }}</h1>
<p class="subtitle">System Administrator - Edit user across companies</p>
<a href="{{ url_for('system_admin_users') }}" class="btn btn-secondary">← Back to Users</a>
<a href="{{ url_for('users.system_admin_users') }}" class="btn btn-secondary">← Back to Users</a>
</div>
<div class="form-container">
@@ -142,13 +142,13 @@
<!-- Form Actions -->
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="{{ url_for('system_admin_users') }}" class="btn btn-secondary">Cancel</a>
<a href="{{ url_for('users.system_admin_users') }}" class="btn btn-secondary">Cancel</a>
{% if user.id != g.user.id and not (user.role == Role.SYSTEM_ADMIN and user.id == g.user.id) %}
<div class="danger-zone">
<h4>Danger Zone</h4>
<p>Permanently delete this user account. This action cannot be undone.</p>
<form method="POST" action="{{ url_for('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.')">
<button type="submit" class="btn btn-danger">Delete User</button>
@@ -160,6 +160,36 @@
</div>
</div>
<script>
// Dynamic team loading when company changes
document.getElementById('company_id').addEventListener('change', function() {
const companyId = this.value;
const teamSelect = document.getElementById('team_id');
// Clear current teams
teamSelect.innerHTML = '<option value="">No Team</option>';
if (companyId) {
// Fetch teams for the selected company
fetch(`/api/teams?company_id=${companyId}`)
.then(response => response.json())
.then(data => {
if (data.success && data.teams) {
data.teams.forEach(team => {
const option = document.createElement('option');
option.value = team.id;
option.textContent = team.name;
teamSelect.appendChild(option);
});
}
})
.catch(error => {
console.error('Error fetching teams:', error);
});
}
});
</script>
<style>
.header-section {
margin-bottom: 2rem;

View File

@@ -5,7 +5,7 @@
<div class="header-section">
<h1>🏥 System Health Check</h1>
<p class="subtitle">System diagnostics and event monitoring</p>
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
</div>
<!-- System Health Status -->

View File

@@ -5,7 +5,7 @@
<div class="header-section">
<h1>⚙️ System Administrator Settings</h1>
<p class="subtitle">Global system configuration and management</p>
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
</div>
<!-- System Statistics -->
@@ -108,7 +108,7 @@
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Settings</button>
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-secondary">Cancel</a>
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
@@ -155,7 +155,7 @@
<p>Run diagnostic checks to identify potential issues with the system.</p>
</div>
<div class="danger-actions">
<a href="{{ url_for('system_admin_health') }}" class="btn btn-warning">
<a href="{{ url_for('system_admin.system_admin_health') }}" class="btn btn-warning">
Run Health Check
</a>
</div>
@@ -167,7 +167,7 @@
<div class="actions-section">
<h3>🚀 Quick Actions</h3>
<div class="actions-grid">
<a href="{{ url_for('system_admin_users') }}" class="action-card">
<a href="{{ url_for('users.system_admin_users') }}" class="action-card">
<div class="action-icon">👥</div>
<div class="action-content">
<h4>Manage All Users</h4>
@@ -175,7 +175,7 @@
</div>
</a>
<a href="{{ url_for('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-content">
<h4>Manage Companies</h4>
@@ -183,7 +183,7 @@
</div>
</a>
<a href="{{ url_for('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-content">
<h4>View Time Entries</h4>

View File

@@ -5,7 +5,7 @@
<div class="header-section">
<h1>⏱️ System Admin - Time Entries</h1>
<p class="subtitle">View time entries across all companies</p>
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
</div>
<!-- Filter Section -->
@@ -25,7 +25,7 @@
</select>
</div>
{% if current_company %}
<a href="{{ url_for('system_admin_time_entries') }}" class="btn btn-sm btn-outline">Clear Filter</a>
<a href="{{ url_for('system_admin.system_admin_time_entries') }}" class="btn btn-sm btn-outline">Clear Filter</a>
{% endif %}
</form>
</div>
@@ -112,13 +112,13 @@
<div class="pagination-section">
<div class="pagination">
{% if entries.has_prev %}
<a href="{{ url_for('system_admin_time_entries', page=entries.prev_num, company=current_company) }}" class="page-link">← Previous</a>
<a href="{{ url_for('system_admin.system_admin_time_entries', page=entries.prev_num, company=current_company) }}" class="page-link">← Previous</a>
{% endif %}
{% for page_num in entries.iter_pages() %}
{% if page_num %}
{% if page_num != entries.page %}
<a href="{{ url_for('system_admin_time_entries', page=page_num, company=current_company) }}" class="page-link">{{ page_num }}</a>
<a href="{{ url_for('system_admin.system_admin_time_entries', page=page_num, company=current_company) }}" class="page-link">{{ page_num }}</a>
{% else %}
<span class="page-link current">{{ page_num }}</span>
{% endif %}
@@ -128,7 +128,7 @@
{% endfor %}
{% if entries.has_next %}
<a href="{{ url_for('system_admin_time_entries', page=entries.next_num, company=current_company) }}" class="page-link">Next →</a>
<a href="{{ url_for('system_admin.system_admin_time_entries', page=entries.next_num, company=current_company) }}" class="page-link">Next →</a>
{% endif %}
</div>
@@ -170,7 +170,6 @@
<div class="summary-card">
<h4>Completed Today</h4>
<p class="summary-number">
{% set today = moment().date() %}
{{ entries.items | selectattr('0.arrival_time') | selectattr('0.departure_time', 'defined') |
list | length }}
</p>

View File

@@ -5,34 +5,34 @@
<div class="header-section">
<h1>👥 System Admin - All Users</h1>
<p class="subtitle">Manage users across all companies</p>
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-md btn-secondary">← Back to Dashboard</a>
<a href="{{ url_for('system_admin.system_admin_dashboard') }}" class="btn btn-md btn-secondary">← Back to Dashboard</a>
</div>
<!-- Filter Options -->
<div class="filter-section">
<h3>Filter Users</h3>
<div class="filter-buttons">
<a href="{{ url_for('system_admin_users') }}"
<a href="{{ url_for('users.system_admin_users') }}"
class="btn btn-filter {% if not current_filter %}active{% endif %}">
All Users ({{ users.total }})
</a>
<a href="{{ url_for('system_admin_users', filter='system_admins') }}"
<a href="{{ url_for('users.system_admin_users', filter='system_admins') }}"
class="btn btn-filter {% if current_filter == 'system_admins' %}active{% endif %}">
System Admins
</a>
<a href="{{ url_for('system_admin_users', filter='admins') }}"
<a href="{{ url_for('users.system_admin_users', filter='admins') }}"
class="btn btn-filter {% if current_filter == 'admins' %}active{% endif %}">
Company Admins
</a>
<a href="{{ url_for('system_admin_users', filter='blocked') }}"
<a href="{{ url_for('users.system_admin_users', filter='blocked') }}"
class="btn btn-filter {% if current_filter == 'blocked' %}active{% endif %}">
Blocked Users
</a>
<a href="{{ url_for('system_admin_users', filter='unverified') }}"
<a href="{{ url_for('users.system_admin_users', filter='unverified') }}"
class="btn btn-filter {% if current_filter == 'unverified' %}active{% endif %}">
Unverified
</a>
<a href="{{ url_for('system_admin_users', filter='freelancers') }}"
<a href="{{ url_for('users.system_admin_users', filter='freelancers') }}"
class="btn btn-filter {% if current_filter == 'freelancers' %}active{% endif %}">
Freelancers
</a>
@@ -57,8 +57,14 @@
</thead>
<tbody>
{% for user_data in users.items %}
{% set user = user_data[0] %}
{% set company_name = user_data[1] %}
{% if user_data is sequence and user_data|length == 2 %}
{% set user = user_data[0] %}
{% set company_name = user_data[1] %}
{% else %}
{# Fallback for when data structure is unexpected #}
{% set user = user_data %}
{% set company_name = user.company.name if user.company else 'Unknown' %}
{% endif %}
<tr class="{% if user.is_blocked %}blocked-user{% endif %}">
<td>
<strong>{{ user.username }}</strong>
@@ -95,11 +101,11 @@
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<div class="action-buttons">
<a href="{{ url_for('system_admin_edit_user', user_id=user.id) }}"
<a href="{{ url_for('users.system_admin_edit_user', user_id=user.id) }}"
class="btn btn-sm btn-primary">Edit</a>
{% if user.id != g.user.id and not (user.role == Role.SYSTEM_ADMIN and user.id == g.user.id) %}
<form method="POST" action="{{ url_for('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 }}\' from company \'{{ company_name }}\'? This action cannot be undone.')">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
@@ -118,13 +124,13 @@
<div class="pagination-section">
<div class="pagination">
{% if users.has_prev %}
<a href="{{ url_for('system_admin_users', page=users.prev_num, filter=current_filter) }}" class="page-link">← Previous</a>
<a href="{{ url_for('users.system_admin_users', page=users.prev_num, filter=current_filter) }}" class="page-link">← Previous</a>
{% endif %}
{% for page_num in users.iter_pages() %}
{% if page_num %}
{% if page_num != users.page %}
<a href="{{ url_for('system_admin_users', page=page_num, filter=current_filter) }}" class="page-link">{{ page_num }}</a>
<a href="{{ url_for('users.system_admin_users', page=page_num, filter=current_filter) }}" class="page-link">{{ page_num }}</a>
{% else %}
<span class="page-link current">{{ page_num }}</span>
{% endif %}
@@ -134,7 +140,7 @@
{% endfor %}
{% if users.has_next %}
<a href="{{ url_for('system_admin_users', page=users.next_num, filter=current_filter) }}" class="page-link">Next →</a>
<a href="{{ url_for('users.system_admin_users', page=users.next_num, filter=current_filter) }}" class="page-link">Next →</a>
{% endif %}
</div>

View File

@@ -36,10 +36,11 @@
<div class="form-group">
<label for="task-status">Status</label>
<select id="task-status">
<option value="NOT_STARTED">Not Started</option>
<option value="TODO">To Do</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="ON_HOLD">On Hold</option>
<option value="COMPLETED">Completed</option>
<option value="IN_REVIEW">In Review</option>
<option value="DONE">Done</option>
<option value="CANCELLED">Cancelled</option>
<option value="ARCHIVED">Archived</option>
</select>
</div>

694
templates/team_form.html Normal file
View File

@@ -0,0 +1,694 @@
{% extends 'layout.html' %}
{% block content %}
<div class="team-form-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">
<span class="team-icon">👥</span>
{% if team %}
{{ team.name }}
{% else %}
Create New Team
{% endif %}
</h1>
<p class="page-subtitle">
{% if team %}
Manage team details and members
{% else %}
Set up a new team for your organization
{% endif %}
</p>
</div>
<div class="header-actions">
<a href="{{ url_for('teams.admin_teams') }}" class="btn btn-outline">
<i class="icon"></i> Back to Teams
</a>
</div>
</div>
</div>
<!-- Main Content Grid -->
<div class="content-grid">
<!-- Left Column: Team Details -->
<div class="content-column">
<!-- Team Details Card -->
<div class="card team-details-card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">📝</span>
Team Details
</h2>
</div>
<div class="card-body">
<form method="POST" action="{% if team %}{{ url_for('teams.manage_team', team_id=team.id) }}{% else %}{{ url_for('teams.create_team') }}{% endif %}" class="modern-form">
{% if team %}
<input type="hidden" name="action" value="update_team">
{% endif %}
<div class="form-group">
<label for="name" class="form-label">Team Name</label>
<input type="text"
class="form-control"
id="name"
name="name"
value="{{ team.name if team else '' }}"
placeholder="Enter team name"
required>
<span class="form-hint">Choose a descriptive name for your team</span>
</div>
<div class="form-group">
<label for="description" class="form-label">Description</label>
<textarea class="form-control"
id="description"
name="description"
rows="4"
placeholder="Describe the team's purpose and responsibilities...">{{ team.description if team else '' }}</textarea>
<span class="form-hint">Optional: Add details about this team's role</span>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon"></span>
{% if team %}Save Changes{% else %}Create Team{% endif %}
</button>
{% if not team %}
<a href="{{ url_for('teams.admin_teams') }}" class="btn btn-outline">Cancel</a>
{% endif %}
</div>
</form>
</div>
</div>
{% if team %}
<!-- Team Statistics -->
<div class="card stats-card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">📊</span>
Team Statistics
</h2>
</div>
<div class="card-body">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{{ team_members|length if team_members else 0 }}</div>
<div class="stat-label">Team Members</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ team.projects|length if team.projects else 0 }}</div>
<div class="stat-label">Active Projects</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ team.created_at.strftime('%b %Y') if team.created_at else 'N/A' }}</div>
<div class="stat-label">Created</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<!-- Right Column: Team Members (only for existing teams) -->
{% if team %}
<div class="content-column">
<!-- Current Members Card -->
<div class="card members-card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">👤</span>
Team Members
</h2>
<span class="member-count">{{ team_members|length if team_members else 0 }} members</span>
</div>
<div class="card-body">
{% if team_members %}
<div class="members-list">
{% for member in team_members %}
<div class="member-item">
<div class="member-avatar">
{{ member.username[:2].upper() }}
</div>
<div class="member-info">
<div class="member-name">{{ member.username }}</div>
<div class="member-details">
<span class="member-email">{{ member.email }}</span>
<span class="member-role role-badge role-{{ member.role.name.lower() }}">
{{ member.role.value }}
</span>
</div>
</div>
<div class="member-actions">
<form method="POST" action="{{ url_for('teams.manage_team', team_id=team.id) }}" class="remove-form">
<input type="hidden" name="action" value="remove_member">
<input type="hidden" name="user_id" value="{{ member.id }}">
<button type="submit"
class="btn-icon btn-danger"
onclick="return confirm('Remove {{ member.username }} from the team?')"
title="Remove from team">
<span class="icon">×</span>
</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">👥</div>
<p class="empty-message">No members in this team yet</p>
<p class="empty-hint">Add members using the form below</p>
</div>
{% endif %}
</div>
</div>
<!-- Add Member Card -->
<div class="card add-member-card">
<div class="card-header">
<h2 class="card-title">
<span class="icon"></span>
Add Team Member
</h2>
</div>
<div class="card-body">
{% if available_users %}
<form method="POST" action="{{ url_for('teams.manage_team', team_id=team.id) }}" class="modern-form">
<input type="hidden" name="action" value="add_member">
<div class="form-group">
<label for="user_id" class="form-label">Select User</label>
<select class="form-control form-select" id="user_id" name="user_id" required>
<option value="">Choose a user to add...</option>
{% for user in available_users %}
<option value="{{ user.id }}">
{{ user.username }} - {{ user.email }}
{% if user.role %}({{ user.role.value }}){% endif %}
</option>
{% endfor %}
</select>
<span class="form-hint">Only users not already in a team are shown</span>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-success">
<span class="icon">+</span>
Add to Team
</button>
</div>
</form>
{% else %}
<div class="empty-state">
<div class="empty-icon"></div>
<p class="empty-message">All users are assigned</p>
<p class="empty-hint">No available users to add to this team</p>
</div>
{% endif %}
</div>
</div>
</div>
{% else %}
<!-- Placeholder for new teams -->
<div class="content-column">
<div class="card info-card">
<div class="card-body">
<div class="info-content">
<div class="info-icon">💡</div>
<h3>Team Members</h3>
<p>After creating the team, you'll be able to add members and manage team composition from this page.</p>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<style>
/* Container and Layout */
.team-form-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: 2.5rem;
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: 1.5rem;
}
.header-left {
flex: 1;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;
gap: 1rem;
}
.team-icon {
font-size: 3rem;
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;
}
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
gap: 2rem;
margin-bottom: 2rem;
}
@media (max-width: 1024px) {
.content-grid {
grid-template-columns: 1fr;
}
}
/* 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;
display: flex;
justify-content: space-between;
align-items: center;
}
.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;
}
/* Info Card for New Teams */
.info-card {
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
border: none;
}
.info-content {
text-align: center;
padding: 2rem;
}
.info-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.info-content h3 {
color: #1f2937;
margin-bottom: 0.5rem;
}
.info-content p {
color: #6b7280;
font-size: 1.05rem;
line-height: 1.6;
}
/* Forms */
.modern-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-label {
font-weight: 600;
color: #374151;
font-size: 0.95rem;
}
.form-control {
padding: 0.75rem 1rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1rem;
transition: all 0.2s ease;
background-color: #f9fafb;
}
.form-control:focus {
outline: none;
border-color: #667eea;
background-color: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-select {
cursor: pointer;
}
.form-hint {
font-size: 0.875rem;
color: #6b7280;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 0.5rem;
}
/* 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-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.btn-outline {
background: white;
color: #6b7280;
border: 2px solid #e5e7eb;
}
.btn-outline:hover {
background: #f3f4f6;
border-color: #d1d5db;
}
.btn-icon {
width: 36px;
height: 36px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-danger {
background: #fee2e2;
color: #dc2626;
}
.btn-danger:hover {
background: #dc2626;
color: white;
}
/* Team Statistics */
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.stat-item {
text-align: center;
padding: 1.5rem;
background: #f8f9fa;
border-radius: 8px;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #667eea;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.875rem;
color: #6b7280;
font-weight: 500;
}
/* Members List */
.members-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.member-item {
display: flex;
align-items: center;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
transition: all 0.2s ease;
}
.member-item:hover {
background: #f3f4f6;
transform: translateX(4px);
}
.member-avatar {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 1.1rem;
margin-right: 1rem;
}
.member-info {
flex: 1;
}
.member-name {
font-weight: 600;
color: #1f2937;
font-size: 1.05rem;
}
.member-details {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 0.25rem;
}
.member-email {
color: #6b7280;
font-size: 0.875rem;
}
.member-count {
background: #e5e7eb;
color: #6b7280;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
}
/* Role Badges */
.role-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.role-team_member {
background: #dbeafe;
color: #1e40af;
}
.role-team_leader {
background: #fef3c7;
color: #92400e;
}
.role-supervisor {
background: #ede9fe;
color: #5b21b6;
}
.role-admin {
background: #fee2e2;
color: #991b1b;
}
.role-system_admin {
background: #fce7f3;
color: #be185d;
}
/* Empty States */
.empty-state {
text-align: center;
padding: 3rem 1.5rem;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.3;
}
.empty-message {
font-size: 1.1rem;
color: #1f2937;
margin-bottom: 0.5rem;
}
.empty-hint {
color: #6b7280;
font-size: 0.875rem;
}
/* Remove Form */
.remove-form {
margin: 0;
}
/* Responsive Design */
@media (max-width: 768px) {
.team-form-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.page-title {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.member-details {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
/* Animation */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
}
.card:nth-child(2) {
animation-delay: 0.1s;
}
.card:nth-child(3) {
animation-delay: 0.2s;
}
</style>
{% endblock %}

1235
templates/time_tracking.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -52,12 +52,12 @@
<!-- Task Board -->
<div class="task-board" id="task-board">
<div class="task-column" data-status="NOT_STARTED">
<div class="task-column" data-status="TODO">
<div class="column-header">
<h3>📝 Not Started</h3>
<h3>📝 To Do</h3>
<span class="task-count">0</span>
</div>
<div class="column-content" id="column-NOT_STARTED">
<div class="column-content" id="column-TODO">
<!-- Task cards will be populated here -->
</div>
</div>
@@ -72,22 +72,32 @@
</div>
</div>
<div class="task-column" data-status="ON_HOLD">
<div class="task-column" data-status="IN_REVIEW">
<div class="column-header">
<h3>⏸️ On Hold</h3>
<h3>🔍 In Review</h3>
<span class="task-count">0</span>
</div>
<div class="column-content" id="column-ON_HOLD">
<div class="column-content" id="column-IN_REVIEW">
<!-- Task cards will be populated here -->
</div>
</div>
<div class="task-column" data-status="COMPLETED">
<div class="task-column" data-status="DONE">
<div class="column-header">
<h3>Completed</h3>
<h3>Done</h3>
<span class="task-count">0</span>
</div>
<div class="column-content" id="column-COMPLETED">
<div class="column-content" id="column-DONE">
<!-- Task cards will be populated here -->
</div>
</div>
<div class="task-column" data-status="CANCELLED">
<div class="column-header">
<h3>❌ Cancelled</h3>
<span class="task-count">0</span>
</div>
<div class="column-content" id="column-CANCELLED">
<!-- Task cards will be populated here -->
</div>
</div>
@@ -875,10 +885,16 @@ class SmartTaskSearch {
normalizeStatus(status) {
const statusMap = {
'not-started': 'NOT_STARTED',
'not-started': 'TODO',
'to-do': 'TODO',
'todo': 'TODO',
'in-progress': 'IN_PROGRESS',
'on-hold': 'ON_HOLD',
'completed': 'COMPLETED'
'in-review': 'IN_REVIEW',
'on-hold': 'IN_REVIEW',
'completed': 'DONE',
'done': 'DONE',
'cancelled': 'CANCELLED',
'archived': 'ARCHIVED'
};
return statusMap[status.toLowerCase()] || status.toUpperCase();
}
@@ -1499,10 +1515,11 @@ class UnifiedTaskManager {
// Group tasks by status
const tasksByStatus = {
'NOT_STARTED': [],
'TODO': [],
'IN_PROGRESS': [],
'ON_HOLD': [],
'COMPLETED': [],
'IN_REVIEW': [],
'DONE': [],
'CANCELLED': [],
'ARCHIVED': []
};
@@ -2300,9 +2317,28 @@ class UnifiedTaskManager {
try {
const response = await fetch(`/api/comments/${commentId}`, {
method: 'DELETE'
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
// Check if response is JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
console.error('Response is not JSON:', await response.text());
if (response.status === 401) {
alert('Your session has expired. Please refresh the page and login again.');
window.location.href = '/login';
return;
} else if (response.status === 404) {
alert('Comment not found. It may have been already deleted.');
} else {
alert('Failed to delete comment. Please try again.');
}
return;
}
const data = await response.json();
if (data.success) {
const taskId = document.getElementById('task-id').value;