HH/templates/layouts/base.html
2026-03-28 14:03:56 +03:00

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>