added frontend unit addition/editing
This commit is contained in:
@@ -24,8 +24,10 @@ def get_or_create_roster_unit(db: Session, unit_id: str):
|
|||||||
def add_roster_unit(
|
def add_roster_unit(
|
||||||
id: str = Form(...),
|
id: str = Form(...),
|
||||||
unit_type: str = Form("series3"),
|
unit_type: str = Form("series3"),
|
||||||
deployed: bool = Form(True),
|
deployed: bool = Form(False),
|
||||||
note: str = Form(""),
|
note: str = Form(""),
|
||||||
|
project_id: str = Form(None),
|
||||||
|
location: str = Form(None),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
if db.query(RosterUnit).filter(RosterUnit.id == id).first():
|
if db.query(RosterUnit).filter(RosterUnit.id == id).first():
|
||||||
@@ -36,6 +38,8 @@ def add_roster_unit(
|
|||||||
unit_type=unit_type,
|
unit_type=unit_type,
|
||||||
deployed=deployed,
|
deployed=deployed,
|
||||||
note=note,
|
note=note,
|
||||||
|
project_id=project_id,
|
||||||
|
location=location,
|
||||||
last_updated=datetime.utcnow(),
|
last_updated=datetime.utcnow(),
|
||||||
)
|
)
|
||||||
db.add(unit)
|
db.add(unit)
|
||||||
|
|||||||
@@ -4,9 +4,27 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Fleet Roster</h1>
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Fleet Roster</h1>
|
||||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Real-time status of all seismograph units</p>
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Real-time status of all seismograph units</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button onclick="openAddUnitModal()" class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg flex items-center gap-2 transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
Add Unit
|
||||||
|
</button>
|
||||||
|
<button onclick="openImportModal()" class="px-4 py-2 bg-seismo-navy hover:bg-blue-800 text-white rounded-lg flex items-center gap-2 transition-colors">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||||
|
</svg>
|
||||||
|
Import CSV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Auto-refresh roster every 10 seconds -->
|
<!-- Auto-refresh roster every 10 seconds -->
|
||||||
<div hx-get="/partials/roster-table" hx-trigger="load, every 10s" hx-swap="innerHTML">
|
<div hx-get="/partials/roster-table" hx-trigger="load, every 10s" hx-swap="innerHTML">
|
||||||
@@ -26,4 +44,185 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Unit Modal -->
|
||||||
|
<div id="addUnitModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Add Unit</h2>
|
||||||
|
<button onclick="closeAddUnitModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="addUnitForm" hx-post="/api/roster/add" hx-swap="none" class="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Unit ID *</label>
|
||||||
|
<input type="text" name="id" required
|
||||||
|
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"
|
||||||
|
placeholder="BE1234">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Unit Type</label>
|
||||||
|
<input type="text" name="unit_type" value="series3"
|
||||||
|
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Project ID</label>
|
||||||
|
<input type="text" name="project_id"
|
||||||
|
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"
|
||||||
|
placeholder="PROJ-001">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Location</label>
|
||||||
|
<input type="text" name="location"
|
||||||
|
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"
|
||||||
|
placeholder="San Francisco, CA">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" name="deployed" value="true" checked
|
||||||
|
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
|
||||||
|
<span class="text-sm text-gray-700 dark:text-gray-300">Deployed</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Notes</label>
|
||||||
|
<textarea name="note" rows="3"
|
||||||
|
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"
|
||||||
|
placeholder="Additional notes..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 pt-4">
|
||||||
|
<button type="submit" class="flex-1 px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg transition-colors">
|
||||||
|
Add Unit
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="closeAddUnitModal()" class="px-4 py-2 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 text-gray-700 dark:text-white rounded-lg transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Import CSV Modal -->
|
||||||
|
<div id="importModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-lg w-full mx-4">
|
||||||
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Import CSV</h2>
|
||||||
|
<button onclick="closeImportModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="importForm" class="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">CSV File *</label>
|
||||||
|
<input type="file" name="file" accept=".csv" required
|
||||||
|
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Format: unit_id,unit_type,deployed,retired,note,project_id,location</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" name="update_existing" id="updateExisting" checked
|
||||||
|
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
|
||||||
|
<label for="updateExisting" class="text-sm text-gray-700 dark:text-gray-300 cursor-pointer">Update existing units</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 pt-4">
|
||||||
|
<button type="submit" class="flex-1 px-4 py-2 bg-seismo-navy hover:bg-blue-800 text-white rounded-lg transition-colors">
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="closeImportModal()" class="px-4 py-2 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 text-gray-700 dark:text-white rounded-lg transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="importResult" class="hidden mt-4 p-4 rounded-lg"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Add Unit Modal
|
||||||
|
function openAddUnitModal() {
|
||||||
|
document.getElementById('addUnitModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAddUnitModal() {
|
||||||
|
document.getElementById('addUnitModal').classList.add('hidden');
|
||||||
|
document.getElementById('addUnitForm').reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import Modal
|
||||||
|
function openImportModal() {
|
||||||
|
document.getElementById('importModal').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeImportModal() {
|
||||||
|
document.getElementById('importModal').classList.add('hidden');
|
||||||
|
document.getElementById('importForm').reset();
|
||||||
|
document.getElementById('importResult').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Add Unit form submission
|
||||||
|
document.getElementById('addUnitForm').addEventListener('htmx:afterRequest', function(event) {
|
||||||
|
if (event.detail.successful) {
|
||||||
|
closeAddUnitModal();
|
||||||
|
// Trigger roster refresh
|
||||||
|
htmx.trigger(document.querySelector('[hx-get="/partials/roster-table"]'), 'load');
|
||||||
|
// Show success message
|
||||||
|
alert('Unit added successfully!');
|
||||||
|
} else {
|
||||||
|
alert('Error adding unit. Please check the form and try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle CSV Import
|
||||||
|
document.getElementById('importForm').addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const resultDiv = document.getElementById('importResult');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/roster/import-csv', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
resultDiv.className = 'mt-4 p-4 rounded-lg bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200';
|
||||||
|
resultDiv.innerHTML = `
|
||||||
|
<p class="font-semibold mb-2">Import Successful!</p>
|
||||||
|
<ul class="text-sm space-y-1">
|
||||||
|
<li>✅ Added: ${result.summary.added}</li>
|
||||||
|
<li>🔄 Updated: ${result.summary.updated}</li>
|
||||||
|
<li>⏭️ Skipped: ${result.summary.skipped}</li>
|
||||||
|
<li>❌ Errors: ${result.summary.errors}</li>
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
resultDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Trigger roster refresh
|
||||||
|
htmx.trigger(document.querySelector('[hx-get="/partials/roster-table"]'), 'load');
|
||||||
|
|
||||||
|
// Close modal after 2 seconds
|
||||||
|
setTimeout(() => closeImportModal(), 2000);
|
||||||
|
} else {
|
||||||
|
resultDiv.className = 'mt-4 p-4 rounded-lg bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200';
|
||||||
|
resultDiv.innerHTML = `<p class="font-semibold">Import Failed</p><p class="text-sm">${result.detail || 'Unknown error'}</p>`;
|
||||||
|
resultDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
resultDiv.className = 'mt-4 p-4 rounded-lg bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200';
|
||||||
|
resultDiv.innerHTML = `<p class="font-semibold">Import Failed</p><p class="text-sm">${error.message}</p>`;
|
||||||
|
resultDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user