377 lines
21 KiB
HTML
377 lines
21 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "Import Staff" %} - PX360{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.page-header-gradient {
|
|
background: linear-gradient(135deg, #005696 0%, #0069a8 50%, #007bbd 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);
|
|
}
|
|
.drop-zone {
|
|
border: 2px dashed #cbd5e1; border-radius: 1rem; padding: 2.5rem;
|
|
text-align: center; transition: all 0.3s ease; cursor: pointer;
|
|
}
|
|
.drop-zone:hover, .drop-zone.drag-over {
|
|
border-color: #005696; background: #f0f7ff;
|
|
}
|
|
.stat-card {
|
|
background: white; border-radius: 1rem; border: 2px solid #e2e8f0; padding: 1.25rem;
|
|
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 8px 15px rgba(0,0,0,0.1); }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="p-6">
|
|
<!-- Breadcrumb -->
|
|
<div class="flex items-center gap-2 text-sm text-slate-500 mb-4">
|
|
<a href="{% url 'organizations:staff_list' %}" class="hover:text-navy transition">{% trans "Staff" %}</a>
|
|
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
|
<span class="text-navy font-semibold">{% trans "Import" %}</span>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="page-header-gradient">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h1 class="text-2xl font-bold mb-1">{% trans "Import Staff" %}</h1>
|
|
<p class="text-blue-100 text-sm">{% trans "Upload a CSV file to bulk-import staff into" %} {{ hospital.name }}</p>
|
|
</div>
|
|
<a href="{% url 'organizations:staff_import_sample_csv' %}"
|
|
class="inline-flex items-center gap-2 px-4 py-2.5 bg-white text-navy font-medium rounded-xl hover:bg-blue-50 transition">
|
|
<i data-lucide="download" class="w-4 h-4"></i>{% trans "Sample CSV" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Upload Form -->
|
|
<div class="lg:col-span-2">
|
|
<form method="post" enctype="multipart/form-data" id="importForm">
|
|
{% csrf_token %}
|
|
|
|
<!-- File Upload -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6 mb-6">
|
|
<h2 class="text-lg font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="upload" class="w-5 h-5"></i> {% trans "Upload CSV File" %}
|
|
</h2>
|
|
<div class="drop-zone" id="dropZone" onclick="document.getElementById('csvFile').click()">
|
|
<i data-lucide="file-up" class="w-12 h-12 mx-auto text-slate-300 mb-3"></i>
|
|
<p class="text-slate-600 font-semibold mb-1">{% trans "Click to upload or drag and drop" %}</p>
|
|
<p class="text-slate-400 text-sm">{% trans "CSV files only. UTF-8 encoding recommended." %}</p>
|
|
<input type="file" name="csv_file" id="csvFile" accept=".csv" class="hidden" onchange="showFileName(this)">
|
|
<p class="mt-3 text-sm font-semibold text-navy hidden" id="fileName"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Options -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6 mb-6">
|
|
<h2 class="text-lg font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="settings" class="w-5 h-5"></i> {% trans "Import Options" %}
|
|
</h2>
|
|
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Default Staff Type" %}</label>
|
|
<select name="staff_type" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-navy text-sm">
|
|
<option value="physician">{% trans "Physician" %}</option>
|
|
<option value="nurse">{% trans "Nurse" %}</option>
|
|
<option value="admin">{% trans "Administrative" %}</option>
|
|
<option value="other" selected>{% trans "Other" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<label class="flex items-center gap-3 p-3 border-2 border-slate-100 rounded-xl hover:border-navy/20 transition cursor-pointer">
|
|
<input type="checkbox" name="update_existing" class="w-4 h-4 accent-navy">
|
|
<div>
|
|
<p class="font-semibold text-navy text-sm">{% trans "Update existing staff" %}</p>
|
|
<p class="text-slate-400 text-xs">{% trans "When Staff ID matches, update the record instead of skipping" %}</p>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center gap-3 p-3 border-2 border-slate-100 rounded-xl hover:border-navy/20 transition cursor-pointer">
|
|
<input type="checkbox" name="create_departments" class="w-4 h-4 accent-navy">
|
|
<div>
|
|
<p class="font-semibold text-navy text-sm">{% trans "Auto-create departments" %}</p>
|
|
<p class="text-slate-400 text-xs">{% trans "Create departments, sections, and subsections that don't exist yet" %}</p>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center gap-3 p-3 border-2 border-amber-200 bg-amber-50/50 rounded-xl hover:border-amber-300 transition cursor-pointer">
|
|
<input type="checkbox" name="dry_run" class="w-4 h-4 accent-amber-500" checked>
|
|
<div>
|
|
<p class="font-semibold text-amber-700 text-sm">{% trans "Dry run (preview only)" %}</p>
|
|
<p class="text-amber-600 text-xs">{% trans "Preview results without making any changes to the database" %}</p>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center gap-3 p-3 border-2 border-red-100 bg-red-50/30 rounded-xl hover:border-red-200 transition cursor-pointer">
|
|
<input type="checkbox" name="deactivate_missing" class="w-4 h-4 accent-red-500">
|
|
<div>
|
|
<p class="font-semibold text-red-700 text-sm">{% trans "Deactivate staff not in CSV" %}</p>
|
|
<p class="text-red-500 text-xs">{% trans "Active staff in the hospital whose ID is not in the CSV will be set to inactive" %}</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Submit -->
|
|
<div class="flex gap-3">
|
|
<button type="submit" class="flex-1 bg-navy text-white px-6 py-3 rounded-xl font-semibold hover:bg-blue transition flex items-center justify-center gap-2">
|
|
<i data-lucide="upload" class="w-4 h-4"></i>
|
|
<span id="submitLabel">{% trans "Preview Import" %}</span>
|
|
</button>
|
|
<a href="{% url 'organizations:staff_list' %}" class="px-6 py-3 border-2 border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition">
|
|
{% trans "Cancel" %}
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div>
|
|
<!-- CSV Format Reference -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6 mb-6">
|
|
<h3 class="text-sm font-bold text-navy mb-3 flex items-center gap-2">
|
|
<i data-lucide="info" class="w-4 h-4"></i> {% trans "Expected Columns" %}
|
|
</h3>
|
|
<div class="space-y-1.5 text-xs">
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-green-100 text-green-700 rounded font-bold">Required</span>
|
|
<span class="text-slate-700 font-mono">Staff ID, Name</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded font-bold">Optional</span>
|
|
<span class="text-slate-500 font-mono">Name_ar</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded font-bold">Optional</span>
|
|
<span class="text-slate-500 font-mono">Manager, Manager_ar</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded font-bold">Optional</span>
|
|
<span class="text-slate-500 font-mono">Civil Identity Number</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded font-bold">Optional</span>
|
|
<span class="text-slate-500 font-mono">Location, Location_ar</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded font-bold">Optional</span>
|
|
<span class="text-slate-500 font-mono">Department, Department_ar</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded font-bold">Optional</span>
|
|
<span class="text-slate-500 font-mono">Section, Section_ar</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded font-bold">Optional</span>
|
|
<span class="text-slate-500 font-mono">Subsection, Subsection_ar</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded font-bold">Optional</span>
|
|
<span class="text-slate-500 font-mono">AlHammadi Job Title, ..._ar</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded font-bold">Optional</span>
|
|
<span class="text-slate-500 font-mono">Country, Country_ar</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 pt-4 border-t border-slate-100">
|
|
<p class="text-xs text-slate-400">
|
|
<i data-lucide="lightbulb" class="w-3 h-3 inline"></i>
|
|
{% trans "Download the sample CSV for the exact format with an example row." %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tips -->
|
|
<div class="bg-blue-50 border border-blue-100 rounded-2xl p-6">
|
|
<h3 class="text-sm font-bold text-blue-800 mb-3 flex items-center gap-2">
|
|
<i data-lucide="lightbulb" class="w-4 h-4"></i> {% trans "Tips" %}
|
|
</h3>
|
|
<ul class="text-xs text-blue-700 space-y-2">
|
|
<li class="flex items-start gap-2">
|
|
<i data-lucide="check-circle" class="w-3.5 h-3.5 mt-0.5 shrink-0"></i>
|
|
{% trans "Start with a dry run to preview results before importing." %}
|
|
</li>
|
|
<li class="flex items-start gap-2">
|
|
<i data-lucide="check-circle" class="w-3.5 h-3.5 mt-0.5 shrink-0"></i>
|
|
{% trans "Staff ID must be unique within the hospital." %}
|
|
</li>
|
|
<li class="flex items-start gap-2">
|
|
<i data-lucide="check-circle" class="w-3.5 h-3.5 mt-0.5 shrink-0"></i>
|
|
{% trans "Manager format: \"EMP001 - Name\" (ID dash Name)." %}
|
|
</li>
|
|
<li class="flex items-start gap-2">
|
|
<i data-lucide="check-circle" class="w-3.5 h-3.5 mt-0.5 shrink-0"></i>
|
|
{% trans "Save your CSV as UTF-8 for Arabic text support." %}
|
|
</li>
|
|
<li class="flex items-start gap-2">
|
|
<i data-lucide="check-circle" class="w-3.5 h-3.5 mt-0.5 shrink-0"></i>
|
|
{% trans "Use 'Deactivate missing' to sync: staff not in the CSV will be set to inactive." %}
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Section -->
|
|
{% if results %}
|
|
<div class="mt-8">
|
|
<h2 class="text-xl font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="bar-chart-3" class="w-5 h-5"></i>
|
|
{% if results.dry_run %}{% trans "Preview Results" %}{% else %}{% trans "Import Results" %}{% endif %}
|
|
</h2>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="grid grid-cols-2 md:grid-cols-6 gap-4 mb-6">
|
|
<div class="stat-card">
|
|
<p class="text-xs font-bold text-slate uppercase">{% trans "Total Rows" %}</p>
|
|
<p class="text-2xl font-bold text-navy mt-1">{{ results.total_rows }}</p>
|
|
</div>
|
|
<div class="stat-card" style="border-color: #dcfce7;">
|
|
<p class="text-xs font-bold text-green-600 uppercase">{% trans "Created" %}</p>
|
|
<p class="text-2xl font-bold text-green-600 mt-1">{{ results.created_count }}</p>
|
|
</div>
|
|
<div class="stat-card" style="border-color: #dbeafe;">
|
|
<p class="text-xs font-bold text-blue-600 uppercase">{% trans "Updated" %}</p>
|
|
<p class="text-2xl font-bold text-blue-600 mt-1">{{ results.updated_count }}</p>
|
|
</div>
|
|
<div class="stat-card" style="border-color: #fef9c3;">
|
|
<p class="text-xs font-bold text-amber-600 uppercase">{% trans "Skipped" %}</p>
|
|
<p class="text-2xl font-bold text-amber-600 mt-1">{{ results.skipped_count }}</p>
|
|
</div>
|
|
<div class="stat-card" style="border-color: #e9d5ff;">
|
|
<p class="text-xs font-bold text-purple-600 uppercase">{% trans "Deactivated" %}</p>
|
|
<p class="text-2xl font-bold text-purple-600 mt-1">{{ results.deactivated_count }}</p>
|
|
</div>
|
|
<div class="stat-card" style="border-color: #fee2e2;">
|
|
<p class="text-xs font-bold text-red-600 uppercase">{% trans "Errors" %}</p>
|
|
<p class="text-2xl font-bold text-red-600 mt-1">{{ results.error_count }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Table -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-slate-50 border-b">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Row" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Staff ID" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Name" %}</th>
|
|
<th class="px-4 py-3 text-left text-[10px] font-bold text-slate uppercase">{% trans "Status" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for r in results.created %}
|
|
<tr class="hover:bg-green-50/50">
|
|
<td class="px-4 py-3 text-sm text-slate">{{ r.row }}</td>
|
|
<td class="px-4 py-3 text-sm font-mono text-navy">{{ r.id }}</td>
|
|
<td class="px-4 py-3 text-sm">{{ r.name }}</td>
|
|
<td class="px-4 py-3"><span class="px-2 py-1 bg-green-100 text-green-700 rounded text-xs font-bold">{{ r.message }}</span></td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% for r in results.updated %}
|
|
<tr class="hover:bg-blue-50/50">
|
|
<td class="px-4 py-3 text-sm text-slate">{{ r.row }}</td>
|
|
<td class="px-4 py-3 text-sm font-mono text-navy">{{ r.id }}</td>
|
|
<td class="px-4 py-3 text-sm">{{ r.name }}</td>
|
|
<td class="px-4 py-3"><span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-bold">{{ r.message }}</span></td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% for r in results.skipped %}
|
|
<tr class="hover:bg-amber-50/50">
|
|
<td class="px-4 py-3 text-sm text-slate">{{ r.row }}</td>
|
|
<td class="px-4 py-3 text-sm font-mono text-navy">{{ r.id }}</td>
|
|
<td class="px-4 py-3 text-sm">{{ r.name }}</td>
|
|
<td class="px-4 py-3"><span class="px-2 py-1 bg-amber-100 text-amber-700 rounded text-xs font-bold">{{ r.message }}</span></td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% for r in results.errors %}
|
|
<tr class="hover:bg-red-50/50">
|
|
<td class="px-4 py-3 text-sm text-slate">{{ r.row }}</td>
|
|
<td class="px-4 py-3 text-sm font-mono text-navy">{{ r.id }}</td>
|
|
<td class="px-4 py-3 text-sm">{{ r.name }}</td>
|
|
<td class="px-4 py-3"><span class="px-2 py-1 bg-red-100 text-red-700 rounded text-xs font-bold">{{ r.message }}</span></td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% for r in results.deactivated %}
|
|
<tr class="hover:bg-purple-50/50">
|
|
<td class="px-4 py-3 text-sm text-slate">{{ r.row }}</td>
|
|
<td class="px-4 py-3 text-sm font-mono text-navy">{{ r.id }}</td>
|
|
<td class="px-4 py-3 text-sm">{{ r.name }}</td>
|
|
<td class="px-4 py-3"><span class="px-2 py-1 bg-purple-100 text-purple-700 rounded text-xs font-bold">{{ r.message }}</span></td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% if results.total_rows == 0 %}
|
|
<tr><td colspan="4" class="px-4 py-8 text-center text-slate">{% trans "No rows found in the CSV file." %}</td></tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Re-import with actual data -->
|
|
{% if results.dry_run and results.error_count == 0 and results.total_rows > 0 %}
|
|
<div class="mt-4 p-4 bg-green-50 border border-green-200 rounded-xl flex items-center justify-between">
|
|
<p class="text-green-700 text-sm font-semibold">
|
|
<i data-lucide="check-circle" class="w-4 h-4 inline"></i>
|
|
{% trans "Preview looks good! Uncheck 'Dry run' and submit again to apply the import." %}
|
|
{% if results.deactivated_count > 0 %}
|
|
<span class="text-purple-600 ml-2">({{ results.deactivated_count }} {% trans "staff would be deactivated" %})</span>
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
lucide.createIcons();
|
|
});
|
|
|
|
const dropZone = document.getElementById('dropZone');
|
|
const fileInput = document.getElementById('csvFile');
|
|
|
|
if (dropZone) {
|
|
dropZone.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
dropZone.classList.add('drag-over');
|
|
});
|
|
dropZone.addEventListener('dragleave', function() {
|
|
dropZone.classList.remove('drag-over');
|
|
});
|
|
dropZone.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('drag-over');
|
|
if (e.dataTransfer.files.length) {
|
|
fileInput.files = e.dataTransfer.files;
|
|
showFileName(fileInput);
|
|
}
|
|
});
|
|
}
|
|
|
|
function showFileName(input) {
|
|
const label = document.getElementById('fileName');
|
|
if (input.files.length) {
|
|
label.textContent = input.files[0].name;
|
|
label.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
const dryRunCheckbox = document.querySelector('input[name="dry_run"]');
|
|
const submitLabel = document.getElementById('submitLabel');
|
|
if (dryRunCheckbox && submitLabel) {
|
|
dryRunCheckbox.addEventListener('change', function() {
|
|
submitLabel.textContent = this.checked ? '{% trans "Preview Import" %}' : '{% trans "Import Staff" %}';
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|