commit 1eeea9f83ad9230a5c1f7a75662770eaab0df837 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 21:15:41 2025 +0200 Disable resuming of old time entries. commit 3e3ec2f01cb7943622b819a19179388078ae1315 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 20:59:19 2025 +0200 Refactor db migrations. commit 15a51a569da36c6b7c9e01ab17b6fdbdee6ad994 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 19:58:04 2025 +0200 Apply new style for Time Tracking view. commit 77e5278b303e060d2b03853b06277f8aa567ae68 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 18:06:04 2025 +0200 Allow direct registrations as a Company. commit 188a8772757cbef374243d3a5f29e4440ddecabe Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 18:04:45 2025 +0200 Add email invitation feature. commit d9ebaa02aa01b518960a20dccdd5a327d82f30c6 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 17:12:32 2025 +0200 Apply common style for Company, User, Team management pages. commit 81149caf4d8fc6317e2ab1b4f022b32fc5aa6d22 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 16:44:32 2025 +0200 Move export functions to own module. commit 1a26e19338e73f8849c671471dd15cc3c1b1fe82 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 15:51:15 2025 +0200 Split up models.py. commit 61f1ccd10f721b0ff4dc1eccf30c7a1ee13f204d Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 12:05:28 2025 +0200 Move utility function into own modules. commit 84b341ed35e2c5387819a8b9f9d41eca900ae79f Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 11:44:24 2025 +0200 Refactor auth functions use. commit 923e311e3da5b26d85845c2832b73b7b17c48adb Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 11:35:52 2025 +0200 Refactor route nameing and fix bugs along the way. commit f0a5c4419c340e62a2615c60b2a9de28204d2995 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 10:34:33 2025 +0200 Fix URL endpoints in announcement template. commit b74d74542a1c8dc350749e4788a9464d067a88b5 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 09:25:53 2025 +0200 Move announcements to own module. commit 9563a28021ac46c82c04fe4649b394dbf96f92c7 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 09:16:30 2025 +0200 Combine Company view and edit templates. commit 6687c373e681d54e4deab6b2582fed5cea9aadf6 Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 08:17:42 2025 +0200 Move Users, Company and System Administration to own modules. commit 8b7894a2e3eb84bb059f546648b6b9536fea724e Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 07:40:57 2025 +0200 Move Teams and Projects to own modules. commit d11bf059d99839ecf1f5d7020b8c8c8a2454c00b Author: Jens Luedicke <jens@luedicke.me> Date: Mon Jul 7 07:09:33 2025 +0200 Move Tasks and Sprints to own modules.
694 lines
18 KiB
HTML
694 lines
18 KiB
HTML
{% 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 %} |