Files
TimeTrack/templates/admin_company.html
Jens Luedicke 9a79778ad6 Squashed commit of the following:
commit 1eeea9f83ad9230a5c1f7a75662770eaab0df837
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 21:15:41 2025 +0200

    Disable resuming of old time entries.

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

    Refactor db migrations.

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

    Apply new style for Time Tracking view.

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

    Allow direct registrations as a Company.

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

    Add email invitation feature.

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

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

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

    Move export functions to own module.

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

    Split up models.py.

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

    Move utility function into own modules.

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

    Refactor auth functions use.

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

    Refactor route nameing and fix bugs along the way.

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

    Fix URL endpoints in announcement template.

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

    Move announcements to own module.

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

    Combine Company view and edit templates.

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

    Move Users, Company and System Administration to own modules.

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

    Move Teams and Projects to own modules.

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

    Move Tasks and Sprints to own modules.
2025-07-07 21:16:36 +02:00

897 lines
27 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.html" %}
{% block content %}
<div class="company-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>
Company Management
</h1>
<p class="page-subtitle">Configure your company settings and policies</p>
</div>
<div class="header-actions">
<a href="{{ url_for('setup_company') }}" class="btn btn-primary">
<span class="icon">+</span>
Create New Company
</a>
</div>
</div>
</div>
<!-- Company Statistics -->
<div class="stats-section">
<div class="stat-card">
<div class="stat-value">{{ stats.total_users }}</div>
<div class="stat-label">Total Users</div>
<a href="{{ url_for('companies.company_users') }}" class="stat-link">View all →</a>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.total_teams }}</div>
<div class="stat-label">Teams</div>
<a href="{{ url_for('teams.admin_teams') }}" class="stat-link">Manage →</a>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.total_projects }}</div>
<div class="stat-label">Total Projects</div>
<a href="{{ url_for('projects.admin_projects') }}" class="stat-link">View all →</a>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.active_projects }}</div>
<div class="stat-label">Active Projects</div>
<a href="{{ url_for('projects.admin_projects') }}" class="stat-link">Manage →</a>
</div>
</div>
<!-- Main Content Grid -->
<div class="content-grid">
<!-- Left Column -->
<div class="content-column">
<!-- Company Information Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span class="icon"></span>
Company Information
</h2>
</div>
<div class="card-body">
<form method="POST" class="modern-form">
<input type="hidden" name="action" value="update_company_details">
<div class="form-group">
<label for="name" class="form-label">Company Name</label>
<input type="text" id="name" name="name" class="form-control"
value="{{ company.name }}" required>
<span class="form-hint">The official name of your company</span>
</div>
<div class="form-row">
<div class="form-group">
<label for="max_users" class="form-label">Maximum Users</label>
<input type="number" id="max_users" name="max_users" class="form-control"
value="{{ company.max_users or '' }}" min="1" placeholder="Unlimited">
<span class="form-hint">Leave empty for unlimited</span>
</div>
<div class="form-group">
<label class="form-label">Status</label>
<div class="toggle-container">
<label class="toggle-switch">
<input type="checkbox" id="is_active" name="is_active"
{{ 'checked' if company.is_active else '' }}>
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Company is {{ 'active' if company.is_active else 'inactive' }}</span>
</div>
</div>
</div>
<div class="form-group">
<label for="description" class="form-label">Description</label>
<textarea id="description" name="description" class="form-control"
rows="3" placeholder="Brief description of your company...">{{ company.description or '' }}</textarea>
<span class="form-hint">Optional: Describe your company's mission or purpose</span>
</div>
<div class="info-panel">
<div class="info-item">
<span class="info-icon">🔑</span>
<div class="info-content">
<label class="info-label">Company Code</label>
<div class="code-display">
<input type="text" value="{{ company.slug }}" readonly id="companyCode" class="code-input">
<button type="button" class="btn btn-copy" onclick="copyCompanyCode()">
<span id="copyIcon">📋</span>
<span id="copyText">Copy</span>
</button>
</div>
<span class="info-hint">Legacy: Use email invitations instead</span>
</div>
</div>
<div class="info-item">
<span class="info-icon">📅</span>
<div class="info-content">
<label class="info-label">Created</label>
<span class="info-value">{{ company.created_at.strftime('%B %d, %Y at %I:%M %p') }}</span>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon"></span>
Save Company Details
</button>
</div>
</form>
</div>
</div>
<!-- Quick Actions Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span class="icon"></span>
Quick Actions
</h2>
</div>
<div class="card-body">
<div class="action-grid">
<a href="{{ url_for('users.admin_users') }}" class="action-item">
<div class="action-icon">👥</div>
<div class="action-content">
<h3>Manage Users</h3>
<p>User accounts & permissions</p>
</div>
</a>
<a href="{{ url_for('teams.admin_teams') }}" class="action-item">
<div class="action-icon">👨‍👩‍👧‍👦</div>
<div class="action-content">
<h3>Manage Teams</h3>
<p>Organize company structure</p>
</div>
</a>
<a href="{{ url_for('projects.admin_projects') }}" class="action-item">
<div class="action-icon">📁</div>
<div class="action-content">
<h3>Manage Projects</h3>
<p>Time tracking projects</p>
</div>
</a>
<a href="{{ url_for('invitations.send_invitation') }}" class="action-item">
<div class="action-icon">📨</div>
<div class="action-content">
<h3>Send Invitation</h3>
<p>Invite team members</p>
</div>
</a>
</div>
</div>
</div>
</div>
<!-- Right Column -->
<div class="content-column">
<!-- Work Policies Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">📋</span>
Work Policies
</h2>
</div>
<div class="card-body">
<!-- Regional Presets -->
<div class="preset-section">
<h3 class="section-title">Regional Preset</h3>
<form method="POST" action="{{ url_for('companies.admin_company') }}" class="preset-form">
<input type="hidden" name="action" value="update_work_policies">
<input type="hidden" name="apply_preset" value="true">
<select name="region_preset" class="form-control form-select" onchange="this.form.submit()">
<option value="">Select a regional preset...</option>
{% for preset in regional_presets %}
<option value="{{ preset.code }}" {% if work_config.work_region.value == preset.code %}selected{% endif %}>
{{ preset.name }} - {{ preset.description }}
</option>
{% endfor %}
</select>
</form>
</div>
<!-- Current Configuration -->
<div class="config-section">
<h3 class="section-title">
Current Configuration
<span class="config-badge">{{ work_config.work_region.value if work_config.work_region else 'Custom' }}</span>
</h3>
<form method="POST" action="{{ url_for('companies.admin_company') }}" class="modern-form">
<input type="hidden" name="action" value="update_work_policies">
<div class="form-grid">
<div class="form-group">
<label for="standard_hours_per_day" class="form-label">Hours per Day</label>
<input type="number" id="standard_hours_per_day" name="standard_hours_per_day"
class="form-control" value="{{ work_config.standard_hours_per_day }}"
step="0.5" min="1" max="24" required>
</div>
<div class="form-group">
<label for="standard_hours_per_week" class="form-label">Hours per Week</label>
<input type="number" id="standard_hours_per_week" name="standard_hours_per_week"
class="form-control" value="{{ work_config.standard_hours_per_week }}"
step="0.5" min="1" max="168" required>
</div>
<div class="form-group">
<label for="break_duration_minutes" class="form-label">Break Duration (min)</label>
<input type="number" id="break_duration_minutes" name="break_duration_minutes"
class="form-control" value="{{ work_config.break_duration_minutes }}"
min="0" max="120" required>
</div>
<div class="form-group">
<label for="break_after_hours" class="form-label">Break After (hours)</label>
<input type="number" id="break_after_hours" name="break_after_hours"
class="form-control" value="{{ work_config.break_after_hours }}"
step="0.5" min="0" max="24" required>
</div>
<div class="form-group">
<label for="overtime_rate" class="form-label">Overtime Rate</label>
<input type="number" id="overtime_rate" name="overtime_rate"
class="form-control" value="{{ work_config.overtime_rate }}"
step="0.1" min="1" max="3" required>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon"></span>
Update Policies
</button>
</div>
</form>
</div>
</div>
</div>
<!-- User Registration Settings Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">👤</span>
User Registration
</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('companies.admin_company') }}" class="modern-form">
<input type="hidden" name="action" value="update_system_settings">
<div class="settings-list">
<div class="setting-item">
<div class="setting-toggle">
<label class="toggle-switch">
<input type="checkbox" id="registration_enabled" name="registration_enabled"
{% if settings.registration_enabled %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-content">
<h4 class="setting-title">Enable User Registration</h4>
<p class="setting-description">Allow new users to register accounts using the company code</p>
</div>
</div>
<div class="setting-item">
<div class="setting-toggle">
<label class="toggle-switch">
<input type="checkbox" id="email_verification_required" name="email_verification_required"
{% if settings.email_verification_required %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-content">
<h4 class="setting-title">Require Email Verification</h4>
<p class="setting-description">New users must verify their email address before accessing the system</p>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon"></span>
Update Settings
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
/* Container */
.company-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;
position: relative;
}
.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;
}
.stat-link {
position: absolute;
bottom: 0.75rem;
right: 1rem;
font-size: 0.875rem;
color: #667eea;
text-decoration: none;
opacity: 0.7;
transition: opacity 0.2s;
}
.stat-link:hover {
opacity: 1;
text-decoration: underline;
}
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 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;
}
.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;
}
/* Forms */
.modern-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.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;
}
/* Toggle Switch */
.toggle-container {
display: flex;
align-items: center;
gap: 1rem;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 32px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #cbd5e1;
transition: .4s;
border-radius: 34px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 24px;
width: 24px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #667eea;
}
input:checked + .toggle-slider:before {
transform: translateX(28px);
}
.toggle-label {
font-weight: 500;
color: #374151;
}
/* Info Panel */
.info-panel {
background: #f3f4f6;
border-radius: 8px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.info-item {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.info-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
.info-content {
flex: 1;
}
.info-label {
font-weight: 600;
color: #374151;
display: block;
margin-bottom: 0.5rem;
}
.info-value {
color: #667eea;
font-weight: 500;
}
.info-hint {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.code-display {
display: flex;
gap: 0.5rem;
align-items: center;
}
.code-input {
font-family: 'Monaco', 'Courier New', monospace;
font-weight: 600;
color: #667eea;
background: white;
border: 2px solid #e5e7eb;
padding: 0.5rem 1rem;
border-radius: 6px;
flex: 1;
max-width: 300px;
}
/* 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-copy {
background: white;
color: #6b7280;
border: 2px solid #e5e7eb;
padding: 0.5rem 1rem;
}
.btn-copy:hover {
background: #f3f4f6;
border-color: #d1d5db;
}
.btn-copy.success {
background: #d1fae5;
color: #059669;
border-color: #10b981;
}
/* Quick Actions */
.action-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.action-item {
display: flex;
gap: 1rem;
padding: 1.25rem;
background: #f8f9fa;
border-radius: 8px;
text-decoration: none;
transition: all 0.2s ease;
border: 2px solid transparent;
}
.action-item:hover {
background: white;
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
}
.action-icon {
font-size: 2rem;
flex-shrink: 0;
}
.action-content h3 {
font-size: 1.05rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 0.25rem 0;
}
.action-content p {
font-size: 0.875rem;
color: #6b7280;
margin: 0;
}
/* Work Policies */
.preset-section,
.config-section {
margin-bottom: 2rem;
}
.section-title {
font-size: 1.1rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.config-badge {
background: #ede9fe;
color: #5b21b6;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
/* Settings List */
.settings-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.setting-item {
display: flex;
gap: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
align-items: flex-start;
}
.setting-toggle {
flex-shrink: 0;
}
.setting-content {
flex: 1;
}
.setting-title {
font-size: 1.05rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 0.25rem 0;
}
.setting-description {
font-size: 0.875rem;
color: #6b7280;
margin: 0;
}
/* Responsive Design */
@media (max-width: 768px) {
.company-admin-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.page-title {
font-size: 1.75rem;
}
.header-content {
flex-direction: column;
text-align: center;
}
.form-row,
.form-grid {
grid-template-columns: 1fr;
}
.action-grid {
grid-template-columns: 1fr;
}
.code-input {
max-width: 100%;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
</style>
<script>
function copyCompanyCode() {
const codeInput = document.getElementById('companyCode');
codeInput.select();
codeInput.setSelectionRange(0, 99999);
try {
document.execCommand('copy');
// Show feedback
const button = event.currentTarget;
const copyIcon = document.getElementById('copyIcon');
const copyText = document.getElementById('copyText');
// Store original values
const originalIcon = copyIcon.textContent;
const originalText = copyText.textContent;
// Update to success state
copyIcon.textContent = '✓';
copyText.textContent = 'Copied!';
button.classList.add('success');
// Reset after 2 seconds
setTimeout(() => {
copyIcon.textContent = originalIcon;
copyText.textContent = originalText;
button.classList.remove('success');
}, 2000);
} catch (err) {
alert('Failed to copy code. Please select and copy manually.');
}
}
</script>
{% endblock %}