HH/templates/standards/standard_detail.html
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

891 lines
49 KiB
HTML

{% extends 'layouts/base.html' %}
{% load i18n static %}
{% load standards_filters %}
{% block title %}{{ standard.code }} - {% trans "Standard Details" %}{% 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;
}
.stat-card {
background: white;
border-radius: 1rem;
border: 2px solid #e2e8f0;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
border-color: #005696;
}
.btn-transition {
transition: all 0.2s ease-in-out;
}
</style>
{% endblock %}
{% block content %}
<div class="px-6 py-4">
<div class="page-header-gradient">
<div class="flex justify-between items-center">
<div>
{% if parent_standard %}
<div class="flex items-center gap-2 mb-2 text-blue-200 text-xs">
<a href="{% url 'standards:standard_detail' pk=parent_standard.pk %}" class="hover:text-white transition">
{{ parent_standard.code }}
</a>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
<span class="text-white font-medium">{{ standard.code }}</span>
</div>
{% endif %}
<h1 class="text-2xl font-bold mb-1">{{ standard.code }}</h1>
<p class="text-blue-100 text-sm">{{ standard.title }}</p>
{% if parent_standard %}
<p class="text-blue-200 text-xs mt-1">
{% trans "Sub-standard of" %}: <a href="{% url 'standards:standard_detail' pk=parent_standard.pk %}" class="text-white hover:underline">{{ parent_standard.code }} - {{ parent_standard.title }}</a>
</p>
{% endif %}
</div>
<div class="flex gap-2">
{% if standard.is_assessable and not standard.is_heading %}
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-bold bg-green-400/20 text-white border border-white/30">
<i data-lucide="check-circle" class="w-3 h-3 me-1"></i>{% trans "Assessable" %}
</span>
{% else %}
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-bold bg-white/10 text-white/70 border border-white/20">
<i data-lucide="info" class="w-3 h-3 me-1"></i>{% trans "Informational" %}
</span>
{% endif %}
{% if is_px_admin %}
<a href="{% url 'standards:standard_update' pk=standard.pk %}" class="btn-transition inline-flex items-center px-4 py-2.5 bg-green-500 text-white font-semibold rounded-xl hover:bg-green-600 transition">
<i data-lucide="edit-2" class="w-4 h-4 me-2"></i>{% trans "Edit" %}
</a>
<a href="{% url 'standards:standard_delete' pk=standard.pk %}" class="btn-transition inline-flex items-center px-4 py-2.5 bg-red-500 text-white font-semibold rounded-xl hover:bg-red-600 transition">
<i data-lucide="trash-2" class="w-4 h-4 me-2"></i>{% trans "Delete" %}
</a>
{% endif %}
<a href="{% url 'standards:search' %}" class="btn-transition inline-flex items-center px-4 py-2.5 bg-white/10 border-2 border-white/30 text-white font-semibold 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 Standards" %}
</a>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<div class="lg:col-span-2">
<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-navy">{% trans "Standard Information" %}</h5>
</div>
<div class="p-6">
<dl class="divide-y divide-slate-100">
<div class="py-3 flex justify-between">
<dt class="text-sm font-semibold text-slate">{% trans "Source" %}</dt>
<dd class="text-sm font-semibold text-navy">{{ standard.source.name }}</dd>
</div>
<div class="py-3 flex justify-between">
<dt class="text-sm font-semibold text-slate">{% trans "Category" %}</dt>
<dd class="text-sm font-semibold text-navy">{{ standard.category.name }}</dd>
</div>
<div class="py-3 flex justify-between">
<dt class="text-sm font-semibold text-slate">{% trans "Department" %}</dt>
<dd class="text-sm font-semibold text-navy">
{% if standard.departments.all %}
{{ standard.departments.all|join:", " }}
{% else %}
<span class="text-slate-400 italic">{% trans "All Departments" %}</span>
{% endif %}
</dd>
</div>
<div class="py-3 flex justify-between">
<dt class="text-sm font-semibold text-slate">{% trans "Assessment Method" %}</dt>
<dd class="text-sm font-semibold text-navy">
{% if standard.assessment_method %}
{{ standard.get_assessment_method_display }}
{% else %}
<span class="text-slate-400 italic">{% trans "Not specified" %}</span>
{% endif %}
</dd>
</div>
<div class="py-3 flex justify-between">
<dt class="text-sm font-semibold text-slate">{% trans "Effective Date" %}</dt>
<dd class="text-sm font-semibold text-navy">
{% if standard.effective_date %}
{{ standard.effective_date|date:"Y-m-d" }}
{% else %}
<span class="text-slate-400 italic">{% trans "Not specified" %}</span>
{% endif %}
</dd>
</div>
<div class="py-3 flex justify-between">
<dt class="text-sm font-semibold text-slate">{% trans "Review Date" %}</dt>
<dd class="text-sm font-semibold text-navy">
{% if standard.review_date %}
{{ standard.review_date|date:"Y-m-d" }}
{% else %}
<span class="text-slate-400 italic">{% trans "Not specified" %}</span>
{% endif %}
</dd>
</div>
{% if parent_standard %}
<div class="py-3 flex justify-between">
<dt class="text-sm font-semibold text-slate">{% trans "Parent Standard" %}</dt>
<dd class="text-sm font-semibold text-navy">
<a href="{% url 'standards:standard_detail' pk=parent_standard.pk %}" class="text-navy hover:underline">
{{ parent_standard.code }} - {{ parent_standard.title }}
</a>
</dd>
</div>
{% endif %}
{% if sub_standards.exists %}
<div class="py-3 flex justify-between">
<dt class="text-sm font-semibold text-slate">{% trans "Sub-Standards" %}</dt>
<dd class="text-sm font-semibold text-navy">
{{ sub_standards.count }}
</dd>
</div>
{% endif %}
</dl>
</div>
</div>
</div>
<div class="lg:col-span-1">
<div class="stat-card">
<div class="text-center mb-4">
<div class="text-4xl font-bold text-navy mb-1">{{ compliance_records.count }}</div>
<div class="text-sm text-slate">{% trans "Assessments" %}</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between p-2 bg-green-50 rounded-lg">
<span class="text-sm text-slate-700">{% trans "Met" %}</span>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800">
{{ compliance_records|count_by:"status:met" }}
</span>
</div>
<div class="flex items-center justify-between p-2 bg-yellow-50 rounded-lg">
<span class="text-sm text-slate-700">{% trans "Partially Met" %}</span>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800">
{{ compliance_records|count_by:"status:partially_met" }}
</span>
</div>
<div class="flex items-center justify-between p-2 bg-red-50 rounded-lg">
<span class="text-sm text-slate-700">{% trans "Not Met" %}</span>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold bg-red-100 text-red-800">
{{ compliance_records|count_by:"status:not_met" }}
</span>
</div>
<div class="flex items-center justify-between p-2 bg-slate-50 rounded-lg">
<span class="text-sm text-slate-700">{% trans "Not Assessed" %}</span>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold bg-slate-100 text-navy">
{{ compliance_records|count_by:"status:not_assessed" }}
</span>
</div>
</div>
{% if standard.is_assessable and not standard.is_heading %}
<div class="mt-4 pt-4 border-t border-slate-200">
<button onclick="showNewAssessmentSection()" class="w-full px-4 py-2.5 bg-navy text-white font-semibold rounded-xl hover:bg-blue transition flex items-center justify-center gap-2">
<i data-lucide="plus-circle" class="w-4 h-4"></i>{% trans "New Assessment" %}
</button>
</div>
{% endif %}
</div>
</div>
</div>
<div class="section-card mb-6">
<div class="section-header">
<div class="section-icon bg-blue/10">
<i data-lucide="file-text" class="w-5 h-5 text-blue"></i>
</div>
<h5 class="text-lg font-semibold text-navy">{% trans "Description" %}</h5>
</div>
<div class="p-6">
<div class="text-sm text-slate-700 leading-relaxed">
{{ standard.description|linebreaks }}
</div>
</div>
</div>
{% if sub_standards.exists %}
<div class="section-card mb-6">
<div class="section-header">
<div class="section-icon bg-indigo/10">
<i data-lucide="git-branch" class="w-5 h-5 text-indigo-600"></i>
</div>
<h5 class="text-lg font-semibold text-navy">{% trans "Sub-Standards" %} ({{ sub_standards.count }})</h5>
</div>
<div class="p-0">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50 border-b">
<tr>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Code" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Title" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Assessable" %}</th>
<th class="px-4 py-3 text-right text-[10px] font-bold text-slate uppercase">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-slate-100">
{% for sub in sub_standards %}
<tr class="hover:bg-slate-50 transition">
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-lg text-[11px] font-semibold bg-indigo-50 text-indigo-700">{{ sub.code }}</span>
</td>
<td class="px-6 py-4">
<a href="{% url 'standards:standard_detail' pk=sub.id %}" class="text-sm font-medium text-navy hover:text-navy transition">{{ sub.title }}</a>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if sub.is_assessable and not sub.is_heading %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold uppercase bg-green-100 text-green-700">{% trans "Yes" %}</span>
{% else %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold uppercase bg-slate-100 text-slate">{% trans "No" %}</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<a href="{% url 'standards:standard_detail' pk=sub.id %}" class="btn-transition inline-flex items-center px-3 py-1.5 bg-navy text-white text-xs font-semibold rounded-lg hover:bg-blue transition">
<i data-lucide="eye" class="w-3 h-3 me-1"></i>{% trans "View" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if not standard.is_assessable or standard.is_heading %}
<div class="section-card p-8 text-center">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-blue-50 flex items-center justify-center">
<i data-lucide="info" class="w-8 h-8 text-blue"></i>
</div>
<h3 class="text-lg font-bold text-navy mb-2">{% trans "Informational Standard" %}</h3>
<p class="text-slate text-sm">{% trans "This standard is informational only and does not require assessment or evidence upload." %}</p>
</div>
{% else %}
<!-- New Assessment Section (hidden by default) -->
<div id="newAssessmentSection" class="section-card mb-6 hidden" style="border-color: #10b981;">
<div class="section-header" style="background: linear-gradient(to right, #ecfdf5, #d1fae5); border-color: #a7f3d0;">
<div class="section-icon bg-green-100">
<i data-lucide="plus-circle" class="w-5 h-5 text-green-600"></i>
</div>
<h5 class="text-lg font-semibold text-green-800">{% trans "New Assessment" %}</h5>
<button type="button" onclick="hideNewAssessmentSection()" class="ml-auto text-green-600 hover:text-green-800 transition">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Department" %} <span class="text-red-500">*</span></label>
<select id="newDeptSelect" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none">
<option value="">{% trans "Select Department..." %}</option>
{% for dept in departments %}
<option value="{{ dept.id }}">{{ dept.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Compliance Status" %} <span class="text-red-500">*</span></label>
<select id="newStatusSelect" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none">
<option value="">{% trans "Select Status..." %}</option>
<option value="met">{% trans "Met" %}</option>
<option value="partially_met">{% trans "Partially Met" %}</option>
<option value="not_met">{% trans "Not Met" %}</option>
<option value="not_applicable">{% trans "Not Applicable" %}</option>
</select>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Notes" %}</label>
<textarea id="newNotes" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none resize-none" placeholder="{% trans 'Assessment notes...' %}"></textarea>
</div>
<div class="mb-4">
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Evidence Summary" %}</label>
<textarea id="newEvidenceSummary" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-navy/20 focus:border-navy outline-none resize-none" placeholder="{% trans 'Summarize the evidence...' %}"></textarea>
</div>
<div class="flex gap-3">
<button type="button" onclick="submitNewAssessment()" class="px-6 py-2.5 bg-green-500 text-white font-semibold rounded-xl hover:bg-green-600 transition flex items-center gap-2">
<i data-lucide="save" class="w-4 h-4"></i>{% trans "Save Assessment" %}
</button>
<button type="button" onclick="hideNewAssessmentSection()" class="px-6 py-2.5 border-2 border-slate-200 text-slate font-semibold rounded-xl hover:bg-slate-50 transition">{% trans "Cancel" %}</button>
</div>
</div>
</div>
<!-- Compliance by Department -->
<div class="section-card">
<div class="section-header">
<div class="section-icon bg-navy/10">
<i data-lucide="building-2" class="w-5 h-5 text-navy"></i>
</div>
<h5 class="text-lg font-semibold text-navy">{% trans "Compliance by Department" %}</h5>
</div>
<div class="p-0">
{% if compliance_records %}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50 border-b">
<tr>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Department" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Status" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Last Assessed" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Assessor" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Evidence" %}</th>
<th class="px-4 py-3 text-right text-[10px] font-bold text-slate uppercase">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-slate-100">
{% for record in compliance_records %}
<tr class="hover:bg-slate-50 transition" data-compliance-id="{{ record.id }}">
<td class="px-6 py-4 whitespace-nowrap">
<a href="{% url 'standards:department_standards' pk=record.department.id %}" class="text-sm font-medium text-navy hover:text-navy transition">
{{ record.department.name }}
</a>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if record.status == 'met' %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-[10px] font-bold uppercase bg-green-100 text-green-800">
<i data-lucide="check-circle" class="w-3 h-3 me-1"></i>{{ record.get_status_display }}
</span>
{% elif record.status == 'partially_met' %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-[10px] font-bold uppercase bg-yellow-100 text-yellow-800">
<i data-lucide="alert-circle" class="w-3 h-3 me-1"></i>{{ record.get_status_display }}
</span>
{% elif record.status == 'not_met' %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-[10px] font-bold uppercase bg-red-100 text-red-800">
<i data-lucide="x-circle" class="w-3 h-3 me-1"></i>{{ record.get_status_display }}
</span>
{% else %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-[10px] font-bold uppercase bg-slate-100 text-navy">
<i data-lucide="help-circle" class="w-3 h-3 me-1"></i>{{ record.get_status_display }}
</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate">
{% if record.last_assessed_date %}
{{ record.last_assessed_date|date:"Y-m-d" }}
{% else %}
<span class="text-slate-400 italic">{% trans "Not assessed" %}</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate">
{% if record.assessor %}
{{ record.assessor.get_full_name }}
{% else %}
<span class="text-slate-400">-</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2 py-1 rounded-full text-[10px] font-bold uppercase bg-blue-100 text-blue-800">
<i data-lucide="file-text" class="w-3 h-3 me-1"></i>{{ record.attachment_count }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<div class="flex gap-2 justify-end">
<button onclick="openAssessModal('{{ record.id }}')" class="btn-transition inline-flex items-center px-3 py-1.5 bg-navy text-white text-xs font-semibold rounded-lg hover:bg-blue transition" title="{% trans 'Assess' %}">
<i data-lucide="clipboard-check" class="w-3 h-3"></i>
</button>
<button onclick="openEvidenceModal('{{ record.id }}', '{{ standard.code|escapejs }}', '{{ standard.title|escapejs }}')" class="btn-transition inline-flex items-center px-3 py-1.5 bg-white border border-slate-200 text-navy text-xs font-semibold rounded-lg hover:bg-navy hover:text-white transition" title="{% trans 'Evidence' %}">
<i data-lucide="upload-cloud" class="w-3 h-3"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-12 text-slate">
<i data-lucide="inbox" class="w-12 h-12 mx-auto mb-3 text-slate-300"></i>
<p class="mb-2">{% trans "No compliance assessments yet" %}</p>
<button onclick="showNewAssessmentSection()" class="inline-flex items-center px-4 py-2 bg-navy text-white font-semibold rounded-xl hover:bg-blue transition gap-2">
<i data-lucide="plus-circle" class="w-4 h-4"></i>{% trans "Create First Assessment" %}
</button>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Assessment Modal -->
<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">
<div class="flex justify-between items-center p-6 border-b border-slate-200">
<h5 class="text-xl font-bold text-navy">{% trans "Update Assessment" %}</h5>
<button type="button" class="text-slate-400 hover:text-slate transition" onclick="closeModal('assessmentModal')">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<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-semibold text-slate mb-2">{% trans "Compliance Status" %} <span class="text-red-500">*</span></label>
<select class="w-full px-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-navy/20 outline-none 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>
<option value="not_applicable">{% trans "Not Applicable" %}</option>
</select>
</div>
<div>
<label for="last_assessed_date" class="block text-sm font-semibold text-slate mb-2">{% trans "Assessment Date" %}</label>
<input type="date" class="w-full px-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-navy/20 outline-none transition" id="last_assessed_date" name="last_assessed_date">
<p class="mt-1 text-xs text-slate">{% trans "Defaults to today" %}</p>
</div>
<div>
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Assessor" %}</label>
<input type="text" class="w-full px-4 py-2.5 border border-slate-200 rounded-lg bg-slate-50 text-slate transition" id="assessor" name="assessor" readonly>
<input type="hidden" id="assessor_id" name="assessor_id" value="{{ request.user.id }}">
</div>
<div>
<label for="notes" class="block text-sm font-semibold text-slate mb-2">{% trans "Notes" %}</label>
<textarea class="w-full px-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-navy/20 outline-none transition" id="notes" name="notes" rows="3" placeholder="{% trans 'Assessment notes...' %}"></textarea>
</div>
<div>
<label for="evidence_summary" class="block text-sm font-semibold text-slate mb-2">{% trans "Evidence Summary" %}</label>
<textarea class="w-full px-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-navy/20 outline-none transition" id="evidence_summary" name="evidence_summary" rows="3" placeholder="{% trans 'Summarize the evidence...' %}"></textarea>
</div>
</div>
<div class="flex justify-end gap-3 p-6 border-t border-slate-200 bg-slate-50 rounded-b-2xl">
<button type="button" class="px-6 py-2.5 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition" onclick="closeModal('assessmentModal')">{% trans "Cancel" %}</button>
<button type="submit" class="px-6 py-2.5 bg-green-500 text-white font-semibold 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>
<!-- Evidence Modal -->
<div class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" id="evidenceModal">
<div class="bg-white rounded-2xl shadow-2xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto border border-slate-200">
<div class="flex justify-between items-center p-6 border-b border-slate-200 bg-gradient-to-r from-navy to-blue">
<div>
<h5 class="text-xl font-bold text-white">{% trans "Evidence Management" %}</h5>
<p class="text-sm text-blue-100 mt-1" id="evidenceModalTitle"></p>
</div>
<button type="button" class="text-white/70 hover:text-white transition" onclick="closeModal('evidenceModal')">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<div class="p-6 space-y-6 bg-gradient-to-b from-slate-50 to-white">
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<h6 class="text-sm font-semibold text-navy mb-4 flex items-center">
<span class="w-8 h-8 rounded-lg bg-navy/10 flex items-center justify-center mr-3"><i data-lucide="upload-cloud" class="w-4 h-4 text-navy"></i></span>
{% trans "Upload New Attachment" %}
</h6>
<form id="evidenceUploadForm" class="space-y-4">
<div>
<label class="block text-sm font-semibold text-slate mb-2">{% trans "File" %}</label>
<input type="file" id="evidenceFile" name="file" accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.zip" class="block w-full text-sm text-navy border border-slate-200 rounded-lg cursor-pointer bg-white focus:outline-none focus:ring-2 focus:ring-navy/20 outline-none">
<p class="mt-2 text-xs text-slate"><i data-lucide="info" class="w-3 h-3 inline mr-1"></i>{% trans "Accepted: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG, ZIP (max 50MB)" %}</p>
</div>
<div>
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Description" %}</label>
<textarea id="evidenceDescription" name="description" rows="2" class="w-full px-4 py-3 border border-slate-200 rounded-lg focus:ring-2 focus:ring-navy/20 outline-none transition" placeholder="{% trans 'Add a description...' %}"></textarea>
</div>
<div id="uploadProgress" class="hidden">
<div class="w-full bg-slate-200 rounded-full h-2.5">
<div id="uploadProgressBar" class="bg-navy h-2.5 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<p class="text-xs text-slate mt-2 flex items-center"><i data-lucide="loader-2" class="w-3 h-3 inline mr-1 animate-spin"></i>{% trans "Uploading..." %}</p>
</div>
<div id="uploadError" class="hidden text-sm text-red-600 bg-red-50 border border-red-200 p-3 rounded-lg"></div>
<button type="button" onclick="uploadEvidence()" class="inline-flex items-center px-5 py-2.5 bg-navy text-white text-sm font-semibold rounded-xl hover:bg-blue transition shadow-md hover:shadow-lg">
<i data-lucide="upload" class="w-4 h-4 mr-2"></i>{% trans "Upload" %}
</button>
</form>
</div>
<div>
<h6 class="text-sm font-semibold text-navy mb-4 flex items-center">
<span class="w-8 h-8 rounded-lg bg-navy/10 flex items-center justify-center mr-3"><i data-lucide="paperclip" class="w-4 h-4 text-navy"></i></span>
{% trans "Existing Attachments" %}
</h6>
<div class="overflow-x-auto border border-slate-200 rounded-xl bg-white shadow-sm">
<table class="min-w-full divide-y divide-slate-100">
<thead class="bg-slate-50 border-b">
<tr>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "File" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Description" %}</th>
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Uploaded" %}</th>
<th class="px-4 py-3 text-right text-[10px] font-bold text-slate uppercase">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody id="attachmentsList" class="bg-white divide-y divide-slate-100">
<tr><td colspan="4" class="px-6 py-8 text-center text-slate"><p class="text-sm">{% trans "Loading..." %}</p></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="flex justify-end gap-3 p-6 border-t border-slate-200 bg-slate-50 rounded-b-2xl">
<button type="button" class="px-6 py-2.5 bg-white border border-slate-200 text-slate-700 font-semibold rounded-xl hover:bg-slate-50 hover:border-slate-300 transition shadow-sm" onclick="closeModal('evidenceModal')">
<i data-lucide="x" class="w-4 h-4 mr-2 inline"></i>{% trans "Close" %}
</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const standardId = '{{ standard.id }}';
const userData = {
id: '{{ request.user.id }}',
fullName: "{% if request.user.get_full_name %}{{ request.user.get_full_name|escapejs }}{% else %}{{ request.user.username|escapejs }}{% endif %}"
};
document.addEventListener('DOMContentLoaded', function() {
const dateInput = document.getElementById('last_assessed_date');
if (dateInput && !dateInput.value) {
dateInput.value = new Date().toISOString().split('T')[0];
}
const assessorInput = document.getElementById('assessor');
if (assessorInput) assessorInput.value = userData.fullName;
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');
setTimeout(() => { if (typeof lucide !== 'undefined') lucide.createIcons(); }, 100);
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.classList.add('hidden');
}
function showNewAssessmentSection() {
document.getElementById('newAssessmentSection').classList.remove('hidden');
document.getElementById('newAssessmentSection').scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => { if (typeof lucide !== 'undefined') lucide.createIcons(); }, 100);
}
function hideNewAssessmentSection() {
document.getElementById('newAssessmentSection').classList.add('hidden');
}
function submitNewAssessment() {
const departmentId = document.getElementById('newDeptSelect').value;
const status = document.getElementById('newStatusSelect').value;
const notes = document.getElementById('newNotes').value;
const evidenceSummary = document.getElementById('newEvidenceSummary').value;
if (!departmentId || !status) {
alert('{% trans "Please select a department and status." %}');
return;
}
fetch('{% url "standards:compliance_update_ajax" %}', {
method: 'POST',
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' },
body: JSON.stringify({
department_id: departmentId,
standard_id: standardId,
status: status,
notes: notes,
evidence_summary: evidenceSummary,
last_assessed_date: new Date().toISOString().split('T')[0],
assessor_id: userData.id
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('{% trans "Error:" %} ' + (data.error || '{% trans "Unknown error" %}'));
}
})
.catch(err => alert('{% trans "Error:" %} ' + err.message));
}
function openAssessModal(complianceId) {
document.getElementById('complianceId').value = complianceId;
document.getElementById('status').value = 'met';
document.getElementById('notes').value = '';
document.getElementById('evidence_summary').value = '';
document.getElementById('last_assessed_date').value = new Date().toISOString().split('T')[0];
document.getElementById('assessor').value = userData.fullName;
showModal('assessmentModal');
}
function submitAssessment() {
const complianceId = document.getElementById('complianceId').value;
const status = document.getElementById('status').value;
const notes = document.getElementById('notes').value;
const evidenceSummary = document.getElementById('evidence_summary').value;
const lastAssessedDate = document.getElementById('last_assessed_date').value;
const assessorId = document.getElementById('assessor_id').value;
if (!complianceId || !status) {
alert('{% trans "Missing required fields" %}');
return;
}
fetch('{% url "standards:compliance_update_ajax" %}', {
method: 'POST',
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' },
body: JSON.stringify({
compliance_id: complianceId,
status: status,
notes: notes,
evidence_summary: evidenceSummary,
last_assessed_date: lastAssessedDate,
assessor_id: assessorId
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
closeModal('assessmentModal');
location.reload();
} else {
alert('{% trans "Error:" %} ' + (data.error || '{% trans "Unknown error" %}'));
}
})
.catch(err => alert('{% trans "Error:" %} ' + err.message));
}
let currentEvidenceComplianceId = null;
function openEvidenceModal(complianceId, standardCode, standardTitle) {
currentEvidenceComplianceId = complianceId;
document.getElementById('evidenceModalTitle').textContent = standardCode + ' - ' + standardTitle;
document.getElementById('evidenceUploadForm').reset();
document.getElementById('uploadProgress').classList.add('hidden');
document.getElementById('uploadError').classList.add('hidden');
loadAttachments(complianceId);
showModal('evidenceModal');
}
function loadAttachments(complianceId) {
const attachmentsList = document.getElementById('attachmentsList');
attachmentsList.innerHTML = '<tr><td colspan="4" class="px-6 py-4 text-center text-slate">{% trans "Loading..." %}</td></tr>';
fetch('{% url "standards:attachments_list_ajax" "00000000-0000-0000-0000-000000000000" %}'.replace('00000000-0000-0000-0000-000000000000', complianceId), {
method: 'GET',
headers: { 'X-CSRFToken': getCookie('csrftoken') }
})
.then(r => r.json())
.then(data => {
if (data.success) renderAttachments(data.attachments);
else attachmentsList.innerHTML = '<tr><td colspan="4" class="px-6 py-4 text-center text-red-500">{% trans "Error loading attachments" %}</td></tr>';
})
.catch(err => {
attachmentsList.innerHTML = '<tr><td colspan="4" class="px-6 py-4 text-center text-red-500">{% trans "Error loading attachments" %}</td></tr>';
});
}
function renderAttachments(attachments) {
const attachmentsList = document.getElementById('attachmentsList');
if (attachments.length === 0) {
attachmentsList.innerHTML = '<tr><td colspan="4" class="px-6 py-10 text-center text-slate"><div class="w-16 h-16 mx-auto mb-3 rounded-full bg-navy/10 flex items-center justify-center"><i data-lucide="inbox" class="w-8 h-8 text-navy/50"></i></div><p class="text-sm">{% trans "No attachments yet" %}</p><p class="text-xs text-slate-400 mt-1">{% trans "Upload files above" %}</p></td></tr>';
if (typeof lucide !== 'undefined') lucide.createIcons();
return;
}
attachmentsList.innerHTML = attachments.map(att => `
<tr class="hover:bg-navy/5 transition border-b border-slate-100 last:border-b-0">
<td class="px-4 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="w-8 h-8 rounded-lg bg-navy/10 flex items-center justify-center mr-3"><i data-lucide="file-text" class="w-4 h-4 text-navy"></i></span>
<span class="text-sm font-medium text-navy">${escapeHtml(att.filename)}</span>
</div>
</td>
<td class="px-4 py-4"><span class="text-sm text-slate">${escapeHtml(att.description) || '<span class="text-slate-400 italic">{% trans "No description" %}</span>'}</span></td>
<td class="px-4 py-4 whitespace-nowrap text-sm"><span class="text-navy font-medium">${escapeHtml(att.uploaded_by)}</span><div class="text-xs text-slate-400">${att.uploaded_at}</div></td>
<td class="px-4 py-4 whitespace-nowrap text-right">
<div class="flex gap-2 justify-end">
<a href="${att.file_url}" target="_blank" class="btn-transition inline-flex items-center px-3 py-1.5 bg-navy text-white text-xs font-semibold rounded-lg hover:bg-blue transition shadow-sm" title="{% trans 'Download' %}"><i data-lucide="download" class="w-3 h-3"></i></a>
<button type="button" onclick="deleteAttachment('${att.id}')" class="btn-transition inline-flex items-center px-3 py-1.5 bg-white border border-red-200 text-red-600 text-xs font-semibold rounded-lg hover:bg-red-50 hover:border-red-300 transition" title="{% trans 'Delete' %}"><i data-lucide="trash-2" class="w-3 h-3"></i></button>
</div>
</td>
</tr>
`).join('');
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function uploadEvidence() {
const fileInput = document.getElementById('evidenceFile');
const descriptionInput = document.getElementById('evidenceDescription');
const progressBar = document.getElementById('uploadProgressBar');
const progressContainer = document.getElementById('uploadProgress');
const errorDiv = document.getElementById('uploadError');
if (!fileInput.files || fileInput.files.length === 0) {
errorDiv.textContent = '{% trans "Please select a file" %}';
errorDiv.classList.remove('hidden');
return;
}
const file = fileInput.files[0];
if (file.size > 50 * 1024 * 1024) {
errorDiv.textContent = '{% trans "File size must be less than 50MB" %}';
errorDiv.classList.remove('hidden');
return;
}
errorDiv.classList.add('hidden');
progressContainer.classList.remove('hidden');
progressBar.style.width = '0%';
const formData = new FormData();
formData.append('compliance_id', currentEvidenceComplianceId);
formData.append('file', file);
formData.append('description', descriptionInput.value);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) progressBar.style.width = (e.loaded / e.total * 100) + '%';
});
xhr.addEventListener('load', function() {
progressContainer.classList.add('hidden');
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
document.getElementById('evidenceUploadForm').reset();
loadAttachments(currentEvidenceComplianceId);
} else {
errorDiv.textContent = response.error || '{% trans "Upload failed" %}';
errorDiv.classList.remove('hidden');
}
} else {
errorDiv.textContent = '{% trans "Upload failed" %}';
errorDiv.classList.remove('hidden');
}
});
xhr.addEventListener('error', function() {
progressContainer.classList.add('hidden');
errorDiv.textContent = '{% trans "Upload failed" %}';
errorDiv.classList.remove('hidden');
});
xhr.open('POST', '{% url "standards:attachment_upload_ajax" %}', true);
xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
xhr.send(formData);
}
function deleteAttachment(attachmentId) {
if (!confirm('{% trans "Are you sure you want to delete this attachment?" %}')) return;
const url = '{% url "standards:attachment_delete_ajax" "00000000-0000-0000-0000-000000000000" %}'.replace('00000000-0000-0000-0000-000000000000', attachmentId);
fetch(url, {
method: 'POST',
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' }
})
.then(r => r.json())
.then(data => {
if (data.success) loadAttachments(currentEvidenceComplianceId);
else alert('{% trans "Error:" %} ' + (data.error || '{% trans "Unknown error" %}'));
})
.catch(err => alert('{% trans "Error deleting attachment" %}'));
}
document.addEventListener('click', function(e) {
if (e.target && e.target.id === 'assessmentModal') closeModal('assessmentModal');
if (e.target && e.target.id === 'evidenceModal') closeModal('evidenceModal');
});
</script>
{% endblock %}