634 lines
24 KiB
HTML
634 lines
24 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "Acknowledgement Checklist" %}{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.signature-pad-container {
|
|
position: relative;
|
|
border: 2px dashed #dee2e6;
|
|
border-radius: 8px;
|
|
background: #f8f9fa;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.signature-pad-container.signing {
|
|
border-color: #0d6efd;
|
|
background: #fff;
|
|
}
|
|
|
|
#signatureCanvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: 250px;
|
|
cursor: crosshair;
|
|
touch-action: none;
|
|
}
|
|
|
|
.signature-placeholder {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: #adb5bd;
|
|
font-size: 1.1rem;
|
|
pointer-events: none;
|
|
text-align: center;
|
|
}
|
|
|
|
.signature-placeholder.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.signature-tools {
|
|
background: #fff;
|
|
border-top: 1px solid #dee2e6;
|
|
padding: 0.75rem;
|
|
}
|
|
|
|
.pen-color-btn {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
border: 2px solid transparent;
|
|
padding: 0;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.pen-color-btn:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.pen-color-btn.active {
|
|
border-color: #495057;
|
|
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #0d6efd;
|
|
}
|
|
|
|
.signature-validation {
|
|
min-height: 24px;
|
|
}
|
|
|
|
.checklist-item {
|
|
transition: all 0.2s ease;
|
|
border-left: 4px solid transparent;
|
|
}
|
|
|
|
.checklist-item:hover {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.checklist-item.acknowledged {
|
|
border-left-color: #198754;
|
|
background-color: #f8fff9;
|
|
}
|
|
|
|
.checklist-item.pending {
|
|
border-left-color: #ffc107;
|
|
}
|
|
|
|
.progress-step {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.progress-step.completed {
|
|
background-color: #198754;
|
|
color: white;
|
|
}
|
|
|
|
.progress-step.pending {
|
|
background-color: #e9ecef;
|
|
color: #6c757d;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<!-- Progress Header -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div class="d-flex align-items-center gap-4">
|
|
<div class="progress-step {% if acknowledged_count > 0 %}completed{% else %}pending{% endif %}">
|
|
<i class="bi bi-{% if acknowledged_count > 0 %}check-lg{% else %}1-circle{% endif %}"></i>
|
|
</div>
|
|
<div>
|
|
<h4 class="mb-0">{% trans "Acknowledgement Checklist" %}</h4>
|
|
<p class="text-muted mb-0">
|
|
{% blocktrans %}{{ acknowledged_count }} of {{ total_count }} items completed{% endblocktrans %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-end">
|
|
<div class="progress" style="width: 200px; height: 10px;">
|
|
<div class="progress-bar bg-success transition-width" role="progressbar"
|
|
style="width: {{ progress_percentage }}%; transition: width 0.5s ease;"
|
|
aria-valuenow="{{ progress_percentage }}"
|
|
aria-valuemin="0" aria-valuemax="100">
|
|
</div>
|
|
</div>
|
|
<small class="text-muted">{{ progress_percentage }}% {% trans "Complete" %}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<!-- Progress Sidebar -->
|
|
<div class="col-lg-3">
|
|
<div class="card sticky-top" style="top: 20px; z-index: 100;">
|
|
<div class="card-body">
|
|
<h6 class="card-title text-muted text-uppercase small mb-3">{% trans "Progress" %}</h6>
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<span>{% trans "Completed" %}</span>
|
|
<span class="badge bg-success">{{ acknowledged_count }}</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<span>{% trans "Remaining" %}</span>
|
|
<span class="badge bg-warning text-dark">{{ total_count|add:"-"|add:acknowledged_count }}</span>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div class="small text-muted">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
{% trans "Complete all required items to proceed" %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Checklist -->
|
|
<div class="col-lg-9">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header bg-white border-bottom">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-clipboard-check text-primary me-2"></i>
|
|
{% trans "Required Acknowledgements" %}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="alert alert-warning d-flex align-items-center mb-4">
|
|
<i class="bi bi-exclamation-triangle-fill me-2 fs-5"></i>
|
|
<div>
|
|
{% trans "Please review and acknowledge all required items below. Your digital signature will be recorded for compliance purposes." %}
|
|
</div>
|
|
</div>
|
|
|
|
<form id="checklistForm">
|
|
<div class="list-group mb-4">
|
|
{% for item in checklist_items %}
|
|
<div class="list-group-item checklist-item {% if item.is_acknowledged %}acknowledged{% else %}pending{% endif %} mb-2 rounded">
|
|
<div class="d-flex align-items-start gap-3">
|
|
<div class="form-check flex-grow-1 pt-1">
|
|
<input class="form-check-input fs-5"
|
|
type="checkbox"
|
|
id="item_{{ item.id }}"
|
|
value="{{ item.id }}"
|
|
{% if item.is_acknowledged %}checked disabled{% endif %}
|
|
onchange="toggleItem(this, '{{ item.id }}')"
|
|
{% if not item.is_required %}data-required="false"{% endif %}>
|
|
<label class="form-check-label ms-2" for="item_{{ item.id }}">
|
|
<span class="fw-semibold">
|
|
{% if request.user.language == 'ar' %}{{ item.text_ar }}{% else %}{{ item.text_en }}{% endif %}
|
|
</span>
|
|
{% if item.is_required %}
|
|
<span class="badge bg-danger ms-2">{% trans "Required" %}</span>
|
|
{% endif %}
|
|
</label>
|
|
|
|
{% if item.description_en %}
|
|
<p class="text-muted small mt-2 mb-0">
|
|
{% if request.user.language == 'ar' %}{{ item.description_ar }}{% else %}{{ item.description_en }}{% endif %}
|
|
</p>
|
|
{% endif %}
|
|
|
|
{% if item.content %}
|
|
<p class="text-info small mb-0 mt-1">
|
|
<i class="bi bi-link-45deg me-1"></i>
|
|
{% if request.user.language == 'ar' %}{{ item.content.title_ar }}{% else %}{{ item.content.title_en }}{% endif %}
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if item.is_acknowledged %}
|
|
<div class="text-success">
|
|
<i class="bi bi-check-circle-fill fs-4"></i>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Enhanced Signature Section -->
|
|
<div id="signatureSection" class="mb-4" style="display: none;">
|
|
<hr class="my-4">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<i class="bi bi-pen-fill text-primary me-2 fs-5"></i>
|
|
<h5 class="mb-0">{% trans "Digital Signature" %}</h5>
|
|
</div>
|
|
|
|
<div class="alert alert-info d-flex">
|
|
<i class="bi bi-info-circle-fill me-2 mt-1"></i>
|
|
<div>
|
|
{% trans "By providing your digital signature below, you acknowledge that you have read, understood, and agreed to all the items listed above. Your signature will be recorded with your IP address and timestamp for compliance purposes." %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Signature Pad -->
|
|
<div class="signature-pad-container mb-2" id="signatureContainer">
|
|
<canvas id="signatureCanvas"></canvas>
|
|
<div class="signature-placeholder" id="signaturePlaceholder">
|
|
<i class="bi bi-signature fs-1 d-block mb-2"></i>
|
|
{% trans "Sign here using your mouse or finger" %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Signature Tools -->
|
|
<div class="signature-tools d-flex flex-wrap align-items-center justify-content-between gap-2">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="text-muted small">{% trans "Pen Color:" %}</span>
|
|
<button type="button" class="pen-color-btn active" style="background-color: #000000;" data-color="#000000" title="{% trans 'Black' %}"></button>
|
|
<button type="button" class="pen-color-btn" style="background-color: #0d6efd;" data-color="#0d6efd" title="{% trans 'Blue' %}"></button>
|
|
<button type="button" class="pen-color-btn" style="background-color: #dc3545;" data-color="#dc3545" title="{% trans 'Red' %}"></button>
|
|
|
|
<div class="vr mx-2"></div>
|
|
|
|
<span class="text-muted small">{% trans "Size:" %}</span>
|
|
<input type="range" class="form-range" id="penSize" min="1" max="5" value="2" style="width: 80px;">
|
|
</div>
|
|
|
|
<div class="d-flex gap-2">
|
|
<button type="button" onclick="undoSignature()" class="btn btn-sm btn-outline-secondary" id="undoBtn" disabled>
|
|
<i class="bi bi-arrow-counterclockwise me-1"></i> {% trans "Undo" %}
|
|
</button>
|
|
<button type="button" onclick="clearSignature()" class="btn btn-sm btn-outline-danger">
|
|
<i class="bi bi-eraser me-1"></i> {% trans "Clear" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Validation Message -->
|
|
<div class="signature-validation mt-2">
|
|
<div id="signatureValidationMsg" class="text-danger small" style="display: none;">
|
|
<i class="bi bi-exclamation-circle me-1"></i>
|
|
{% trans "Please provide your signature before proceeding." %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-grid gap-2 d-md-flex justify-content-md-between">
|
|
<button type="button" onclick="goBack()" class="btn btn-outline-secondary">
|
|
<i class="bi bi-arrow-left me-2"></i>
|
|
{% trans "Back" %}
|
|
</button>
|
|
<button type="button" id="proceedBtn"
|
|
onclick="proceedToActivation()"
|
|
class="btn btn-primary btn-lg"
|
|
disabled>
|
|
<span class="d-flex align-items-center justify-content-center">
|
|
{% trans "Proceed to Account Setup" %}
|
|
<i class="bi bi-arrow-right ms-2"></i>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Configuration
|
|
let requiredItems = {{ required_items_json|safe }};
|
|
let acknowledgedItems = new Set();
|
|
let isDrawing = false;
|
|
let currentPenColor = '#000000';
|
|
let currentPenSize = 2;
|
|
let drawHistory = [];
|
|
let currentPath = [];
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Load acknowledged items
|
|
{% for item in checklist_items %}
|
|
{% if item.is_acknowledged %}
|
|
acknowledgedItems.add('{{ item.id }}');
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
checkProgress();
|
|
initEnhancedSignaturePad();
|
|
initPenTools();
|
|
});
|
|
|
|
function toggleItem(checkbox, itemId) {
|
|
const itemDiv = checkbox.closest('.checklist-item');
|
|
|
|
if (checkbox.checked) {
|
|
acknowledgedItems.add(itemId);
|
|
itemDiv.classList.remove('pending');
|
|
itemDiv.classList.add('acknowledged');
|
|
} else {
|
|
acknowledgedItems.delete(itemId);
|
|
itemDiv.classList.remove('acknowledged');
|
|
itemDiv.classList.add('pending');
|
|
}
|
|
|
|
// Visual feedback
|
|
checkbox.disabled = true;
|
|
|
|
// Save acknowledgement via API
|
|
fetch('/api/accounts/onboarding/acknowledge/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': getCookie('csrftoken')
|
|
},
|
|
body: JSON.stringify({
|
|
checklist_item_id: itemId
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
checkProgress();
|
|
} else {
|
|
checkbox.checked = !checkbox.checked;
|
|
checkbox.disabled = false;
|
|
itemDiv.classList.toggle('acknowledged');
|
|
itemDiv.classList.toggle('pending');
|
|
alert('{% trans "Error: " %}' + (data.error || '{% trans "Failed to save acknowledgement" %}'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
checkbox.checked = !checkbox.checked;
|
|
checkbox.disabled = false;
|
|
itemDiv.classList.toggle('acknowledged');
|
|
itemDiv.classList.toggle('pending');
|
|
console.error('Error:', error);
|
|
});
|
|
}
|
|
|
|
function checkProgress() {
|
|
let allRequiredAck = true;
|
|
requiredItems.forEach(id => {
|
|
if (!acknowledgedItems.has(id)) {
|
|
allRequiredAck = false;
|
|
}
|
|
});
|
|
|
|
const proceedBtn = document.getElementById('proceedBtn');
|
|
const signatureSection = document.getElementById('signatureSection');
|
|
|
|
if (allRequiredAck) {
|
|
proceedBtn.disabled = false;
|
|
signatureSection.style.display = 'block';
|
|
// Smooth scroll to signature
|
|
setTimeout(() => {
|
|
signatureSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}, 300);
|
|
} else {
|
|
proceedBtn.disabled = true;
|
|
signatureSection.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Enhanced Signature Pad
|
|
function initEnhancedSignaturePad() {
|
|
const canvas = document.getElementById('signatureCanvas');
|
|
const container = document.getElementById('signatureContainer');
|
|
const placeholder = document.getElementById('signaturePlaceholder');
|
|
|
|
// Handle high DPI displays
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const rect = container.getBoundingClientRect();
|
|
|
|
canvas.width = rect.width * dpr;
|
|
canvas.height = 250 * dpr;
|
|
canvas.style.width = rect.width + 'px';
|
|
canvas.style.height = '250px';
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.scale(dpr, dpr);
|
|
ctx.lineCap = 'round';
|
|
ctx.lineJoin = 'round';
|
|
|
|
// Drawing state
|
|
let lastX = 0;
|
|
let lastY = 0;
|
|
|
|
function getPos(e) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
return {
|
|
x: clientX - rect.left,
|
|
y: clientY - rect.top
|
|
};
|
|
}
|
|
|
|
function startDrawing(e) {
|
|
e.preventDefault();
|
|
isDrawing = true;
|
|
const pos = getPos(e);
|
|
lastX = pos.x;
|
|
lastY = pos.y;
|
|
|
|
currentPath = [{x: pos.x, y: pos.y, color: currentPenColor, size: currentPenSize}];
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(pos.x, pos.y);
|
|
|
|
// Hide placeholder
|
|
placeholder.classList.add('hidden');
|
|
container.classList.add('signing');
|
|
|
|
// Clear validation message
|
|
document.getElementById('signatureValidationMsg').style.display = 'none';
|
|
}
|
|
|
|
function draw(e) {
|
|
if (!isDrawing) return;
|
|
e.preventDefault();
|
|
|
|
const pos = getPos(e);
|
|
|
|
ctx.lineWidth = currentPenSize;
|
|
ctx.strokeStyle = currentPenColor;
|
|
|
|
// Smooth curve drawing
|
|
ctx.beginPath();
|
|
ctx.moveTo(lastX, lastY);
|
|
ctx.lineTo(pos.x, pos.y);
|
|
ctx.stroke();
|
|
|
|
currentPath.push({x: pos.x, y: pos.y, color: currentPenColor, size: currentPenSize});
|
|
|
|
lastX = pos.x;
|
|
lastY = pos.y;
|
|
}
|
|
|
|
function stopDrawing() {
|
|
if (!isDrawing) return;
|
|
isDrawing = false;
|
|
|
|
if (currentPath.length > 0) {
|
|
drawHistory.push([...currentPath]);
|
|
updateUndoButton();
|
|
}
|
|
currentPath = [];
|
|
container.classList.remove('signing');
|
|
}
|
|
|
|
// Mouse events
|
|
canvas.addEventListener('mousedown', startDrawing);
|
|
canvas.addEventListener('mousemove', draw);
|
|
canvas.addEventListener('mouseup', stopDrawing);
|
|
canvas.addEventListener('mouseout', stopDrawing);
|
|
|
|
// Touch events with passive: false for better control
|
|
canvas.addEventListener('touchstart', startDrawing, { passive: false });
|
|
canvas.addEventListener('touchmove', draw, { passive: false });
|
|
canvas.addEventListener('touchend', stopDrawing);
|
|
canvas.addEventListener('touchcancel', stopDrawing);
|
|
|
|
// Store references
|
|
window.signatureCanvas = canvas;
|
|
window.signatureCtx = ctx;
|
|
window.drawHistory = drawHistory;
|
|
}
|
|
|
|
function initPenTools() {
|
|
// Color picker
|
|
document.querySelectorAll('.pen-color-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
document.querySelectorAll('.pen-color-btn').forEach(b => b.classList.remove('active'));
|
|
this.classList.add('active');
|
|
currentPenColor = this.dataset.color;
|
|
});
|
|
});
|
|
|
|
// Pen size
|
|
const sizeSlider = document.getElementById('penSize');
|
|
sizeSlider.addEventListener('input', function() {
|
|
currentPenSize = parseInt(this.value);
|
|
});
|
|
}
|
|
|
|
function undoSignature() {
|
|
if (drawHistory.length === 0) return;
|
|
|
|
drawHistory.pop();
|
|
redrawCanvas();
|
|
updateUndoButton();
|
|
|
|
// Show placeholder if empty
|
|
if (drawHistory.length === 0) {
|
|
document.getElementById('signaturePlaceholder').classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function redrawCanvas() {
|
|
const canvas = window.signatureCanvas;
|
|
const ctx = window.signatureCtx;
|
|
|
|
ctx.clearRect(0, 0, canvas.width / (window.devicePixelRatio || 1), canvas.height / (window.devicePixelRatio || 1));
|
|
|
|
drawHistory.forEach(path => {
|
|
if (path.length < 2) return;
|
|
|
|
ctx.beginPath();
|
|
ctx.lineWidth = path[0].size;
|
|
ctx.strokeStyle = path[0].color;
|
|
ctx.moveTo(path[0].x, path[0].y);
|
|
|
|
for (let i = 1; i < path.length; i++) {
|
|
ctx.lineTo(path[i].x, path[i].y);
|
|
}
|
|
ctx.stroke();
|
|
});
|
|
}
|
|
|
|
function updateUndoButton() {
|
|
const undoBtn = document.getElementById('undoBtn');
|
|
undoBtn.disabled = drawHistory.length === 0;
|
|
}
|
|
|
|
function clearSignature() {
|
|
const canvas = window.signatureCanvas;
|
|
const ctx = window.signatureCtx;
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
drawHistory.length = 0;
|
|
updateUndoButton();
|
|
|
|
document.getElementById('signaturePlaceholder').classList.remove('hidden');
|
|
document.getElementById('signatureValidationMsg').style.display = 'none';
|
|
}
|
|
|
|
function isSignatureEmpty() {
|
|
return drawHistory.length === 0;
|
|
}
|
|
|
|
function proceedToActivation() {
|
|
if (isSignatureEmpty()) {
|
|
document.getElementById('signatureValidationMsg').style.display = 'block';
|
|
document.getElementById('signatureContainer').scrollIntoView({ behavior: 'smooth' });
|
|
return;
|
|
}
|
|
|
|
const canvas = window.signatureCanvas;
|
|
const signatureData = canvas.toDataURL('image/png');
|
|
|
|
// Store signature for final submission
|
|
sessionStorage.setItem('onboardingSignature', signatureData);
|
|
|
|
window.location.href = '/accounts/onboarding/wizard/activation/';
|
|
}
|
|
|
|
function goBack() {
|
|
window.location.href = '/accounts/onboarding/wizard/step/1/';
|
|
}
|
|
|
|
// CSRF Token helper
|
|
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;
|
|
}
|
|
|
|
// Handle window resize
|
|
let resizeTimeout;
|
|
window.addEventListener('resize', function() {
|
|
clearTimeout(resizeTimeout);
|
|
resizeTimeout = setTimeout(function() {
|
|
// Redraw canvas on resize
|
|
if (window.signatureCanvas && drawHistory.length > 0) {
|
|
redrawCanvas();
|
|
}
|
|
}, 250);
|
|
});
|
|
</script>
|
|
{% endblock %}
|