400 lines
16 KiB
HTML
400 lines
16 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% trans "New Observation" %} - PX360{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.page-header {
|
|
background: linear-gradient(135deg, #005696 0%, #0069a8 50%, #007bbd 100%);
|
|
color: white;
|
|
padding: 2rem 2.5rem;
|
|
border-radius: 1rem;
|
|
margin-bottom: 2rem;
|
|
box-shadow: 0 10px 15px -3px rgba(0, 86, 150, 0.2);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.page-header::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 200px;
|
|
height: 200px;
|
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
.back-link {
|
|
color: rgba(255,255,255,0.85);
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-weight: 500;
|
|
margin-bottom: 1rem;
|
|
transition: color 0.2s ease;
|
|
}
|
|
.back-link:hover {
|
|
color: white;
|
|
}
|
|
.form-section {
|
|
background: white;
|
|
border-radius: 1rem;
|
|
border: 1px solid #e2e8f0;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
transition: all 0.3s ease;
|
|
}
|
|
.form-section:hover {
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
transform: translateY(-2px);
|
|
}
|
|
.form-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 2px solid #f1f5f9;
|
|
}
|
|
.form-section-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 0.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.severity-option {
|
|
cursor: pointer;
|
|
padding: 12px 16px;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
text-align: center;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.severity-option:hover {
|
|
border-color: #005696;
|
|
background: #f8fafc;
|
|
}
|
|
.severity-option.selected {
|
|
border-color: #005696;
|
|
background: #eef6fb;
|
|
box-shadow: 0 0 0 3px rgba(0, 86, 150, 0.1);
|
|
}
|
|
.severity-option.selected i {
|
|
transform: scale(1.2);
|
|
}
|
|
.file-upload-area {
|
|
border: 2px dashed #e2e8f0;
|
|
border-radius: 12px;
|
|
padding: 30px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.file-upload-area:hover {
|
|
border-color: #005696;
|
|
background: #f8fafc;
|
|
}
|
|
.field-error {
|
|
color: #ef4444;
|
|
font-size: 0.75rem;
|
|
margin-top: 0.25rem;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Page Header -->
|
|
<div class="page-header">
|
|
<a href="{% url 'observations:observation_list' %}" class="back-link">
|
|
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
|
{% trans "Back to Observations" %}
|
|
</a>
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center">
|
|
<i data-lucide="eye" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-2xl font-bold">{% trans "New Observation" %}</h1>
|
|
<p class="text-sm opacity-75">{% trans "Submit a staff observation report" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Messages -->
|
|
{% if messages %}
|
|
{% for message in messages %}
|
|
<div class="bg-{% if message.tags == 'error' %}red{% elif message.tags == 'warning' %}yellow{% else %}green{% endif %}-50 border border-{% if message.tags == 'error' %}red{% elif message.tags == 'warning' %}yellow{% else %}green{% endif %}-200 rounded-xl p-4 mb-4 text-sm text-{% if message.tags == 'error' %}red{% elif message.tags == 'warning' %}yellow{% else %}green{% endif %}-800">
|
|
{{ message }}
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
<!-- Form -->
|
|
<form method="post" enctype="multipart/form-data" id="observationForm">
|
|
{% csrf_token %}
|
|
|
|
<!-- Observation Details -->
|
|
<div class="form-section">
|
|
<div class="form-section-header">
|
|
<div class="form-section-icon" style="background: linear-gradient(135deg, #005696, #007bbd);">
|
|
<i data-lucide="file-text" style="width: 20px; height: 20px; color: white;"></i>
|
|
</div>
|
|
<div>
|
|
<h5 class="mb-0 fw-bold">{% trans "Observation Details" %}</h5>
|
|
<small class="text-slate">{% trans "Describe what was observed" %}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<!-- Category -->
|
|
<div class="col-md-6">
|
|
<label class="block text-xs font-bold text-slate uppercase mb-2">
|
|
<i data-lucide="tag" class="w-3.5 h-3.5 inline-block mr-1"></i>
|
|
{% trans "Category" %}
|
|
<span class="text-xs font-normal text-slate normal-case">({% trans "optional" %})</span>
|
|
</label>
|
|
{{ form.category }}
|
|
{% if form.category.errors %}
|
|
<div class="field-error">{{ form.category.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Title -->
|
|
<div class="col-md-6">
|
|
<label class="block text-xs font-bold text-slate uppercase mb-2">
|
|
<i data-lucide="type" class="w-3.5 h-3.5 inline-block mr-1"></i>
|
|
{% trans "Title" %}
|
|
<span class="text-xs font-normal text-slate normal-case">({% trans "optional" %})</span>
|
|
</label>
|
|
{{ form.title }}
|
|
{% if form.title.errors %}
|
|
<div class="field-error">{{ form.title.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="col-12">
|
|
<label class="block text-xs font-bold text-slate uppercase mb-2">
|
|
<i data-lucide="align-left" class="w-3.5 h-3.5 inline-block mr-1"></i>
|
|
{% trans "Description" %}
|
|
<span class="text-red-500">*</span>
|
|
</label>
|
|
{{ form.description }}
|
|
{% if form.description.errors %}
|
|
<div class="field-error">{{ form.description.errors.0 }}</div>
|
|
{% endif %}
|
|
<p class="text-xs text-slate mt-1">{% trans "Please describe what you observed in detail (at least 10 characters)." %}</p>
|
|
</div>
|
|
|
|
<!-- Severity -->
|
|
<div class="col-12">
|
|
<label class="block text-xs font-bold text-slate uppercase mb-2">
|
|
<i data-lucide="alert-triangle" class="w-3.5 h-3.5 inline-block mr-1"></i>
|
|
{% trans "Severity" %}
|
|
</label>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<div class="severity-option" data-value="low" onclick="selectSeverity(this)">
|
|
<i data-lucide="info" class="w-5 h-5 text-green-600 mx-auto mb-1 transition"></i>
|
|
<div class="text-xs font-bold text-slate">{% trans "Low" %}</div>
|
|
</div>
|
|
<div class="severity-option selected" data-value="medium" onclick="selectSeverity(this)">
|
|
<i data-lucide="alert-circle" class="w-5 h-5 text-yellow-600 mx-auto mb-1 transition"></i>
|
|
<div class="text-xs font-bold text-slate">{% trans "Medium" %}</div>
|
|
</div>
|
|
<div class="severity-option" data-value="high" onclick="selectSeverity(this)">
|
|
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-500 mx-auto mb-1 transition"></i>
|
|
<div class="text-xs font-bold text-slate">{% trans "High" %}</div>
|
|
</div>
|
|
<div class="severity-option" data-value="critical" onclick="selectSeverity(this)">
|
|
<i data-lucide="x-octagon" class="w-5 h-5 text-gray-800 mx-auto mb-1 transition"></i>
|
|
<div class="text-xs font-bold text-slate">{% trans "Critical" %}</div>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" name="severity" id="severityInput" value="medium">
|
|
{% if form.severity.errors %}
|
|
<div class="field-error mt-2">{{ form.severity.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Location & Timing -->
|
|
<div class="form-section">
|
|
<div class="form-section-header">
|
|
<div class="form-section-icon" style="background: linear-gradient(135deg, #0891b2, #06b6d4);">
|
|
<i data-lucide="map-pin" style="width: 20px; height: 20px; color: white;"></i>
|
|
</div>
|
|
<div>
|
|
<h5 class="mb-0 fw-bold">{% trans "Location & Timing" %}</h5>
|
|
<small class="text-slate">{% trans "Where and when did this occur?" %}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<!-- Location -->
|
|
<div class="col-md-6">
|
|
<label class="block text-xs font-bold text-slate uppercase mb-2">
|
|
<i data-lucide="map-pin" class="w-3.5 h-3.5 inline-block mr-1"></i>
|
|
{% trans "Location" %}
|
|
<span class="text-xs font-normal text-slate normal-case">({% trans "optional" %})</span>
|
|
</label>
|
|
{{ form.location_text }}
|
|
{% if form.location_text.errors %}
|
|
<div class="field-error">{{ form.location_text.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Incident Date/Time -->
|
|
<div class="col-md-6">
|
|
<label class="block text-xs font-bold text-slate uppercase mb-2">
|
|
<i data-lucide="calendar" class="w-3.5 h-3.5 inline-block mr-1"></i>
|
|
{% trans "When did this occur?" %}
|
|
</label>
|
|
{{ form.incident_datetime }}
|
|
{% if form.incident_datetime.errors %}
|
|
<div class="field-error">{{ form.incident_datetime.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assignment -->
|
|
<div class="form-section">
|
|
<div class="form-section-header">
|
|
<div class="form-section-icon" style="background: linear-gradient(135deg, #7c3aed, #8b5cf6);">
|
|
<i data-lucide="user-check" style="width: 20px; height: 20px; color: white;"></i>
|
|
</div>
|
|
<div>
|
|
<h5 class="mb-0 fw-bold">{% trans "Assignment" %}</h5>
|
|
<small class="text-slate">{% trans "Optionally assign to a department or user" %}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<!-- Assigned Department -->
|
|
<div class="col-md-6">
|
|
<label class="block text-xs font-bold text-slate uppercase mb-2">
|
|
<i data-lucide="building" class="w-3.5 h-3.5 inline-block mr-1"></i>
|
|
{% trans "Department" %}
|
|
<span class="text-xs font-normal text-slate normal-case">({% trans "optional" %})</span>
|
|
</label>
|
|
{{ form.assigned_department }}
|
|
{% if form.assigned_department.errors %}
|
|
<div class="field-error">{{ form.assigned_department.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Assigned To -->
|
|
<div class="col-md-6">
|
|
<label class="block text-xs font-bold text-slate uppercase mb-2">
|
|
<i data-lucide="user" class="w-3.5 h-3.5 inline-block mr-1"></i>
|
|
{% trans "Assign To" %}
|
|
<span class="text-xs font-normal text-slate normal-case">({% trans "optional" %})</span>
|
|
</label>
|
|
{{ form.assigned_to }}
|
|
{% if form.assigned_to.errors %}
|
|
<div class="field-error">{{ form.assigned_to.errors.0 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Attachments -->
|
|
<div class="form-section">
|
|
<div class="form-section-header">
|
|
<div class="form-section-icon" style="background: linear-gradient(135deg, #059669, #10b981);">
|
|
<i data-lucide="paperclip" style="width: 20px; height: 20px; color: white;"></i>
|
|
</div>
|
|
<div>
|
|
<h5 class="mb-0 fw-bold">{% trans "Attachments" %}</h5>
|
|
<small class="text-slate">{% trans "Upload supporting documents or images" %}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="file-upload-area" onclick="document.getElementById('attachments').click()">
|
|
<i data-lucide="cloud-upload" class="w-8 h-8 text-navy mx-auto mb-2"></i>
|
|
<p class="text-sm font-semibold text-slate mb-1">{% trans "Click to upload files" %}</p>
|
|
<p class="text-xs text-slate mb-0">{% trans "Images, PDF, Word, Excel (max 10MB each)" %}</p>
|
|
</div>
|
|
<input type="file" id="attachments" name="attachments" multiple
|
|
accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx"
|
|
style="display: none;" onchange="updateFileList()">
|
|
<div id="fileList" class="mt-2"></div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex gap-3 justify-end">
|
|
<a href="{% url 'observations:observation_list' %}" class="px-6 py-3 border-2 border-slate-200 text-slate rounded-xl font-bold text-sm hover:bg-gray-50 transition flex items-center gap-2">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
{% trans "Cancel" %}
|
|
</a>
|
|
<button type="submit" class="px-8 py-3 bg-navy text-white rounded-xl font-bold text-sm hover:bg-blue transition shadow-lg shadow-navy/25 flex items-center gap-2">
|
|
<i data-lucide="send" class="w-4 h-4"></i>
|
|
{% trans "Create Observation" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
function selectSeverity(el) {
|
|
document.querySelectorAll('.severity-option').forEach(o => o.classList.remove('selected'));
|
|
el.classList.add('selected');
|
|
document.getElementById('severityInput').value = el.dataset.value;
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function updateFileList() {
|
|
const input = document.getElementById('attachments');
|
|
const fileList = document.getElementById('fileList');
|
|
fileList.innerHTML = '';
|
|
|
|
if (input.files.length > 0) {
|
|
let html = '<div class="space-y-2 mt-3">';
|
|
for (let i = 0; i < input.files.length; i++) {
|
|
html += '<div class="flex items-center gap-2 bg-slate-50 rounded-lg px-3 py-2 text-sm text-slate"><i data-lucide="file" class="w-4 h-4"></i>' + input.files[i].name + '</div>';
|
|
}
|
|
html += '</div>';
|
|
fileList.innerHTML = html;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const deptSelect = document.getElementById('id_assigned_department');
|
|
const assigneeSelect = document.getElementById('id_assigned_to');
|
|
|
|
if (deptSelect && assigneeSelect) {
|
|
deptSelect.addEventListener('change', function() {
|
|
const deptId = this.value;
|
|
if (!deptId) {
|
|
assigneeSelect.innerHTML = '<option value="">{% trans "Select assignee (optional)" %}</option>';
|
|
return;
|
|
}
|
|
|
|
fetch("{% url 'observations:get_users_by_department' %}?department_id=" + deptId)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
let html = '<option value="">{% trans "Select assignee (optional)" %}</option>';
|
|
data.users.forEach(function(user) {
|
|
html += '<option value="' + user.id + '">' + user.name + '</option>';
|
|
});
|
|
assigneeSelect.innerHTML = html;
|
|
})
|
|
.catch(function() {
|
|
console.error('Failed to load users for department');
|
|
});
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|