Files
TimeTrack/templates/profile.html

1052 lines
30 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="profile-container">
<!-- Header Section -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="profile-header-info">
<img src="{{ user.get_avatar_url(80) }}" alt="{{ user.username }}" class="header-avatar">
<div>
<h1 class="page-title">{{ user.username }}</h1>
<p class="page-subtitle">Manage your profile and account settings</p>
</div>
</div>
</div>
<div class="header-stats">
<div class="stat-badge">
<span class="stat-icon">🏢</span>
<span class="stat-text">{{ user.company.name if user.company else 'No Company' }}</span>
</div>
<div class="stat-badge">
<span class="stat-icon"><i class="ti ti-users"></i></span>
<span class="stat-text">{{ user.team.name if user.team else 'No Team' }}</span>
</div>
<div class="stat-badge">
<span class="stat-icon">👤</span>
<span class="stat-text">{{ user.role.value if user.role else 'Team Member' }}</span>
</div>
</div>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
<span class="alert-icon">{% if category == 'success' %}✓{% elif category == 'error' %}✕{% else %}{% endif %}</span>
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Main Content Grid -->
<div class="content-grid">
<!-- Left Column -->
<div class="content-column">
<!-- Profile Picture Card -->
<div class="card avatar-card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">🖼️</span>
Profile Picture
</h2>
</div>
<div class="card-body">
<div class="avatar-showcase">
<img src="{{ user.get_avatar_url(160) }}" alt="{{ user.username }}" class="profile-avatar" id="avatar-preview">
</div>
<div class="avatar-controls">
<div class="control-tabs">
<button class="tab-btn active" data-tab="default">
<span class="tab-icon">👤</span>
Default
</button>
<button class="tab-btn" data-tab="upload">
<span class="tab-icon">📤</span>
Upload
</button>
<button class="tab-btn" data-tab="url">
<span class="tab-icon">🔗</span>
URL
</button>
</div>
<!-- Default Avatar Tab -->
<div class="tab-content active" id="default-tab">
<div class="info-message">
<span class="info-icon"><i class="ti ti-bulb"></i></span>
<p>Your default avatar is automatically generated based on your username.</p>
</div>
<button type="button" class="btn btn-outline" onclick="resetAvatar()">
<span class="icon"></span>
Reset to Default
</button>
</div>
<!-- Upload Avatar Tab -->
<div class="tab-content" id="upload-tab">
<form method="POST" action="{{ url_for('upload_avatar') }}" enctype="multipart/form-data" class="modern-form">
<div class="upload-area">
<label for="avatar_file" class="upload-label">
<div class="upload-icon">📁</div>
<div class="upload-text">Drop image here or click to browse</div>
<div class="upload-hint">Max 5MB • JPG, PNG, GIF, WebP</div>
<div class="file-name" id="file-name"></div>
</label>
<input type="file" id="avatar_file" name="avatar_file" class="file-input"
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp" required>
</div>
<div class="upload-preview" id="upload-preview" style="display: none;">
<img id="upload-preview-img" src="" alt="Preview">
</div>
<button type="submit" class="btn btn-primary" id="upload-btn" disabled>
<span class="icon"></span>
Upload Avatar
</button>
</form>
</div>
<!-- URL Avatar Tab -->
<div class="tab-content" id="url-tab">
<form method="POST" action="{{ url_for('update_avatar') }}" class="modern-form">
<div class="form-group">
<label for="avatar_url" class="form-label">Image URL</label>
<input type="url" id="avatar_url" name="avatar_url" class="form-control"
placeholder="https://example.com/avatar.jpg"
value="{{ user.avatar_url or '' }}">
<span class="form-hint">Enter a direct link to an image</span>
</div>
<button type="submit" class="btn btn-primary">
<span class="icon"></span>
Set Avatar URL
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Account Information Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span class="icon"></span>
Account Information
</h2>
</div>
<div class="card-body">
<div class="info-grid">
<div class="info-item">
<span class="info-label">Username</span>
<span class="info-value">{{ user.username }}</span>
</div>
<div class="info-item">
<span class="info-label">Email</span>
<span class="info-value">
{{ user.email if user.email else 'Not provided' }}
{% if user.email and not user.is_verified %}
<span class="badge badge-warning">Unverified</span>
{% endif %}
</span>
</div>
<div class="info-item">
<span class="info-label">Member Since</span>
<span class="info-value">{{ user.created_at.strftime('%B %d, %Y') }}</span>
</div>
<div class="info-item">
<span class="info-label">Account Type</span>
<span class="info-value">{{ user.account_type.value if user.account_type else 'Standard' }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Right Column -->
<div class="content-column">
<!-- Email Settings Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">✉️</span>
Email Settings
</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('profile') }}" 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"
value="{{ user.email or '' }}" placeholder="your@email.com">
<span class="form-hint">Used for notifications and account recovery</span>
</div>
{% if user.email and not user.is_verified %}
<div class="alert alert-warning">
<span class="alert-icon"><i class="ti ti-alert-triangle"></i></span>
<div>
<p>Your email address is not verified.</p>
<a href="{{ url_for('profile') }}" class="btn btn-sm btn-warning">Send Verification Email</a>
</div>
</div>
{% elif not user.email %}
<div class="alert alert-info">
<span class="alert-icon"></span>
<p>Adding an email enables account recovery and notifications.</p>
</div>
{% endif %}
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon"></span>
{% if user.email %}Update{% else %}Add{% endif %} Email
</button>
</div>
</form>
</div>
</div>
<!-- Security Settings Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span class="icon">🔒</span>
Security Settings
</h2>
</div>
<div class="card-body">
<!-- Change Password Section -->
<div class="security-section">
<h3 class="section-title">Change Password</h3>
<form method="POST" action="{{ url_for('profile') }}" class="modern-form">
<input type="hidden" name="email" value="{{ user.email or '' }}">
<div class="form-group">
<label for="current_password" class="form-label">Current Password</label>
<input type="password" id="current_password" name="current_password"
class="form-control" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="new_password" class="form-label">New Password</label>
<input type="password" id="new_password" name="new_password"
class="form-control" required>
<span class="form-hint">Min. 8 characters</span>
</div>
<div class="form-group">
<label for="confirm_password" class="form-label">Confirm Password</label>
<input type="password" id="confirm_password" name="confirm_password"
class="form-control" required>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-warning">
<span class="icon">🔑</span>
Change Password
</button>
</div>
</form>
</div>
<!-- Two-Factor Authentication Section -->
<div class="security-section">
<h3 class="section-title">Two-Factor Authentication</h3>
<div class="tfa-status">
{% if user.two_factor_enabled %}
<div class="status-indicator enabled">
<span class="status-icon">🛡️</span>
<div>
<div class="status-text">Enabled</div>
<div class="status-description">Your account is protected with 2FA</div>
</div>
</div>
<form method="POST" action="{{ url_for('disable_2fa') }}" class="modern-form"
onsubmit="return confirm('Are you sure you want to disable two-factor authentication?');">
<div class="form-group">
<label for="password_disable" class="form-label">Password</label>
<input type="password" id="password_disable" name="password"
class="form-control" placeholder="Enter your password to disable 2FA" required>
</div>
<button type="submit" class="btn btn-danger">
<span class="icon"></span>
Disable 2FA
</button>
</form>
{% else %}
<div class="status-indicator disabled">
<span class="status-icon"><i class="ti ti-alert-triangle"></i></span>
<div>
<div class="status-text">Disabled</div>
<div class="status-description">Add extra security to your account</div>
</div>
</div>
<a href="{{ url_for('setup_2fa') }}" class="btn btn-success">
<span class="icon"></span>
Enable 2FA
</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* Container and Layout */
.profile-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: 2rem;
}
.profile-header-info {
display: flex;
align-items: center;
gap: 1.5rem;
}
.header-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
border: 4px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
margin: 0;
}
.page-subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin: 0.25rem 0 0 0;
}
.header-stats {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
.stat-badge {
display: flex;
align-items: center;
gap: 0.5rem;
background: rgba(255, 255, 255, 0.2);
padding: 0.75rem 1.25rem;
border-radius: 12px;
backdrop-filter: blur(10px);
}
.stat-icon {
font-size: 1.5rem;
}
.stat-text {
font-weight: 500;
}
/* Flash Messages */
.flash-messages {
margin-bottom: 2rem;
}
.alert {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
border-radius: 8px;
margin-bottom: 1rem;
animation: slideIn 0.3s ease-out;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.alert-icon {
font-size: 1.25rem;
font-weight: bold;
}
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: 1fr 1.2fr;
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;
}
/* Avatar Card */
.avatar-showcase {
text-align: center;
margin-bottom: 2rem;
}
.profile-avatar {
width: 160px;
height: 160px;
border-radius: 50%;
object-fit: cover;
border: 6px solid #f3f4f6;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.control-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
background: #f3f4f6;
padding: 0.5rem;
border-radius: 8px;
}
.tab-btn {
flex: 1;
padding: 0.75rem 1rem;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-weight: 500;
color: #6b7280;
transition: all 0.2s ease;
}
.tab-btn:hover {
background: rgba(102, 126, 234, 0.1);
color: #667eea;
}
.tab-btn.active {
background: white;
color: #667eea;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.tab-icon {
font-size: 1.25rem;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Upload Area */
.upload-area {
margin-bottom: 1.5rem;
}
.upload-label {
display: flex;
flex-direction: column;
align-items: center;
padding: 3rem 2rem;
border: 2px dashed #e5e7eb;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
background: #f9fafb;
}
.upload-label:hover {
border-color: #667eea;
background: #f3f4f6;
}
.upload-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.upload-text {
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
.upload-hint {
font-size: 0.875rem;
color: #6b7280;
}
.file-name {
margin-top: 1rem;
font-size: 0.875rem;
color: #667eea;
font-weight: 500;
}
.file-input {
display: none;
}
.upload-preview {
text-align: center;
margin: 1rem 0;
}
.upload-preview img {
max-width: 200px;
max-height: 200px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
/* Info Grid */
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.info-label {
font-size: 0.875rem;
color: #6b7280;
font-weight: 600;
text-transform: uppercase;
}
.info-value {
font-size: 1.05rem;
color: #1f2937;
font-weight: 500;
}
.info-message {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
background: #f3f4f6;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.info-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
/* 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-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;
}
.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:not(:disabled) {
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-warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
}
.btn-warning:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
}
.btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.btn-outline {
background: white;
color: #6b7280;
border: 2px solid #e5e7eb;
}
.btn-outline:hover {
background: #f3f4f6;
border-color: #d1d5db;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
/* Security Section */
.security-section {
padding: 1.5rem 0;
border-bottom: 1px solid #e5e7eb;
}
.security-section:last-child {
border-bottom: none;
padding-bottom: 0;
}
.section-title {
font-size: 1.1rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1rem;
}
.tfa-status {
margin-top: 1rem;
}
.status-indicator {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.status-indicator.enabled {
background: #d4edda;
border: 1px solid #c3e6cb;
}
.status-indicator.disabled {
background: #fff3cd;
border: 1px solid #ffeaa7;
}
.status-icon {
font-size: 2rem;
}
.status-text {
font-weight: 600;
color: #1f2937;
margin-bottom: 0.25rem;
}
.status-description {
font-size: 0.875rem;
color: #6b7280;
}
/* Badges */
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 4px;
text-transform: uppercase;
}
.badge-warning {
background: #fef3c7;
color: #92400e;
}
/* Responsive Design */
@media (max-width: 768px) {
.profile-container {
padding: 1rem;
}
.page-header {
padding: 1.5rem;
}
.profile-header-info {
flex-direction: column;
text-align: center;
}
.header-stats {
justify-content: center;
}
.page-title {
font-size: 2rem;
}
.info-grid {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.control-tabs {
flex-direction: column;
}
.tab-btn {
justify-content: flex-start;
}
}
/* Animations */
@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>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Tab switching
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
tabBtns.forEach(btn => {
btn.addEventListener('click', function() {
const tabName = this.getAttribute('data-tab');
// Update active states
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
this.classList.add('active');
document.getElementById(tabName + '-tab').classList.add('active');
});
});
// File upload handling
const fileInput = document.getElementById('avatar_file');
const fileName = document.getElementById('file-name');
const uploadPreview = document.getElementById('upload-preview');
const uploadPreviewImg = document.getElementById('upload-preview-img');
const uploadBtn = document.getElementById('upload-btn');
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
// Update file name display
fileName.textContent = file.name;
// Validate file size (5MB max)
if (file.size > 5 * 1024 * 1024) {
alert('File size must be less than 5MB');
this.value = '';
fileName.textContent = '';
uploadPreview.style.display = 'none';
uploadBtn.disabled = true;
return;
}
// Validate file type
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!validTypes.includes(file.type)) {
alert('Please select a valid image file (JPG, PNG, GIF, or WebP)');
this.value = '';
fileName.textContent = '';
uploadPreview.style.display = 'none';
uploadBtn.disabled = true;
return;
}
// Preview the image
const reader = new FileReader();
reader.onload = function(e) {
uploadPreviewImg.src = e.target.result;
uploadPreview.style.display = 'block';
uploadBtn.disabled = false;
};
reader.readAsDataURL(file);
} else {
fileName.textContent = '';
uploadPreview.style.display = 'none';
uploadBtn.disabled = true;
}
});
// Avatar URL preview
const avatarUrlInput = document.getElementById('avatar_url');
const avatarPreview = document.getElementById('avatar-preview');
avatarUrlInput.addEventListener('input', function() {
const url = this.value.trim();
if (url && isValidUrl(url)) {
// Test if image loads
const img = new Image();
img.onload = function() {
avatarPreview.src = url;
};
img.onerror = function() {
// Keep current avatar if URL is invalid
avatarPreview.src = '{{ user.get_avatar_url(160) }}';
};
img.src = url;
}
});
// Drag and drop for file upload
const uploadLabel = document.querySelector('.upload-label');
uploadLabel.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.borderColor = '#667eea';
this.style.background = '#f3f4f6';
});
uploadLabel.addEventListener('dragleave', function(e) {
e.preventDefault();
this.style.borderColor = '#e5e7eb';
this.style.background = '#f9fafb';
});
uploadLabel.addEventListener('drop', function(e) {
e.preventDefault();
this.style.borderColor = '#e5e7eb';
this.style.background = '#f9fafb';
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
fileInput.dispatchEvent(new Event('change'));
}
});
});
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
function resetAvatar() {
if (confirm('Reset to your default avatar? This will remove any custom avatar.')) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("update_avatar") }}';
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'avatar_url';
input.value = '';
form.appendChild(input);
document.body.appendChild(form);
form.submit();
}
}
</script>
{% endblock %}