317 lines
12 KiB
HTML
317 lines
12 KiB
HTML
{% load i18n %}
|
|
<!DOCTYPE html>
|
|
<html lang="{% get_current_language as LANGUAGE_CODE %}{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
<title>{% block title %}{% trans "PX360 - Patient Experience Management" %}{% endblock %}</title>
|
|
|
|
<!-- TailwindCSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- Lucide Icons -->
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
|
|
<!-- Google Fonts - Inter -->
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<!-- ApexCharts -->
|
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.45.1/dist/apexcharts.min.js"></script>
|
|
|
|
<!-- HTMX for dynamic updates -->
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
|
|
<!-- Tailwind Config -->
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
'inter': ['Inter', 'sans-serif'],
|
|
},
|
|
colors: {
|
|
'navy': '#005696', /* Primary Al Hammadi Blue */
|
|
'blue': '#007bbd', /* Accent Blue */
|
|
'light': '#eef6fb', /* Background Soft Blue */
|
|
'slate': '#64748b', /* Secondary text */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<!-- Custom Styles -->
|
|
<style>
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
/* Custom scrollbar */
|
|
::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 3px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #94a3b8;
|
|
}
|
|
|
|
/* RTL adjustments */
|
|
[dir="rtl"] .main-content {
|
|
margin-left: 0;
|
|
margin-right: 5rem;
|
|
}
|
|
|
|
[dir="rtl"] #sidebar {
|
|
left: auto;
|
|
right: 0;
|
|
}
|
|
|
|
/* Main content margin for narrow sidebar */
|
|
.main-content {
|
|
margin-left: 5rem;
|
|
transition: margin-left 0.3s ease;
|
|
}
|
|
|
|
/* When sidebar expands on hover */
|
|
#sidebar:hover ~ .main-content,
|
|
#sidebar:hover + .main-content {
|
|
margin-left: 16rem;
|
|
}
|
|
|
|
[dir="rtl"] #sidebar:hover ~ .main-content,
|
|
[dir="rtl"] #sidebar:hover + .main-content {
|
|
margin-left: 0;
|
|
margin-right: 16rem;
|
|
}
|
|
|
|
/* Responsive sidebar */
|
|
@media (max-width: 1024px) {
|
|
#sidebar {
|
|
transform: translateX(-100%);
|
|
transition: transform 0.3s ease;
|
|
width: 16rem !important;
|
|
}
|
|
#sidebar.show {
|
|
transform: translateX(0);
|
|
}
|
|
[dir="rtl"] #sidebar {
|
|
transform: translateX(100%);
|
|
}
|
|
[dir="rtl"] #sidebar.show {
|
|
transform: translateX(0);
|
|
}
|
|
.main-content {
|
|
margin-left: 0 !important;
|
|
margin-right: 0 !important;
|
|
}
|
|
}
|
|
|
|
/* Form styles */
|
|
.form-input {
|
|
@apply w-full px-4 py-2.5 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent transition;
|
|
}
|
|
|
|
.form-label {
|
|
@apply block text-sm font-medium text-gray-700 mb-1.5;
|
|
}
|
|
|
|
.form-select {
|
|
@apply w-full px-4 py-2.5 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-navy focus:border-transparent transition bg-white;
|
|
}
|
|
|
|
/* Card hover effects */
|
|
.card-hover {
|
|
@apply transition duration-200 hover:shadow-lg hover:-translate-y-0.5;
|
|
}
|
|
|
|
/* Button transitions */
|
|
.btn-transition {
|
|
@apply transition duration-200 ease-in-out;
|
|
}
|
|
|
|
/* Toast notifications */
|
|
.toast {
|
|
@apply fixed top-4 right-4 z-50 p-4 rounded-xl shadow-lg transform transition-all duration-300;
|
|
}
|
|
.toast.success {
|
|
@apply bg-green-50 text-green-800 border border-green-200;
|
|
}
|
|
.toast.error {
|
|
@apply bg-red-50 text-red-800 border border-red-200;
|
|
}
|
|
.toast.warning {
|
|
@apply bg-yellow-50 text-yellow-800 border border-yellow-200;
|
|
}
|
|
</style>
|
|
|
|
{% block extra_css %}{% endblock %}
|
|
</head>
|
|
<body class="bg-slate-50 text-gray-800 min-h-screen">
|
|
|
|
<div class="flex h-screen">
|
|
<!-- Sidebar -->
|
|
{% include 'layouts/partials/sidebar.html' %}
|
|
|
|
<!-- Main Content Area -->
|
|
<div class="flex-1 flex flex-col main-content overflow-hidden">
|
|
<!-- Page Content -->
|
|
<main class="flex-1 overflow-y-auto p-8">
|
|
<!-- Flash Messages -->
|
|
{% include 'layouts/partials/flash_messages.html' %}
|
|
|
|
<!-- Page Content Block -->
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile sidebar toggle button -->
|
|
<button onclick="toggleSidebar()" class="fixed bottom-4 right-4 z-50 lg:hidden bg-navy text-white p-3 rounded-full shadow-lg">
|
|
<i data-lucide="menu" class="w-6 h-6"></i>
|
|
</button>
|
|
|
|
<!-- CSRF Token for JavaScript -->
|
|
{% csrf_token %}
|
|
<script>
|
|
// Get CSRF token for AJAX requests
|
|
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;
|
|
}
|
|
|
|
// Initialize Lucide icons
|
|
lucide.createIcons();
|
|
|
|
// Mobile sidebar toggle
|
|
function toggleSidebar() {
|
|
document.getElementById('sidebar').classList.toggle('show');
|
|
}
|
|
|
|
// Close sidebar when clicking outside on mobile
|
|
document.addEventListener('click', function(e) {
|
|
if (window.innerWidth < 1024) {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const toggleBtn = document.querySelector('[onclick="toggleSidebar()"]');
|
|
if (sidebar && !sidebar.contains(e.target) && !toggleBtn?.contains(e.target)) {
|
|
sidebar.classList.remove('show');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Notification dropdown handler
|
|
document.addEventListener('click', function(e) {
|
|
const notificationBtn = e.target.closest('[onclick*="notificationDropdown"]');
|
|
if (notificationBtn) {
|
|
loadNotifications();
|
|
}
|
|
});
|
|
|
|
// Load notifications for dropdown
|
|
async function loadNotifications() {
|
|
try {
|
|
const response = await fetch('/notifications/api/latest/');
|
|
const data = await response.json();
|
|
const container = document.getElementById('notificationDropdownContent');
|
|
|
|
if (!container) return;
|
|
|
|
if (data.notifications.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="p-8 text-center">
|
|
<div class="bg-gray-100 p-4 rounded-full w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-gray-400"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"></path><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"></path></svg>
|
|
</div>
|
|
<p class="text-gray-600 font-medium">{% trans "No new notifications" %}</p>
|
|
<p class="text-gray-400 text-sm mt-1">{% trans "You're all caught up!" %}</p>
|
|
</div>
|
|
`;
|
|
} else {
|
|
container.innerHTML = data.notifications.map(n => {
|
|
const icon = n.type === 'complaint_assigned' ? 'user-check' :
|
|
n.type === 'sla_reminder' ? 'clock' : 'bell';
|
|
return `
|
|
<a href="/notifications/inbox/" class="block p-4 hover:bg-gray-50 transition border-b border-gray-50">
|
|
<div class="flex items-start gap-3">
|
|
<div class="bg-blue-100 p-2 rounded-xl">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600">
|
|
${icon === 'user-check' ? '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><polyline points="16 11 18 13 22 9"></polyline>' :
|
|
icon === 'clock' ? '<circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline>' :
|
|
'<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"></path><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"></path>'}
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-sm text-gray-800 font-medium truncate">${n.title}</p>
|
|
<p class="text-xs text-gray-400 mt-1">${formatTime(n.created_at)}</p>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
`}).join('');
|
|
|
|
if (data.has_more) {
|
|
container.innerHTML += `
|
|
<a href="{% url 'notifications:inbox' %}" class="block p-3 text-center text-navy text-sm font-semibold hover:bg-gray-50 transition">
|
|
{% trans "View all notifications" %}
|
|
</a>
|
|
`;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading notifications:', error);
|
|
}
|
|
}
|
|
|
|
// Format timestamp to relative time
|
|
function formatTime(isoString) {
|
|
const date = new Date(isoString);
|
|
const now = new Date();
|
|
const diff = Math.floor((now - date) / 1000);
|
|
|
|
if (diff < 60) return '{% trans "Just now" %}';
|
|
if (diff < 3600) return Math.floor(diff / 60) + ' {% trans "min ago" %}';
|
|
if (diff < 86400) return Math.floor(diff / 3600) + ' {% trans "hours ago" %}';
|
|
return Math.floor(diff / 86400) + ' {% trans "days ago" %}';
|
|
}
|
|
|
|
// Poll for new notifications every 30 seconds
|
|
setInterval(async () => {
|
|
try {
|
|
const response = await fetch('/notifications/api/unread-count/');
|
|
const data = await response.json();
|
|
const badge = document.getElementById('notification-count-badge');
|
|
|
|
if (badge) {
|
|
if (data.count > 0) {
|
|
badge.textContent = data.count;
|
|
badge.classList.remove('hidden');
|
|
} else {
|
|
badge.classList.add('hidden');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error polling notifications:', error);
|
|
}
|
|
}, 30000);
|
|
</script>
|
|
|
|
{% block extra_js %}{% endblock %}
|
|
</body>
|
|
</html>
|