This commit is contained in:
Marwan Alwali 2026-01-12 13:20:34 +03:00
parent 350607d0cc
commit d7847da450
14 changed files with 571 additions and 16 deletions

View File

@ -117,6 +117,8 @@ class ProvisionalUserSerializer(serializers.ModelSerializer):
roles = serializers.ListField(
child=serializers.CharField(),
write_only=True,
required=False,
default=list,
help_text="List of role names to assign"
)

View File

@ -9,6 +9,8 @@ from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils import timezone
from django.db import models as db_models
from .models import (
AcknowledgementChecklistItem,
AcknowledgementContent,
@ -111,14 +113,26 @@ class OnboardingService:
Returns:
QuerySet of AcknowledgementContent
"""
# Get user's role
# Get user's role - convert group name to role code
role = None
if user.groups.exists():
role = user.groups.first().name
group_name = user.groups.first().name
# Map group names to role codes
role_mapping = {
'PX Admin': 'px_admin',
'Hospital Admin': 'hospital_admin',
'Department Manager': 'department_manager',
'PX Coordinator': 'px_coordinator',
'Physician': 'physician',
'Nurse': 'nurse',
'Staff': 'staff',
'Viewer': 'viewer',
}
role = role_mapping.get(group_name, group_name.lower().replace(' ', '_'))
# Get content for user's role or all roles
content = AcknowledgementContent.objects.filter(is_active=True).filter(
models.Q(role=role) | models.Q(role__isnull=True)
db_models.Q(role=role) | db_models.Q(role__isnull=True)
).order_by('order')
return content
@ -134,16 +148,26 @@ class OnboardingService:
Returns:
QuerySet of AcknowledgementChecklistItem
"""
from django.db import models
# Get user's role
# Get user's role - convert group name to role code
role = None
if user.groups.exists():
role = user.groups.first().name
group_name = user.groups.first().name
# Map group names to role codes
role_mapping = {
'PX Admin': 'px_admin',
'Hospital Admin': 'hospital_admin',
'Department Manager': 'department_manager',
'PX Coordinator': 'px_coordinator',
'Physician': 'physician',
'Nurse': 'nurse',
'Staff': 'staff',
'Viewer': 'viewer',
}
role = role_mapping.get(group_name, group_name.lower().replace(' ', '_'))
# Get items for user's role or all roles
items = AcknowledgementChecklistItem.objects.filter(is_active=True).filter(
models.Q(role=role) | models.Q(role__isnull=True)
db_models.Q(role=role) | db_models.Q(role__isnull=True)
).order_by('order')
return items
@ -452,7 +476,7 @@ class EmailService:
Boolean indicating success
"""
base_url = getattr(settings, 'BASE_URL', 'http://localhost:8000')
user_detail_url = f"{base_url}/accounts/management/progress/{user.id}/"
user_detail_url = f"{base_url}/accounts/onboarding/provisional/{user.id}/progress/"
# Render email content
context = {

View File

@ -75,9 +75,9 @@
</div>
<div class="d-grid gap-2">
<a href="/login/" class="btn btn-primary btn-lg">
<a href="/" class="btn btn-primary btn-lg">
<i class="bi bi-box-arrow-in-right me-2"></i>
{% trans "Log In to PX360" %}
{% trans "Go to Dashboard" %}
</a>
</div>

View File

@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Onboarding Completed</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: #ffffff;
border-radius: 8px;
padding: 40px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
font-size: 28px;
font-weight: bold;
color: #0d6efd;
}
h1 {
color: #198754;
font-size: 24px;
margin-bottom: 20px;
}
.content {
margin-bottom: 30px;
}
.success-icon {
font-size: 48px;
text-align: center;
margin-bottom: 20px;
}
.user-info {
background-color: #f8f9fa;
border-radius: 6px;
padding: 20px;
margin: 20px 0;
}
.user-info table {
width: 100%;
border-collapse: collapse;
}
.user-info td {
padding: 8px 0;
border-bottom: 1px solid #dee2e6;
}
.user-info td:first-child {
font-weight: bold;
color: #666;
width: 40%;
}
.user-info tr:last-child td {
border-bottom: none;
}
.button {
display: inline-block;
background-color: #0d6efd;
color: #ffffff !important;
text-decoration: none;
padding: 12px 24px;
border-radius: 6px;
font-weight: bold;
font-size: 14px;
margin: 10px 0;
}
.button:hover {
background-color: #0b5ed7;
}
.button-container {
text-align: center;
margin: 30px 0;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 12px;
color: #666;
text-align: center;
}
.timestamp {
font-size: 12px;
color: #666;
text-align: center;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">PX360</div>
<p style="color: #666; margin-top: 5px;">Patient Experience Platform</p>
</div>
<div class="success-icon"></div>
<h1>User Onboarding Completed</h1>
<div class="content">
<p>A new user has successfully completed the onboarding process and is now active in the PX360 system.</p>
<div class="user-info">
<table>
<tr>
<td>Name:</td>
<td>{{ user.get_full_name|default:"Not provided" }}</td>
</tr>
<tr>
<td>Email:</td>
<td>{{ user.email }}</td>
</tr>
<tr>
<td>Username:</td>
<td>{{ user.username }}</td>
</tr>
<tr>
<td>Employee ID:</td>
<td>{{ user.employee_id|default:"Not provided" }}</td>
</tr>
<tr>
<td>Hospital:</td>
<td>{{ user.hospital.name|default:"Not assigned" }}</td>
</tr>
<tr>
<td>Department:</td>
<td>{{ user.department.name|default:"Not assigned" }}</td>
</tr>
<tr>
<td>Completed At:</td>
<td>{{ user.acknowledgement_completed_at|date:"F j, Y, g:i a" }}</td>
</tr>
</table>
</div>
<div class="button-container">
<a href="{{ user_detail_url }}" class="button">View User Details</a>
</div>
</div>
<p class="timestamp">
This notification was sent on {{ "now"|date:"F j, Y, g:i a" }}
</p>
<div class="footer">
<p>This is an automated notification from PX360.</p>
<p>&copy; {{ "now"|date:"Y" }} PX360 - Patient Experience Platform</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,22 @@
User Onboarding Completed
==========================
A new user has successfully completed the onboarding process and is now active in the PX360 system.
USER DETAILS
------------
Name: {{ user.get_full_name|default:"Not provided" }}
Email: {{ user.email }}
Username: {{ user.username }}
Employee ID: {{ user.employee_id|default:"Not provided" }}
Hospital: {{ user.hospital.name|default:"Not assigned" }}
Department: {{ user.department.name|default:"Not assigned" }}
Completed At: {{ user.acknowledgement_completed_at|date:"F j, Y, g:i a" }}
View user details: {{ user_detail_url }}
---
This is an automated notification from PX360.
© PX360 - Patient Experience Platform

View File

@ -0,0 +1 @@
PX360: {{ user.get_full_name|default:user.email }} has completed onboarding

View File

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to PX360</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: #ffffff;
border-radius: 8px;
padding: 40px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
font-size: 28px;
font-weight: bold;
color: #0d6efd;
}
h1 {
color: #0d6efd;
font-size: 24px;
margin-bottom: 20px;
}
.greeting {
font-size: 18px;
margin-bottom: 20px;
}
.content {
margin-bottom: 30px;
}
.button {
display: inline-block;
background-color: #0d6efd;
color: #ffffff !important;
text-decoration: none;
padding: 14px 30px;
border-radius: 6px;
font-weight: bold;
font-size: 16px;
margin: 20px 0;
}
.button:hover {
background-color: #0b5ed7;
}
.button-container {
text-align: center;
margin: 30px 0;
}
.expiry-notice {
background-color: #fff3cd;
border: 1px solid #ffc107;
border-radius: 6px;
padding: 15px;
margin: 20px 0;
font-size: 14px;
}
.expiry-notice strong {
color: #856404;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 12px;
color: #666;
text-align: center;
}
.link-fallback {
font-size: 12px;
color: #666;
word-break: break-all;
margin-top: 15px;
}
ul {
padding-left: 20px;
}
li {
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">PX360</div>
<p style="color: #666; margin-top: 5px;">Patient Experience Platform</p>
</div>
<h1>Welcome to PX360!</h1>
<p class="greeting">Hello {{ user.first_name|default:user.email }},</p>
<div class="content">
<p>You have been invited to join PX360, our comprehensive Patient Experience management platform. To complete your account setup, please click the button below:</p>
<div class="button-container">
<a href="{{ activation_url }}" class="button">Complete Account Setup</a>
</div>
<p>During the onboarding process, you will:</p>
<ul>
<li>Learn about PX360 features and your role responsibilities</li>
<li>Review and acknowledge important policies and guidelines</li>
<li>Set up your username and password</li>
<li>Complete your profile information</li>
</ul>
</div>
<div class="expiry-notice">
<strong>⏰ Important:</strong> This invitation link will expire on <strong>{{ expires_at|date:"F j, Y, g:i a" }}</strong>. Please complete your registration before this date.
</div>
<p class="link-fallback">
If the button above doesn't work, copy and paste this link into your browser:<br>
<a href="{{ activation_url }}">{{ activation_url }}</a>
</p>
<div class="footer">
<p>This is an automated message from PX360. Please do not reply to this email.</p>
<p>If you did not expect this invitation or have questions, please contact your system administrator.</p>
<p>&copy; {{ "now"|date:"Y" }} PX360 - Patient Experience Platform</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,26 @@
Welcome to PX360!
==================
Hello {{ user.first_name|default:user.email }},
You have been invited to join PX360, our comprehensive Patient Experience management platform.
To complete your account setup, please visit the following link:
{{ activation_url }}
During the onboarding process, you will:
- Learn about PX360 features and your role responsibilities
- Review and acknowledge important policies and guidelines
- Set up your username and password
- Complete your profile information
IMPORTANT: This invitation link will expire on {{ expires_at|date:"F j, Y, g:i a" }}.
Please complete your registration before this date.
---
This is an automated message from PX360. Please do not reply to this email.
If you did not expect this invitation or have questions, please contact your system administrator.
© PX360 - Patient Experience Platform

View File

@ -0,0 +1 @@
Welcome to PX360 - Complete Your Account Setup

View File

@ -155,7 +155,7 @@
<i class="bi bi-activity"></i>
</a>
<button class="btn btn-sm btn-outline-danger"
onclick="resendInvitation({{ user.id }})"
onclick="resendInvitation('{{ user.id }}')"
title="{% trans 'Resend Invitation' %}">
<i class="bi bi-send"></i>
</button>

View File

@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reminder: Complete Your PX360 Account Setup</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: #ffffff;
border-radius: 8px;
padding: 40px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
font-size: 28px;
font-weight: bold;
color: #0d6efd;
}
h1 {
color: #fd7e14;
font-size: 24px;
margin-bottom: 20px;
}
.greeting {
font-size: 18px;
margin-bottom: 20px;
}
.content {
margin-bottom: 30px;
}
.button {
display: inline-block;
background-color: #0d6efd;
color: #ffffff !important;
text-decoration: none;
padding: 14px 30px;
border-radius: 6px;
font-weight: bold;
font-size: 16px;
margin: 20px 0;
}
.button:hover {
background-color: #0b5ed7;
}
.button-container {
text-align: center;
margin: 30px 0;
}
.expiry-notice {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 6px;
padding: 15px;
margin: 20px 0;
font-size: 14px;
}
.expiry-notice strong {
color: #721c24;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 12px;
color: #666;
text-align: center;
}
.link-fallback {
font-size: 12px;
color: #666;
word-break: break-all;
margin-top: 15px;
}
.reminder-icon {
font-size: 48px;
text-align: center;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">PX360</div>
<p style="color: #666; margin-top: 5px;">Patient Experience Platform</p>
</div>
<div class="reminder-icon"></div>
<h1>Reminder: Complete Your Account Setup</h1>
<p class="greeting">Hello {{ user.first_name|default:user.email }},</p>
<div class="content">
<p>We noticed that you haven't completed your PX360 account setup yet. Your invitation is still active, and we'd love to have you on board!</p>
<p>Click the button below to continue where you left off:</p>
<div class="button-container">
<a href="{{ activation_url }}" class="button">Complete Account Setup</a>
</div>
</div>
<div class="expiry-notice">
<strong>⚠️ Time Sensitive:</strong> Your invitation link will expire on <strong>{{ expires_at|date:"F j, Y, g:i a" }}</strong>. Please complete your registration before this date to avoid requesting a new invitation.
</div>
<p class="link-fallback">
If the button above doesn't work, copy and paste this link into your browser:<br>
<a href="{{ activation_url }}">{{ activation_url }}</a>
</p>
<div class="footer">
<p>This is an automated reminder from PX360. Please do not reply to this email.</p>
<p>If you have already completed your registration, please disregard this message.</p>
<p>If you have questions, please contact your system administrator.</p>
<p>&copy; {{ "now"|date:"Y" }} PX360 - Patient Experience Platform</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,21 @@
Reminder: Complete Your PX360 Account Setup
============================================
Hello {{ user.first_name|default:user.email }},
We noticed that you haven't completed your PX360 account setup yet. Your invitation is still active, and we'd love to have you on board!
To continue where you left off, please visit the following link:
{{ activation_url }}
TIME SENSITIVE: Your invitation link will expire on {{ expires_at|date:"F j, Y, g:i a" }}.
Please complete your registration before this date to avoid requesting a new invitation.
---
This is an automated reminder from PX360. Please do not reply to this email.
If you have already completed your registration, please disregard this message.
If you have questions, please contact your system administrator.
© PX360 - Patient Experience Platform

View File

@ -0,0 +1 @@
Reminder: Complete Your PX360 Account Setup

View File

@ -21,6 +21,7 @@
</div>
<form id="activationForm">
{% csrf_token %}
<div class="mb-3">
<label for="username" class="form-label">
<i class="bi bi-person me-2"></i>{% trans "Username" %}
@ -31,7 +32,7 @@
name="username"
required
minlength="3"
pattern="[a-zA-Z0-9_-]+"
pattern="[a-zA-Z0-9_\-]+"
title="{% trans 'Username can only contain letters, numbers, underscores, and hyphens' %}">
<div class="form-text">
{% trans "Choose a unique username (3+ characters)" %}
@ -170,6 +171,22 @@ function checkPasswordMatch() {
return true;
}
// Helper function to get cookie value
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function activateAccount() {
const form = document.getElementById('activationForm');
const username = document.getElementById('username').value;
@ -196,11 +213,15 @@ function activateAccount() {
activateBtn.disabled = true;
activateBtn.innerHTML = '<i class="bi bi-arrow-clockwise fa-spin me-2"></i>{% trans "Activating..." %}';
fetch('/api/accounts/users/onboarding/complete/', {
// Get CSRF token
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value ||
getCookie('csrftoken');
fetch('/accounts/onboarding/complete-activation/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
username: username,
@ -211,7 +232,7 @@ function activateAccount() {
})
.then(response => response.json())
.then(data => {
if (data.message === 'Account activated successfully') {
if (data.success) {
// Clear stored signature
localStorage.removeItem('onboardingSignature');
@ -224,6 +245,7 @@ function activateAccount() {
}
})
.catch(error => {
console.error('Error:', error);
activateBtn.disabled = false;
activateBtn.innerHTML = '<i class="bi bi-rocket-takeoff me-2"></i>{% trans "Activate Account" %}';
alert('{% trans "An error occurred. Please try again." %}');