455 lines
19 KiB
HTML
455 lines
19 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}My Profile - {{ block.super }}{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.profile-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 2rem 0;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.profile-avatar {
|
|
width: 100px;
|
|
height: 100px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
border: 4px solid #fff;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.profile-card {
|
|
background: white;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.form-section {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.form-section h6 {
|
|
color: #495057;
|
|
font-weight: 600;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 2px solid #f1f3f4;
|
|
}
|
|
|
|
.session-card {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 0.75rem;
|
|
border-left: 4px solid #28a745;
|
|
}
|
|
|
|
.session-card.current {
|
|
border-left-color: #007bff;
|
|
background: #e3f2fd;
|
|
}
|
|
|
|
.device-card {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 0.75rem;
|
|
border-left: 4px solid #007bff;
|
|
}
|
|
|
|
.social-account-card {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 0.75rem;
|
|
border-left: 4px solid #28a745;
|
|
}
|
|
|
|
.preferences-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.preference-item {
|
|
background: #f8f9fa;
|
|
padding: 1rem;
|
|
border-radius: 0.375rem;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
.avatar-upload {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
.avatar-upload input[type="file"] {
|
|
position: absolute;
|
|
opacity: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.avatar-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0,0,0,0.5);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.avatar-upload:hover .avatar-overlay {
|
|
opacity: 1;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="profile-header">
|
|
<div class="container-fluid">
|
|
<div class="row align-items-center">
|
|
<div class="col-auto">
|
|
<div class="avatar-upload">
|
|
{% if user.employee_profile.profile_picture %}
|
|
<img src="{{ user.employee_profile.profile_picture.url }}" alt="{{ user.employee_profile.get_full_name }}" class="profile-avatar">
|
|
{% else %}
|
|
<div class="profile-avatar bg-secondary d-flex align-items-center justify-content-center">
|
|
<i class="fas fa-user fa-2x text-white"></i>
|
|
</div>
|
|
{% endif %}
|
|
<div class="avatar-overlay">
|
|
<i class="fas fa-camera text-white"></i>
|
|
</div>
|
|
<input type="file" accept="image/*" id="avatar-upload">
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<h1 class="mb-2">{{ user.employee_profile.get_full_name }}</h1>
|
|
<p class="mb-0 opacity-75">{{ user.employee_profile.get_role_display }} • {{ user.employee_profile.department|default:"No Department" }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<!-- Profile Information -->
|
|
<div class="col-lg-8">
|
|
<div class="profile-card">
|
|
<h5 class="mb-4">
|
|
<i class="fas fa-user me-2"></i>Profile Information
|
|
</h5>
|
|
|
|
<form id="profile-form"
|
|
hx-post="{% url 'accounts:user_profile_update' %}"
|
|
hx-trigger="submit"
|
|
hx-swap="none">
|
|
{% csrf_token %}
|
|
|
|
<div class="form-section">
|
|
<h6>Personal Information</h6>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="first_name" class="form-label">First Name</label>
|
|
<input type="text" class="form-control" id="first_name" name="first_name"
|
|
value="{{ user.employee_profile.first_name }}" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="last_name" class="form-label">Last Name</label>
|
|
<input type="text" class="form-control" id="last_name" name="last_name"
|
|
value="{{ user.employee_profile.last_name }}" required>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="email" class="form-label">Email Address</label>
|
|
<input type="email" class="form-control" id="email" name="email"
|
|
value="{{ user.employee_profile.email }}" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="phone" class="form-label">Phone Number</label>
|
|
<input type="tel" class="form-control" id="phone" name="phone"
|
|
value="{{ user.employee_profile.phone }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="mobile_phone" class="form-label">Mobile Number</label>
|
|
<input type="tel" class="form-control" id="mobile_phone" name="mobile_phone"
|
|
value="{{ user.employee_profile.mobile_phone }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="bio" class="form-label">Bio</label>
|
|
<textarea class="form-control" id="bio" name="bio" rows="3"
|
|
placeholder="Tell us about yourself...">{{ user.employee_profile.bio }}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<h6>Preferences</h6>
|
|
<div class="preferences-grid">
|
|
<div class="preference-item">
|
|
<label for="timezone" class="form-label">Timezone</label>
|
|
<select class="form-select" id="timezone" name="timezone">
|
|
<option value="Asia/Riyadh" {% if user.employee_profile.timezone == 'Asia/Riyadh' %}selected{% endif %}>Asia/Riyadh</option>
|
|
<option value="America/New_York" {% if user.employee_profile.timezone == 'America/New_York' %}selected{% endif %}>Eastern Time</option>
|
|
<option value="America/Chicago" {% if user.employee_profile.timezone == 'America/Chicago' %}selected{% endif %}>Central Time</option>
|
|
<option value="America/Denver" {% if user.employee_profile.timezone == 'America/Denver' %}selected{% endif %}>Mountain Time</option>
|
|
<option value="America/Los_Angeles" {% if user.employee_profile.timezone == 'America/Los_Angeles' %}selected{% endif %}>Pacific Time</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="preference-item">
|
|
<label for="language" class="form-label">Language</label>
|
|
<select class="form-select" id="language" name="language">
|
|
<option value="en" {% if user.employee_profile.language == 'en' %}selected{% endif %}>English</option>
|
|
<option value="ar" {% if user.employee_profile.language == 'ar' %}selected{% endif %}>Arabic</option>
|
|
<option value="fr" {% if user.employee_profile.language == 'fr' %}selected{% endif %}>French</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="preference-item">
|
|
<label for="theme" class="form-label">Theme</label>
|
|
<select class="form-select" id="theme" name="theme">
|
|
<option value="light" {% if user.employee_profile.theme == 'light' %}selected{% endif %}>Light</option>
|
|
<option value="dark" {% if user.employee_profile.theme == 'dark' %}selected{% endif %}>Dark</option>
|
|
<option value="auto" {% if user.employee_profile.theme == 'auto' %}selected{% endif %}>Auto</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-end gap-2">
|
|
<button type="button" class="btn btn-secondary">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save me-1"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Security & Sessions -->
|
|
<div class="col-lg-4">
|
|
<!-- Active Sessions -->
|
|
<div class="profile-card">
|
|
<h6 class="mb-3">
|
|
<i class="fas fa-desktop me-2"></i>Active Sessions
|
|
<span class="badge bg-primary ms-2">{{ active_sessions.count }}</span>
|
|
</h6>
|
|
|
|
{% if active_sessions %}
|
|
{% for session in active_sessions %}
|
|
<div class="session-card {% if session.is_current_session %}current{% endif %}">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div class="flex-grow-1">
|
|
<div class="fw-bold small">{{ session.device_type|title }}</div>
|
|
<div class="text-muted small">
|
|
{{ session.ip_address }} • {{ session.location|default:"Unknown" }}
|
|
</div>
|
|
<div class="text-muted small">
|
|
{{ session.last_activity_at|timesince }} ago
|
|
</div>
|
|
</div>
|
|
<div>
|
|
{% if session.is_current_session %}
|
|
<span class="badge bg-primary small">Current</span>
|
|
{% else %}
|
|
<button class="btn btn-sm btn-outline-danger"
|
|
hx-post="{% url 'accounts:end_session' session.session_id %}"
|
|
hx-confirm="End this session?">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center py-3 text-muted">
|
|
<i class="fas fa-desktop fa-2x mb-2"></i>
|
|
<p class="small">No active sessions</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Two-Factor Authentication -->
|
|
<div class="profile-card">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="mb-0">
|
|
<i class="fas fa-shield-alt me-2"></i>Two-Factor Authentication
|
|
</h6>
|
|
<button class="btn btn-sm btn-outline-primary"
|
|
hx-get="{% url 'accounts:two_factor_setup' %}"
|
|
hx-target="#two-factor-modal .modal-body"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#two-factor-modal">
|
|
<i class="fas fa-plus me-1"></i>Add
|
|
</button>
|
|
</div>
|
|
|
|
{% if two_factor_devices %}
|
|
{% for device in two_factor_devices %}
|
|
<div class="device-card">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div class="flex-grow-1">
|
|
<div class="fw-bold small">{{ device.name }}</div>
|
|
<div class="text-muted small">
|
|
{{ device.get_device_type_display }}
|
|
{% if device.phone_number %} • {{ device.phone_number }}{% endif %}
|
|
</div>
|
|
<div class="text-muted small">
|
|
Added {{ device.created_at|date:"M d, Y" }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
{% if device.is_verified %}
|
|
<span class="badge bg-success small">Verified</span>
|
|
{% else %}
|
|
<span class="badge bg-warning small">Pending</span>
|
|
{% endif %}
|
|
<button class="btn btn-sm btn-outline-danger ms-1"
|
|
hx-delete="{% url 'accounts:remove_two_factor_device' device.device_id %}"
|
|
hx-confirm="Remove this device?">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center py-3 text-muted">
|
|
<i class="fas fa-shield-alt fa-2x mb-2"></i>
|
|
<p class="small">No two-factor devices configured</p>
|
|
<p class="small">Add a device to enhance your account security</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Social Accounts -->
|
|
<div class="profile-card">
|
|
<h6 class="mb-3">
|
|
<i class="fas fa-share-alt me-2"></i>Connected Accounts
|
|
</h6>
|
|
|
|
{% if social_accounts %}
|
|
{% for account in social_accounts %}
|
|
<div class="social-account-card">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="fw-bold small">{{ account.get_provider_display }}</div>
|
|
<div class="text-muted small">{{ account.provider_user_id }}</div>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-danger">
|
|
<i class="fas fa-unlink"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center py-3 text-muted">
|
|
<i class="fas fa-share-alt fa-2x mb-2"></i>
|
|
<p class="small">No connected accounts</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Two-Factor Setup Modal -->
|
|
<div class="modal fade" id="two-factor-modal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Setup Two-Factor Authentication</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Content loaded via HTMX -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Handle avatar upload
|
|
document.getElementById('avatar-upload').addEventListener('change', function(e) {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const formData = new FormData();
|
|
formData.append('avatar', file);
|
|
|
|
// Upload avatar via fetch
|
|
fetch('{% url "accounts:upload_avatar" %}', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Failed to upload avatar');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle form submission success
|
|
document.body.addEventListener('htmx:afterRequest', function(event) {
|
|
if (event.detail.target.id === 'profile-form' && event.detail.xhr.status === 200) {
|
|
// Show success message
|
|
const alert = document.createElement('div');
|
|
alert.className = 'alert alert-success alert-dismissible fade show';
|
|
alert.innerHTML = `
|
|
<i class="fas fa-check-circle me-2"></i>Profile updated successfully!
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
document.querySelector('.profile-card').insertBefore(alert, document.querySelector('#profile-form'));
|
|
|
|
// Auto-dismiss after 3 seconds
|
|
setTimeout(() => {
|
|
alert.remove();
|
|
}, 3000);
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|