397 lines
16 KiB
HTML
397 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ user_profile.get_full_name }} - User Details - {{ block.super }}{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.user-avatar {
|
|
width: 120px;
|
|
height: 120px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
border: 4px solid #fff;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.user-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 2rem 0;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.status-badge {
|
|
font-size: 0.875rem;
|
|
padding: 0.375rem 0.75rem;
|
|
}
|
|
|
|
.info-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;
|
|
}
|
|
|
|
.info-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.75rem 0;
|
|
border-bottom: 1px solid #f1f3f4;
|
|
}
|
|
|
|
.info-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.info-label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
min-width: 140px;
|
|
}
|
|
|
|
.info-value {
|
|
color: #212529;
|
|
text-align: right;
|
|
flex: 1;
|
|
}
|
|
|
|
.session-item {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 0.75rem;
|
|
border-left: 4px solid #28a745;
|
|
}
|
|
|
|
.session-item.expired {
|
|
border-left-color: #dc3545;
|
|
background: #fff5f5;
|
|
}
|
|
|
|
.device-item {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 0.75rem;
|
|
border-left: 4px solid #007bff;
|
|
}
|
|
|
|
.activity-timeline {
|
|
position: relative;
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
.activity-timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0.75rem;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: #dee2e6;
|
|
}
|
|
|
|
.activity-item {
|
|
position: relative;
|
|
background: white;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.activity-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -1.5rem;
|
|
top: 1rem;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: #007bff;
|
|
border: 3px solid white;
|
|
box-shadow: 0 0 0 2px #dee2e6;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="user-header">
|
|
<div class="container-fluid">
|
|
<div class="row align-items-center">
|
|
<div class="col-auto">
|
|
{% if user_profile.profile_picture %}
|
|
<img src="{{ user_profile.profile_picture.url }}" alt="{{ user_profile.get_full_name }}" class="user-avatar">
|
|
{% else %}
|
|
<div class="user-avatar bg-secondary d-flex align-items-center justify-content-center">
|
|
<i class="fas fa-user fa-3x text-white"></i>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="col">
|
|
<h1 class="mb-2">{{ user_profile.get_full_name }}</h1>
|
|
<p class="mb-2 opacity-75">{{ user_profile.role|title }} • {{ user_profile.department|default:"No Department" }}</p>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
{% if user_profile.is_active %}
|
|
<span class="badge bg-success status-badge">
|
|
<i class="fas fa-check-circle me-1"></i>Active
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-danger status-badge">
|
|
<i class="fas fa-times-circle me-1"></i>Inactive
|
|
</span>
|
|
{% endif %}
|
|
|
|
{% if user_profile.is_approved %}
|
|
<span class="badge bg-info status-badge">
|
|
<i class="fas fa-shield-check me-1"></i>Approved
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-warning status-badge">
|
|
<i class="fas fa-clock me-1"></i>Pending Approval
|
|
</span>
|
|
{% endif %}
|
|
|
|
{% if user_profile.two_factor_enabled %}
|
|
<span class="badge bg-primary status-badge">
|
|
<i class="fas fa-lock me-1"></i>2FA Enabled
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<div class="btn-group">
|
|
<button class="btn btn-light" onclick="window.print()">
|
|
<i class="fas fa-print me-1"></i>Print
|
|
</button>
|
|
<button class="btn btn-light" data-bs-toggle="modal" data-bs-target="#editUserModal">
|
|
<i class="fas fa-edit me-1"></i>Edit
|
|
</button>
|
|
<div class="btn-group">
|
|
<button class="btn btn-light dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#"><i class="fas fa-key me-2"></i>Reset Password</a></li>
|
|
<li><a class="dropdown-item" href="#"><i class="fas fa-ban me-2"></i>Suspend Account</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item text-danger" href="#"><i class="fas fa-trash me-2"></i>Delete Account</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<!-- User Information -->
|
|
<div class="col-lg-4">
|
|
<div class="info-card">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-user me-2"></i>Personal Information
|
|
</h5>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Employee ID</span>
|
|
<span class="info-value">{{ user_profile.employee_id|default:"Not set" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Email</span>
|
|
<span class="info-value">{{ user_profile.email }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Phone</span>
|
|
<span class="info-value">{{ user_profile.phone_number|default:"Not provided" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Mobile</span>
|
|
<span class="info-value">{{ user_profile.mobile_number|default:"Not provided" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Date of Birth</span>
|
|
<span class="info-value">{{ user_profile.date_of_birth|date:"M d, Y"|default:"Not provided" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Gender</span>
|
|
<span class="info-value">{{ user_profile.get_gender_display|default:"Not specified" }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-briefcase me-2"></i>Professional Information
|
|
</h5>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Role</span>
|
|
<span class="info-value">{{ user_profile.get_role_display }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Department</span>
|
|
<span class="info-value">{{ user_profile.department|default:"Not assigned" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">License Number</span>
|
|
<span class="info-value">{{ user_profile.license_number|default:"Not provided" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Specialization</span>
|
|
<span class="info-value">{{ user_profile.specialization|default:"Not specified" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Date Joined</span>
|
|
<span class="info-value">{{ user_profile.date_joined|date:"M d, Y" }}</span>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<span class="info-label">Last Login</span>
|
|
<span class="info-value">{{ user_profile.last_login|date:"M d, Y H:i"|default:"Never" }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sessions and Security -->
|
|
<div class="col-lg-8">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="info-card">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-desktop me-2"></i>Active Sessions
|
|
<span class="badge bg-primary ms-2">{{ active_sessions.count }}</span>
|
|
</h5>
|
|
|
|
{% if active_sessions %}
|
|
{% for session in active_sessions %}
|
|
<div class="session-item {% if session.expires_at < now %}expired{% endif %}">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="fw-bold">{{ session.device_type|title }} • {{ session.browser_name }}</div>
|
|
<div class="text-muted small">
|
|
<i class="fas fa-globe me-1"></i>{{ session.ip_address }}
|
|
<span class="mx-2">•</span>
|
|
<i class="fas fa-map-marker-alt me-1"></i>{{ session.location|default:"Unknown location" }}
|
|
</div>
|
|
<div class="text-muted small">
|
|
<i class="fas fa-clock me-1"></i>
|
|
Started: {{ session.created_at|date:"M d, Y H:i" }}
|
|
<span class="mx-2">•</span>
|
|
Last activity: {{ session.last_activity_at|date:"M d, Y H:i" }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
{% if session.is_current_session %}
|
|
<span class="badge bg-success">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>No active sessions</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div class="info-card">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-shield-alt me-2"></i>Two-Factor Authentication
|
|
<span class="badge bg-primary ms-2">{{ two_factor_devices.count }}</span>
|
|
</h5>
|
|
|
|
{% if two_factor_devices %}
|
|
{% for device in two_factor_devices %}
|
|
<div class="device-item">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<div class="fw-bold">{{ device.name }}</div>
|
|
<div class="text-muted small">
|
|
<i class="fas fa-mobile-alt me-1"></i>{{ device.get_device_type_display }}
|
|
{% if device.phone_number %}
|
|
<span class="mx-2">•</span>{{ device.phone_number }}
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-muted small">
|
|
<i class="fas fa-clock me-1"></i>
|
|
Added: {{ device.created_at|date:"M d, Y H:i" }}
|
|
<span class="mx-2">•</span>
|
|
Last used: {{ device.last_used_at|date:"M d, Y H:i"|default:"Never" }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
{% if device.is_verified %}
|
|
<span class="badge bg-success">Verified</span>
|
|
{% else %}
|
|
<span class="badge bg-warning">Pending</span>
|
|
{% endif %}
|
|
</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>No two-factor devices configured</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div class="info-card">
|
|
<h5 class="mb-3">
|
|
<i class="fas fa-history me-2"></i>Recent Activity
|
|
</h5>
|
|
|
|
<div id="user-activity-log"
|
|
hx-get="{% url 'accounts:user_activity_log' user_profile.id %}"
|
|
hx-trigger="load">
|
|
<div class="text-center py-3">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Auto-refresh activity log every 60 seconds
|
|
setInterval(function() {
|
|
htmx.trigger('#user-activity-log', 'refresh');
|
|
}, 60000);
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|