HH/templates/layouts/base.html
ismail 946c31cf34
Some checks failed
Build and Push Docker Image / build (push) Failing after 21s
Add CI pipeline for Docker image builds
2026-04-19 13:34:40 +03:00

280 lines
12 KiB
HTML

{% load i18n %}{% load static %}
<!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 -->
<link rel="stylesheet" href="{% static 'dist/css/tailwind.css' %}">
<!-- Lucide Icons -->
<script src="{% static 'vendor/lucide/lucide.min.js' %}"></script>
<!-- Google Fonts - Inter -->
<link rel="stylesheet" href="{% static 'vendor/fonts/inter/latin.css' %}">
<!-- Bootstrap to Tailwind Migration Helper (Temporary) -->
<!-- TODO: Remove this after all templates are migrated -->
<script src="{% static 'js/bootstrap-to-tailwind.js' %}"></script>
<!-- ApexCharts -->
<script src="{% static 'vendor/apexcharts/apexcharts.min.js' %}"></script>
<!-- HTMX for dynamic updates -->
<script src="{% static 'vendor/htmx/htmx.min.js' %}"></script>
<!-- Layout-specific styles -->
<style>
/* 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;
}
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body class="bg-slate-50 text-gray-800 min-h-screen">
<div class="flex h-screen">
<!-- Sidebar -->
{% block sidebar %}
{% include 'layouts/partials/sidebar.html' %}
{% endblock %}
<!-- Main Content Area -->
<div class="flex-1 flex flex-col main-content overflow-hidden">
<!-- Topbar -->
{% include 'layouts/partials/topbar.html' %}
<!-- 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>
<!-- Footer -->
<footer class="border-t border-slate-200 bg-white px-8 py-3 text-center">
<p class="text-xs text-slate-400">
Powered by <a href="https://tenhal.sa" target="_blank" class="text-slate-500 hover:text-navy hover:underline font-medium">tenhal.sa</a>
</p>
</footer>
</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>
<!-- Global Form Loading State Handler -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle forms with data-loading attribute
document.querySelectorAll('form[data-loading]').forEach(form => {
form.addEventListener('submit', function(e) {
const btn = form.querySelector('button[type="submit"], input[type="submit"]');
if (btn && !btn.disabled) {
btn.disabled = true;
btn.dataset.originalText = btn.innerHTML;
const loadingText = btn.dataset.loadingText || '{% trans "Processing..." %}';
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 inline mr-2 animate-spin"></i> ' + loadingText;
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
});
});
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>