862 lines
52 KiB
HTML
862 lines
52 KiB
HTML
{% extends 'layouts/base.html' %}
|
|
{% load i18n static %}
|
|
|
|
{% block title %}{% trans "Standards List" %}{% 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 %}
|
|
<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">{% trans "Standards List" %}</h1>
|
|
<p class="text-blue-100 text-sm">{{ hospital.name }}</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
{% if is_px_admin %}
|
|
<a href="{% url 'standards:standard_create_global' %}" 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="plus" class="w-4 h-4 me-2"></i>{% trans "Create 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-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 Dashboard" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-card mb-6">
|
|
<div class="section-header">
|
|
<div class="section-icon bg-navy/10">
|
|
<i data-lucide="search" class="w-5 h-5 text-navy"></i>
|
|
</div>
|
|
<h5 class="text-lg font-semibold text-navy">{% trans "Search Filters" %}</h5>
|
|
</div>
|
|
<div class="p-6">
|
|
<form method="get" class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
|
<div class="md:col-span-2">
|
|
<label for="q" class="block text-sm font-semibold text-slate mb-2">{% trans "Search" %}</label>
|
|
<input type="text"
|
|
class="w-full px-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-navy/20 outline-none transition"
|
|
name="q"
|
|
id="q"
|
|
value="{{ query }}"
|
|
placeholder="{% trans 'Search by code, title, or description...' %}">
|
|
</div>
|
|
<div>
|
|
<label for="source" class="block text-sm font-semibold text-slate mb-2">{% trans "Source" %}</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"
|
|
name="source"
|
|
id="source">
|
|
<option value="">{% trans "All Sources" %}</option>
|
|
{% for source in sources %}
|
|
<option value="{{ source.id }}" {% if source_filter == source.id|stringformat:"s" %}selected{% endif %}>
|
|
{{ source.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="category" class="block text-sm font-semibold text-slate mb-2">{% trans "Category" %}</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"
|
|
name="category"
|
|
id="category">
|
|
<option value="">{% trans "All Categories" %}</option>
|
|
{% for category in categories %}
|
|
<option value="{{ category.id }}" {% if category_filter == category.id|stringformat:"s" %}selected{% endif %}>
|
|
{{ category.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="department" class="block text-sm font-semibold text-slate mb-2">{% trans "Department" %}</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"
|
|
name="department"
|
|
id="department">
|
|
<option value="">{% trans "All Departments" %}</option>
|
|
{% for dept in departments %}
|
|
<option value="{{ dept.id }}" {% if department_filter == dept.id|stringformat:"s" %}selected{% endif %}>
|
|
{{ dept.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="md:col-span-5 flex gap-3">
|
|
<button type="submit" class="btn-transition inline-flex items-center px-6 py-2.5 bg-navy text-white font-semibold rounded-xl hover:bg-blue transition">
|
|
<i data-lucide="search" class="w-4 h-4 me-2"></i>{% trans "Search" %}
|
|
</button>
|
|
<a href="{% url 'standards:search' %}" class="btn-transition inline-flex items-center px-6 py-2.5 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition">
|
|
{% trans "Clear" %}
|
|
</a>
|
|
{% if department %}
|
|
<span class="inline-flex items-center px-4 py-2.5 bg-green-50 text-green-700 rounded-xl text-sm font-medium">
|
|
<i data-lucide="building-2" class="w-4 h-4 me-2"></i>{% trans "Department" %}: {{ department.name }}
|
|
<a href="?{% if query %}q={{ query }}&{% endif %}{% if source_filter %}source={{ source_filter }}&{% endif %}{% if category_filter %}category={{ category_filter }}&{% endif %}" class="ml-2 text-green-600 hover:text-green-800">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</a>
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<div class="section-icon bg-blue/10">
|
|
<i data-lucide="clipboard-list" class="w-5 h-5 text-blue"></i>
|
|
</div>
|
|
<h5 class="text-lg font-semibold text-navy">
|
|
{% if query or source_filter or category_filter or department_filter %}
|
|
{% trans "Search Results" %} ({{ standards.count }})
|
|
{% else %}
|
|
{% trans "All Standards" %} ({{ standards.count }})
|
|
{% endif %}
|
|
</h5>
|
|
</div>
|
|
<div class="p-0">
|
|
{% if department and standards_data %}
|
|
<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 "Status" %}
|
|
</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 item in standards_data %}
|
|
<tr class="hover:bg-slate-50 transition {% if not item.standard.is_assessable or item.standard.is_heading %}bg-slate-50{% endif %}"
|
|
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 {% if not item.standard.is_assessable or item.standard.is_heading %}bg-slate-100 text-slate{% else %}bg-navy/10 text-navy{% endif %}">
|
|
{{ item.standard.code }}
|
|
</span>
|
|
{% if item.has_sub_standards %}
|
|
<button class="ml-2 text-slate-400 hover:text-navy transition" onclick="toggleSubStandards(this)" title="{% trans 'Toggle sub-standards' %}">
|
|
<i data-lucide="chevron-down" class="w-4 h-4 inline"></i>
|
|
</button>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<div class="flex items-center gap-2">
|
|
<a href="{% url 'standards:standard_detail' pk=item.standard.id %}" class="text-sm font-medium text-navy hover:text-navy transition {% if not item.standard.is_assessable or item.standard.is_heading %}italic{% endif %}">
|
|
{{ item.standard.title }}
|
|
</a>
|
|
{% if item.is_substandard %}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold uppercase bg-indigo-100 text-indigo-700">{% trans "Sub-Standard" %}</span>
|
|
{% elif item.has_sub_standards %}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold uppercase bg-amber-100 text-amber-700">{% trans "Standard" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
{% if not item.standard.is_assessable or item.standard.is_heading %}
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-[10px] font-bold uppercase bg-slate-100 text-slate">
|
|
{% trans "Informational" %}
|
|
</span>
|
|
{% else %}
|
|
{% if item.compliance %}
|
|
{% if item.compliance.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>{{ item.compliance.get_status_display }}
|
|
</span>
|
|
{% elif item.compliance.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>{{ item.compliance.get_status_display }}
|
|
</span>
|
|
{% elif item.compliance.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>{{ item.compliance.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>{{ item.compliance.get_status_display }}
|
|
</span>
|
|
{% endif %}
|
|
{% 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>{% trans "Not Assessed" %}
|
|
</span>
|
|
{% endif %}
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
{% if not item.standard.is_assessable or item.standard.is_heading %}
|
|
<span class="text-xs text-slate-400">-</span>
|
|
{% else %}
|
|
<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>{{ item.attachment_count }}
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
{% if not item.standard.is_assessable or item.standard.is_heading %}
|
|
<span class="text-xs text-slate-400">-</span>
|
|
{% else %}
|
|
<div class="flex gap-2 justify-end">
|
|
{% if item.compliance %}
|
|
<button class="btn-transition inline-flex items-center px-3 py-1.5 border border-navy text-navy text-xs font-semibold rounded-lg hover:bg-navy hover:text-white transition"
|
|
onclick="openEvidenceModal('{{ item.compliance.id }}', '{{ item.standard.code|escapejs }}', '{{ item.standard.title|escapejs }}')">
|
|
<i data-lucide="paperclip" class="w-3 h-3 me-1"></i>{% trans "Evidence" %}
|
|
{% if item.attachment_count > 0 %}
|
|
<span class="ml-1 bg-navy text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full">{{ item.attachment_count }}</span>
|
|
{% endif %}
|
|
</button>
|
|
<button 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"
|
|
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-semibold 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 %}
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% for sub in item.sub_standards %}
|
|
<tr class="hover:bg-blue-50/50 transition sub-standard-row bg-slate-50/50"
|
|
data-standard-id="{{ sub.standard.id }}"
|
|
data-compliance-id="{% if sub.compliance %}{{ sub.compliance.id }}{% endif %}"
|
|
data-parent-id="{{ item.standard.id }}">
|
|
<td class="px-6 py-3 whitespace-nowrap">
|
|
<div class="flex items-center">
|
|
<span class="text-slate-300 mr-2">└</span>
|
|
<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.standard.code }}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-3">
|
|
<div class="flex items-center gap-2">
|
|
<a href="{% url 'standards:standard_detail' pk=sub.standard.id %}" class="text-sm font-medium text-navy hover:text-navy transition">
|
|
{{ sub.standard.title }}
|
|
</a>
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold uppercase bg-indigo-100 text-indigo-700">{% trans "Sub-Standard" %}</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-3 whitespace-nowrap">
|
|
{% if sub.compliance %}
|
|
{% if sub.compliance.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>{{ sub.compliance.get_status_display }}
|
|
</span>
|
|
{% elif sub.compliance.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>{{ sub.compliance.get_status_display }}
|
|
</span>
|
|
{% elif sub.compliance.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>{{ sub.compliance.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>{{ sub.compliance.get_status_display }}
|
|
</span>
|
|
{% endif %}
|
|
{% 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>{% trans "Not Assessed" %}
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-3 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>{{ sub.attachment_count }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-3 whitespace-nowrap text-right">
|
|
<div class="flex gap-2 justify-end">
|
|
{% if sub.compliance %}
|
|
<button class="btn-transition inline-flex items-center px-3 py-1.5 border border-indigo-300 text-indigo-600 text-xs font-semibold rounded-lg hover:bg-indigo-500 hover:text-white transition"
|
|
onclick="openEvidenceModal('{{ sub.compliance.id }}', '{{ sub.standard.code|escapejs }}', '{{ sub.standard.title|escapejs }}')">
|
|
<i data-lucide="paperclip" class="w-3 h-3 me-1"></i>{% trans "Evidence" %}
|
|
{% if sub.attachment_count > 0 %}
|
|
<span class="ml-1 bg-indigo-500 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full">{{ sub.attachment_count }}</span>
|
|
{% endif %}
|
|
</button>
|
|
<button class="btn-transition inline-flex items-center px-3 py-1.5 bg-indigo-500 text-white text-xs font-semibold rounded-lg hover:bg-indigo-600 transition"
|
|
onclick="openAssessModal('{{ sub.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-semibold rounded-lg hover:bg-green-600 transition"
|
|
onclick="createAndAssess('{{ sub.standard.id }}')">
|
|
<i data-lucide="plus" class="w-3 h-3 me-1"></i>{% trans "Assess" %}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% elif standards %}
|
|
<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 "Source" %}
|
|
</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">
|
|
{% trans "Category" %}
|
|
</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 standard in standards %}
|
|
<tr class="hover:bg-slate-50 transition {% if standard.parent_standard %}bg-indigo-50/30{% endif %}">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center gap-1">
|
|
{% if standard.parent_standard %}
|
|
<span class="text-slate-300 mr-1">└</span>
|
|
{% endif %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-semibold {% if standard.parent_standard %}bg-indigo-50 text-indigo-700{% else %}bg-navy/10 text-navy{% endif %}">
|
|
{{ standard.code }}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<div class="flex items-center gap-2">
|
|
<a href="{% url 'standards:standard_detail' pk=standard.id %}" class="text-sm font-medium text-navy hover:text-navy transition">
|
|
{{ standard.title }}
|
|
</a>
|
|
{% if standard.parent_standard %}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold uppercase bg-indigo-100 text-indigo-700">{% trans "Sub" %}</span>
|
|
{% elif standard.sub_standards.exists %}
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold uppercase bg-amber-100 text-amber-700">{% trans "Parent" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate">
|
|
{{ standard.source.name }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate">
|
|
{{ standard.category.name }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right">
|
|
<a href="{% url 'standards:standard_detail' pk=standard.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>
|
|
{% else %}
|
|
<div class="text-center py-12 text-slate">
|
|
<i data-lucide="search-x" class="w-16 h-16 mx-auto mb-4 text-slate-300"></i>
|
|
<p class="text-lg mb-4">{% trans "No standards found" %}</p>
|
|
{% if query or source_filter or category_filter or department_filter %}
|
|
<a href="{% url 'standards:search' %}" class="btn-transition inline-flex items-center px-6 py-2.5 bg-navy text-white font-semibold rounded-xl hover:bg-blue transition mx-auto">
|
|
<i data-lucide="refresh-cw" class="w-4 h-4 me-2"></i>{% trans "Clear Filters" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if department %}
|
|
<!-- Compliance 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 "Compliance 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" data-loading data-loading-text="{% trans 'Saving...' %}">
|
|
<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>
|
|
</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 "Auto-filled with today's date" %}</p>
|
|
</div>
|
|
<div>
|
|
<label for="assessor" 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="{{ 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 'Add any notes about the assessment...' %}"></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 supporting this assessment...' %}"></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">
|
|
{% 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 for this attachment...' %}"></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">
|
|
<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 "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>
|
|
{% endif %}
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const sourceSelect = document.getElementById('source');
|
|
const categorySelect = document.getElementById('category');
|
|
if (sourceSelect && categorySelect) {
|
|
sourceSelect.addEventListener('change', function() {
|
|
const sourceId = this.value;
|
|
const currentCat = categorySelect.value;
|
|
categorySelect.innerHTML = '<option value="">{% trans "All Categories" %}</option>';
|
|
if (!sourceId) {
|
|
{% for cat in all_categories %}
|
|
const opt{{ forloop.counter }} = document.createElement('option');
|
|
opt{{ forloop.counter }}.value = '{{ cat.id }}';
|
|
opt{{ forloop.counter }}.textContent = '{{ cat.name|escapejs }}';
|
|
if ('{{ cat.id }}' === currentCat) opt{{ forloop.counter }}.selected = true;
|
|
categorySelect.appendChild(opt{{ forloop.counter }});
|
|
{% endfor %}
|
|
return;
|
|
}
|
|
categorySelect.innerHTML = '<option value="">{% trans "Loading..." %}</option>';
|
|
fetch('/standards/api/categories/?is_active=true&source=' + sourceId)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
categorySelect.innerHTML = '<option value="">{% trans "All Categories" %}</option>';
|
|
(data.results || data).forEach(cat => {
|
|
const opt = document.createElement('option');
|
|
opt.value = cat.id;
|
|
opt.textContent = cat.name;
|
|
if (cat.id === currentCat) opt.selected = true;
|
|
categorySelect.appendChild(opt);
|
|
});
|
|
})
|
|
.catch(() => {
|
|
categorySelect.innerHTML = '<option value="">{% trans "All Categories" %}</option>';
|
|
});
|
|
});
|
|
}
|
|
|
|
{% if department %}
|
|
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 = "{{ user.get_full_name|escapejs }}";
|
|
|
|
const form = document.getElementById('assessmentForm');
|
|
if (form) {
|
|
form.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
submitAssessment();
|
|
});
|
|
}
|
|
{% endif %}
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
{% if department %}
|
|
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 toggleSubStandards(btn) {
|
|
const parentRow = btn.closest('tr');
|
|
const parentId = parentRow.getAttribute('data-standard-id');
|
|
const subRows = document.querySelectorAll(`tr[data-parent-id="${parentId}"]`);
|
|
const icon = btn.querySelector('i, svg');
|
|
subRows.forEach(row => { row.style.display = row.style.display === 'none' ? '' : 'none'; });
|
|
if (icon) icon.style.transform = icon.style.transform === 'rotate(180deg)' ? '' : 'rotate(180deg)';
|
|
}
|
|
|
|
function createAndAssess(standardId) {
|
|
fetch('{% url "standards:compliance_create_ajax" %}', {
|
|
method: 'POST',
|
|
headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ standard_id: standardId, department_id: '{{ department.id }}' })
|
|
})
|
|
.then(r => { if (!r.ok) throw new Error('Server error: ' + r.status); return r.json(); })
|
|
.then(data => {
|
|
if (data.success) openAssessModal(data.compliance_id);
|
|
else alert('{% trans "Error creating compliance record:" %} ' + (data.error || '{% trans "Unknown error" %}'));
|
|
})
|
|
.catch(error => alert('{% trans "Error creating compliance record:" %} ' + error.message));
|
|
}
|
|
|
|
function openAssessModal(complianceId) {
|
|
document.getElementById('complianceId').value = complianceId;
|
|
showModal('assessmentModal');
|
|
}
|
|
|
|
function submitAssessment() {
|
|
const data = {
|
|
compliance_id: document.getElementById('complianceId').value,
|
|
status: document.getElementById('status').value,
|
|
notes: document.getElementById('notes').value || '',
|
|
evidence_summary: document.getElementById('evidence_summary').value || '',
|
|
last_assessed_date: document.getElementById('last_assessed_date').value || '',
|
|
assessor_id: document.getElementById('assessor_id').value || ''
|
|
};
|
|
if (!data.compliance_id || !data.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(data)
|
|
})
|
|
.then(r => { if (!r.ok) throw new Error('Server error: ' + r.status); return r.json(); })
|
|
.then(data => {
|
|
if (data.success) { closeModal('assessmentModal'); location.reload(); }
|
|
else alert('{% trans "Error updating compliance:" %} ' + (data.error || '{% trans "Unknown error" %}'));
|
|
})
|
|
.catch(error => alert('{% trans "Error updating compliance:" %} ' + error.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))
|
|
.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(() => 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></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"><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"><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 deleting attachment:" %} ' + (data.error || '{% trans "Unknown error" %}'));
|
|
})
|
|
.catch(() => 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');
|
|
});
|
|
{% endif %}
|
|
</script>
|
|
{% endblock %}
|