470 lines
21 KiB
HTML
470 lines
21 KiB
HTML
{% extends 'layouts/base.html' %}
|
|
{% load i18n static %}
|
|
{% load standards_filters %}
|
|
|
|
{% block title %}{% trans "Department Standards" %} - {{ department.name }}{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
:root {
|
|
--hh-navy: #005696;
|
|
--hh-blue: #007bbd;
|
|
--hh-light: #eef6fb;
|
|
--hh-slate: #64748b;
|
|
}
|
|
|
|
.page-header-gradient {
|
|
background: linear-gradient(135deg, var(--hh-navy) 0%, #0069a8 50%, var(--hh-blue) 100%);
|
|
color: white;
|
|
padding: 1.5rem 2rem;
|
|
border-radius: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
box-shadow: 0 10px 15px -3px rgba(0, 86, 150, 0.2);
|
|
}
|
|
|
|
.section-card {
|
|
background: white;
|
|
border-radius: 1rem;
|
|
border: 2px solid #e2e8f0;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
.section-card:hover {
|
|
border-color: #005696;
|
|
box-shadow: 0 10px 25px -5px rgba(0, 86, 150, 0.15);
|
|
}
|
|
|
|
.section-header {
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 2px solid #e2e8f0;
|
|
background: linear-gradient(to right, #f8fafc, #f1f5f9);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.section-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 0.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-transition {
|
|
transition: all 0.2s ease-in-out;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- CSRF token for AJAX requests -->
|
|
<meta name="csrf-token" content="{{ csrf_token }}">
|
|
<script>
|
|
window.CSRF_TOKEN = '{{ csrf_token }}';
|
|
</script>
|
|
|
|
<div class="px-6 py-4">
|
|
<div class="page-header-gradient">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h1 class="text-2xl font-bold mb-1">{{ department.name }}</h1>
|
|
<p class="text-blue-100 text-sm">{% trans "Standards Compliance" %}</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
{% if is_px_admin %}
|
|
<a href="{% url 'standards:standard_create' department_id=department.id %}" class="btn-transition inline-flex items-center px-4 py-2.5 bg-green-500 text-white font-medium rounded-xl hover:bg-green-600 transition">
|
|
<i data-lucide="plus" class="w-4 h-4 me-2"></i>{% trans "Add Standard" %}
|
|
</a>
|
|
{% endif %}
|
|
<a href="{% url 'standards:dashboard' %}" class="btn-transition inline-flex items-center px-4 py-2.5 bg-white/10 border-2 border-white/30 text-white font-medium rounded-xl hover:bg-white hover:text-navy transition">
|
|
<i data-lucide="arrow-left" class="w-4 h-4 me-2"></i>{% trans "Back to Dashboard" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-card mb-6">
|
|
<div class="p-6">
|
|
<div class="relative">
|
|
<i data-lucide="search" class="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400"></i>
|
|
<input type="text"
|
|
class="w-full pl-12 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-navy focus:border-transparent transition"
|
|
id="searchInput"
|
|
placeholder="{% trans 'Search standards...' %}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<div class="section-icon bg-navy/10">
|
|
<i data-lucide="clipboard-list" class="w-5 h-5 text-navy"></i>
|
|
</div>
|
|
<h5 class="text-lg font-semibold text-gray-800">{% trans "Standards List" %}</h5>
|
|
</div>
|
|
<div class="p-0">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200" id="standardsTable">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{% trans "Code" %}
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{% trans "Title" %}
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{% trans "Status" %}
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{% trans "Evidence" %}
|
|
</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{% trans "Actions" %}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
{% for item in standards_data %}
|
|
<tr class="hover:bg-gray-50 transition"
|
|
data-standard-id="{{ item.standard.id }}"
|
|
data-compliance-id="{% if item.compliance %}{{ item.compliance.id }}{% endif %}">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-semibold bg-navy/10 text-navy">
|
|
{{ item.standard.code }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<a href="{% url 'standards:standard_detail' pk=item.standard.id %}" class="text-sm font-medium text-gray-900 hover:text-navy transition">
|
|
{{ item.standard.title }}
|
|
</a>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
{% if item.compliance %}
|
|
{% if item.compliance.status == 'met' %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
<i data-lucide="check-circle" class="w-3 h-3 me-1"></i>{{ item.compliance.get_status_display }}
|
|
</span>
|
|
{% elif item.compliance.status == 'partially_met' %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
<i data-lucide="alert-circle" class="w-3 h-3 me-1"></i>{{ item.compliance.get_status_display }}
|
|
</span>
|
|
{% elif item.compliance.status == 'not_met' %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
<i data-lucide="x-circle" class="w-3 h-3 me-1"></i>{{ item.compliance.get_status_display }}
|
|
</span>
|
|
{% else %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
<i data-lucide="help-circle" class="w-3 h-3 me-1"></i>{{ item.compliance.get_status_display }}
|
|
</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
<i data-lucide="help-circle" class="w-3 h-3 me-1"></i>{% trans "Not Assessed" %}
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
<i data-lucide="file-text" class="w-3 h-3 me-1"></i>{{ item.attachment_count }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
{% if item.compliance %}
|
|
<button class="btn-transition inline-flex items-center px-3 py-1.5 bg-navy text-white text-xs font-medium rounded-lg hover:bg-blue transition"
|
|
onclick="openAssessModal('{{ item.compliance.id }}')">
|
|
<i data-lucide="edit-2" class="w-3 h-3 me-1"></i>{% trans "Assess" %}
|
|
</button>
|
|
{% else %}
|
|
<button class="btn-transition inline-flex items-center px-3 py-1.5 bg-green-500 text-white text-xs font-medium rounded-lg hover:bg-green-600 transition"
|
|
onclick="createAndAssess('{{ item.standard.id }}')">
|
|
<i data-lucide="plus" class="w-3 h-3 me-1"></i>{% trans "Assess" %}
|
|
</button>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="5" class="px-6 py-12 text-center text-gray-500">
|
|
<i data-lucide="inbox" class="w-12 h-12 mx-auto mb-3 text-gray-300"></i>
|
|
<p>{% trans "No standards found" %}</p>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Compliance Assessment Modal (Tailwind) -->
|
|
<div class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" id="assessmentModal">
|
|
<div class="bg-white rounded-2xl shadow-2xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
<!-- Modal Header -->
|
|
<div class="flex justify-between items-center p-6 border-b border-gray-200">
|
|
<h5 class="text-xl font-bold text-gray-900">{% trans "Compliance Assessment" %}</h5>
|
|
<button type="button" class="text-gray-400 hover:text-gray-600 transition" onclick="closeModal('assessmentModal')">
|
|
<i data-lucide="x" class="w-6 h-6"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Modal Body -->
|
|
<form id="assessmentForm">
|
|
<div class="p-6 space-y-4">
|
|
<input type="hidden" id="complianceId" name="compliance_id">
|
|
|
|
<div>
|
|
<label for="status" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{% trans "Compliance Status" %} <span class="text-red-500">*</span>
|
|
</label>
|
|
<select class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-navy focus:border-transparent transition" id="status" name="status" required>
|
|
<option value="not_assessed">{% trans "Not Assessed" %}</option>
|
|
<option value="met">{% trans "Met" %}</option>
|
|
<option value="partially_met">{% trans "Partially Met" %}</option>
|
|
<option value="not_met">{% trans "Not Met" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="last_assessed_date" class="block text-sm font-medium text-gray-700 mb-2">
|
|
{% trans "Assessment Date" %}
|
|
</label>
|
|
<input type="date" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-navy focus:border-transparent transition" id="last_assessed_date" name="last_assessed_date">
|
|
<p class="mt-1 text-xs text-gray-500">{% trans "Auto-filled with today's date" %}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="assessor" class="block text-sm font-medium text-gray-700 mb-2">{% trans "Assessor" %}</label>
|
|
<input type="text" class="w-full px-4 py-2.5 border border-gray-300 rounded-lg bg-gray-50 text-gray-600 transition" id="assessor" name="assessor" readonly>
|
|
<input type="hidden" id="assessor_id" name="assessor_id" value="{{ user.id }}">
|
|
<p class="mt-1 text-xs text-gray-500">{% trans "Current logged-in user" %}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="notes" class="block text-sm font-medium text-gray-700 mb-2">{% trans "Notes" %}</label>
|
|
<textarea class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-navy focus:border-transparent transition" id="notes" name="notes" rows="3"
|
|
placeholder="{% trans 'Add any notes about the assessment...' %}"></textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="evidence_summary" class="block text-sm font-medium text-gray-700 mb-2">{% trans "Evidence Summary" %}</label>
|
|
<textarea class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-navy focus:border-transparent transition" id="evidence_summary" name="evidence_summary" rows="3"
|
|
placeholder="{% trans 'Summarize the evidence supporting this assessment...' %}"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Footer -->
|
|
<div class="flex justify-end gap-3 p-6 border-t border-gray-200 bg-gray-50 rounded-b-2xl">
|
|
<button type="button" class="px-6 py-2.5 bg-gray-100 text-gray-700 font-medium rounded-xl hover:bg-gray-200 transition" onclick="closeModal('assessmentModal')">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
<button type="submit" class="px-6 py-2.5 bg-green-500 text-white font-medium rounded-xl hover:bg-green-600 transition inline-flex items-center">
|
|
<i data-lucide="save" class="w-4 h-4 me-2"></i>{% trans "Save Assessment" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const userData = {
|
|
id: '{{ user.id }}',
|
|
username: "{{ user.username|escapejs }}",
|
|
fullName: "{% if user.get_full_name %}{{ user.get_full_name|escapejs }}{% else %}{{ user.username|escapejs }}{% endif %}"
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const dateInput = document.getElementById('last_assessed_date');
|
|
if (dateInput) {
|
|
dateInput.value = today;
|
|
}
|
|
|
|
const assessorInput = document.getElementById('assessor');
|
|
if (assessorInput) {
|
|
assessorInput.value = userData.fullName;
|
|
}
|
|
|
|
const searchInput = document.getElementById('searchInput');
|
|
const table = document.getElementById('standardsTable');
|
|
|
|
function filterTable() {
|
|
const searchText = searchInput.value.toLowerCase();
|
|
const rows = table.querySelectorAll('tbody tr');
|
|
|
|
rows.forEach(row => {
|
|
const text = row.textContent.toLowerCase();
|
|
const matchesSearch = text.includes(searchText);
|
|
|
|
if (matchesSearch) {
|
|
row.style.display = '';
|
|
} else {
|
|
row.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
searchInput.addEventListener('input', filterTable);
|
|
|
|
const form = document.getElementById('assessmentForm');
|
|
if (form) {
|
|
form.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
submitAssessment();
|
|
});
|
|
}
|
|
});
|
|
|
|
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 showModal(modalId) {
|
|
const modal = document.getElementById(modalId);
|
|
if (modal) {
|
|
modal.classList.remove('hidden');
|
|
// Reinitialize Lucide icons
|
|
setTimeout(() => {
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
const modal = document.getElementById(modalId);
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function createAndAssess(standardId) {
|
|
const data = {
|
|
standard_id: standardId,
|
|
department_id: '{{ department.id }}'
|
|
};
|
|
|
|
console.log('Creating compliance:', data);
|
|
|
|
fetch('{% url "standards:compliance_create_ajax" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': window.CSRF_TOKEN || getCookie('csrftoken'),
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(data)
|
|
})
|
|
.then(response => {
|
|
console.log('Response status:', response.status);
|
|
if (!response.ok) {
|
|
return response.text().then(text => {
|
|
console.error('Error response:', text);
|
|
throw new Error('Server error: ' + response.status);
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Response data:', data);
|
|
if (data.success) {
|
|
openAssessModal(data.compliance_id);
|
|
} else {
|
|
alert('{% trans "Error creating compliance record:" %} ' + (data.error || '{% trans "Unknown error" %}'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('{% trans "Error creating compliance record:" %} ' + error.message);
|
|
});
|
|
}
|
|
|
|
function openAssessModal(complianceId) {
|
|
document.getElementById('complianceId').value = complianceId;
|
|
showModal('assessmentModal');
|
|
}
|
|
|
|
function submitAssessment() {
|
|
const form = document.getElementById('assessmentForm');
|
|
const formData = new FormData(form);
|
|
|
|
const compliance_id = document.getElementById('complianceId').value;
|
|
const status = document.getElementById('status').value;
|
|
const notes = document.getElementById('notes').value;
|
|
const evidence_summary = document.getElementById('evidence_summary').value;
|
|
const last_assessed_date = document.getElementById('last_assessed_date').value;
|
|
const assessor_id = document.getElementById('assessor_id').value;
|
|
|
|
if (!compliance_id || !status) {
|
|
alert('{% trans "Missing required fields" %}');
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
compliance_id: compliance_id,
|
|
status: status,
|
|
notes: notes || '',
|
|
evidence_summary: evidence_summary || '',
|
|
last_assessed_date: last_assessed_date || '',
|
|
assessor_id: assessor_id || ''
|
|
};
|
|
|
|
console.log('Submitting assessment:', data);
|
|
|
|
fetch('{% url "standards:compliance_update_ajax" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': window.CSRF_TOKEN || getCookie('csrftoken'),
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(data)
|
|
})
|
|
.then(response => {
|
|
console.log('Response status:', response.status);
|
|
if (!response.ok) {
|
|
return response.text().then(text => {
|
|
console.error('Error response:', text);
|
|
throw new Error('Server error: ' + response.status);
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Response data:', data);
|
|
if (data.success) {
|
|
closeModal('assessmentModal');
|
|
location.reload();
|
|
} else {
|
|
alert('{% trans "Error updating compliance:" %} ' + (data.error || '{% trans "Unknown error" %}'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('{% trans "Error updating compliance:" %} ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Close modal when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target && e.target.id === 'assessmentModal') {
|
|
closeModal('assessmentModal');
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|