Merge branch 'master' into feature-markdown-notes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
152
templates/emails/invitation.html
Normal file
152
templates/emails/invitation.html
Normal 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>© {{ g.branding.app_name }} - Time Tracking Made Simple</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
108
templates/emails/invitation_reminder.html
Normal file
108
templates/emails/invitation_reminder.html
Normal 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>© {{ g.branding.app_name }} - Time Tracking Made Simple</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
|
||||
1634
templates/index.html
1634
templates/index.html
File diff suppressed because it is too large
Load Diff
938
templates/index_old.html
Normal file
938
templates/index_old.html
Normal 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">×</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">×</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">×</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 %}
|
||||
473
templates/invitations/list.html
Normal file
473
templates/invitations/list.html
Normal 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 %}
|
||||
378
templates/invitations/send.html
Normal file
378
templates/invitations/send.html
Normal 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 }} <{{ g.branding.email_from or 'noreply@timetrack.com' }}>
|
||||
</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 %}
|
||||
@@ -129,32 +129,31 @@
|
||||
<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('notes.notes_list') }}" data-tooltip="Notes"><i class="nav-icon">📝</i><span class="nav-text">Notes</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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
172
templates/register_invitation.html
Normal file
172
templates/register_invitation.html
Normal 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>
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
694
templates/team_form.html
Normal 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
1235
templates/time_tracking.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user