unit history added
This commit is contained in:
78
backend/migrate_add_unit_history.py
Normal file
78
backend/migrate_add_unit_history.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"""
|
||||||
|
Migration script to add unit history timeline support.
|
||||||
|
|
||||||
|
This creates the unit_history table to track all changes to units:
|
||||||
|
- Note changes (archived old notes, new notes)
|
||||||
|
- Deployment status changes (deployed/benched)
|
||||||
|
- Retired status changes
|
||||||
|
- Other field changes
|
||||||
|
|
||||||
|
Run this script once to migrate an existing database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Database path
|
||||||
|
DB_PATH = "./data/seismo_fleet.db"
|
||||||
|
|
||||||
|
def migrate_database():
|
||||||
|
"""Create the unit_history table"""
|
||||||
|
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
print(f"Database not found at {DB_PATH}")
|
||||||
|
print("The database will be created automatically when you run the application.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Migrating database: {DB_PATH}")
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if unit_history table already exists
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='unit_history'")
|
||||||
|
if cursor.fetchone():
|
||||||
|
print("Migration already applied - unit_history table exists")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Creating unit_history table...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE unit_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
unit_id TEXT NOT NULL,
|
||||||
|
change_type TEXT NOT NULL,
|
||||||
|
field_name TEXT,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
changed_at TIMESTAMP NOT NULL,
|
||||||
|
source TEXT DEFAULT 'manual',
|
||||||
|
notes TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
print(" ✓ Created unit_history table")
|
||||||
|
|
||||||
|
# Create indexes for better query performance
|
||||||
|
cursor.execute("CREATE INDEX idx_unit_history_unit_id ON unit_history(unit_id)")
|
||||||
|
print(" ✓ Created index on unit_id")
|
||||||
|
|
||||||
|
cursor.execute("CREATE INDEX idx_unit_history_changed_at ON unit_history(changed_at)")
|
||||||
|
print(" ✓ Created index on changed_at")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("\nMigration completed successfully!")
|
||||||
|
print("Units will now track their complete history of changes.")
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"\nError during migration: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
migrate_database()
|
||||||
@@ -59,6 +59,24 @@ class IgnoredUnit(Base):
|
|||||||
ignored_at = Column(DateTime, default=datetime.utcnow)
|
ignored_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class UnitHistory(Base):
|
||||||
|
"""
|
||||||
|
Unit history: complete timeline of changes to each unit.
|
||||||
|
Tracks note changes, status changes, deployment/benched events, and more.
|
||||||
|
"""
|
||||||
|
__tablename__ = "unit_history"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
unit_id = Column(String, nullable=False, index=True) # FK to RosterUnit.id
|
||||||
|
change_type = Column(String, nullable=False) # note_change, deployed_change, retired_change, etc.
|
||||||
|
field_name = Column(String, nullable=True) # Which field changed
|
||||||
|
old_value = Column(Text, nullable=True) # Previous value
|
||||||
|
new_value = Column(Text, nullable=True) # New value
|
||||||
|
changed_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||||
|
source = Column(String, default="manual") # manual, csv_import, telemetry, offline_sync
|
||||||
|
notes = Column(Text, nullable=True) # Optional reason/context for the change
|
||||||
|
|
||||||
|
|
||||||
class UserPreferences(Base):
|
class UserPreferences(Base):
|
||||||
"""
|
"""
|
||||||
User preferences: persistent storage for application settings.
|
User preferences: persistent storage for application settings.
|
||||||
|
|||||||
@@ -5,11 +5,28 @@ import csv
|
|||||||
import io
|
import io
|
||||||
|
|
||||||
from backend.database import get_db
|
from backend.database import get_db
|
||||||
from backend.models import RosterUnit, IgnoredUnit, Emitter
|
from backend.models import RosterUnit, IgnoredUnit, Emitter, UnitHistory
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/roster", tags=["roster-edit"])
|
router = APIRouter(prefix="/api/roster", tags=["roster-edit"])
|
||||||
|
|
||||||
|
|
||||||
|
def record_history(db: Session, unit_id: str, change_type: str, field_name: str = None,
|
||||||
|
old_value: str = None, new_value: str = None, source: str = "manual", notes: str = None):
|
||||||
|
"""Helper function to record a change in unit history"""
|
||||||
|
history_entry = UnitHistory(
|
||||||
|
unit_id=unit_id,
|
||||||
|
change_type=change_type,
|
||||||
|
field_name=field_name,
|
||||||
|
old_value=old_value,
|
||||||
|
new_value=new_value,
|
||||||
|
changed_at=datetime.utcnow(),
|
||||||
|
source=source,
|
||||||
|
notes=notes
|
||||||
|
)
|
||||||
|
db.add(history_entry)
|
||||||
|
# Note: caller is responsible for db.commit()
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_roster_unit(db: Session, unit_id: str):
|
def get_or_create_roster_unit(db: Session, unit_id: str):
|
||||||
unit = db.query(RosterUnit).filter(RosterUnit.id == unit_id).first()
|
unit = db.query(RosterUnit).filter(RosterUnit.id == unit_id).first()
|
||||||
if not unit:
|
if not unit:
|
||||||
@@ -154,6 +171,11 @@ def edit_roster_unit(
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="Invalid next_calibration_due date format. Use YYYY-MM-DD")
|
raise HTTPException(status_code=400, detail="Invalid next_calibration_due date format. Use YYYY-MM-DD")
|
||||||
|
|
||||||
|
# Track changes for history
|
||||||
|
old_note = unit.note
|
||||||
|
old_deployed = unit.deployed
|
||||||
|
old_retired = unit.retired
|
||||||
|
|
||||||
# Update all fields
|
# Update all fields
|
||||||
unit.device_type = device_type
|
unit.device_type = device_type
|
||||||
unit.unit_type = unit_type
|
unit.unit_type = unit_type
|
||||||
@@ -176,6 +198,20 @@ def edit_roster_unit(
|
|||||||
unit.phone_number = phone_number if phone_number else None
|
unit.phone_number = phone_number if phone_number else None
|
||||||
unit.hardware_model = hardware_model if hardware_model else None
|
unit.hardware_model = hardware_model if hardware_model else None
|
||||||
|
|
||||||
|
# Record history entries for changed fields
|
||||||
|
if old_note != note:
|
||||||
|
record_history(db, unit_id, "note_change", "note", old_note, note, "manual")
|
||||||
|
|
||||||
|
if old_deployed != deployed:
|
||||||
|
status_text = "deployed" if deployed else "benched"
|
||||||
|
old_status_text = "deployed" if old_deployed else "benched"
|
||||||
|
record_history(db, unit_id, "deployed_change", "deployed", old_status_text, status_text, "manual")
|
||||||
|
|
||||||
|
if old_retired != retired:
|
||||||
|
status_text = "retired" if retired else "active"
|
||||||
|
old_status_text = "retired" if old_retired else "active"
|
||||||
|
record_history(db, unit_id, "retired_change", "retired", old_status_text, status_text, "manual")
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Unit updated", "id": unit_id, "device_type": device_type}
|
return {"message": "Unit updated", "id": unit_id, "device_type": device_type}
|
||||||
|
|
||||||
@@ -183,8 +219,24 @@ def edit_roster_unit(
|
|||||||
@router.post("/set-deployed/{unit_id}")
|
@router.post("/set-deployed/{unit_id}")
|
||||||
def set_deployed(unit_id: str, deployed: bool = Form(...), db: Session = Depends(get_db)):
|
def set_deployed(unit_id: str, deployed: bool = Form(...), db: Session = Depends(get_db)):
|
||||||
unit = get_or_create_roster_unit(db, unit_id)
|
unit = get_or_create_roster_unit(db, unit_id)
|
||||||
|
old_deployed = unit.deployed
|
||||||
unit.deployed = deployed
|
unit.deployed = deployed
|
||||||
unit.last_updated = datetime.utcnow()
|
unit.last_updated = datetime.utcnow()
|
||||||
|
|
||||||
|
# Record history entry for deployed status change
|
||||||
|
if old_deployed != deployed:
|
||||||
|
status_text = "deployed" if deployed else "benched"
|
||||||
|
old_status_text = "deployed" if old_deployed else "benched"
|
||||||
|
record_history(
|
||||||
|
db=db,
|
||||||
|
unit_id=unit_id,
|
||||||
|
change_type="deployed_change",
|
||||||
|
field_name="deployed",
|
||||||
|
old_value=old_status_text,
|
||||||
|
new_value=status_text,
|
||||||
|
source="manual"
|
||||||
|
)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Updated", "id": unit_id, "deployed": deployed}
|
return {"message": "Updated", "id": unit_id, "deployed": deployed}
|
||||||
|
|
||||||
@@ -192,8 +244,24 @@ def set_deployed(unit_id: str, deployed: bool = Form(...), db: Session = Depends
|
|||||||
@router.post("/set-retired/{unit_id}")
|
@router.post("/set-retired/{unit_id}")
|
||||||
def set_retired(unit_id: str, retired: bool = Form(...), db: Session = Depends(get_db)):
|
def set_retired(unit_id: str, retired: bool = Form(...), db: Session = Depends(get_db)):
|
||||||
unit = get_or_create_roster_unit(db, unit_id)
|
unit = get_or_create_roster_unit(db, unit_id)
|
||||||
|
old_retired = unit.retired
|
||||||
unit.retired = retired
|
unit.retired = retired
|
||||||
unit.last_updated = datetime.utcnow()
|
unit.last_updated = datetime.utcnow()
|
||||||
|
|
||||||
|
# Record history entry for retired status change
|
||||||
|
if old_retired != retired:
|
||||||
|
status_text = "retired" if retired else "active"
|
||||||
|
old_status_text = "retired" if old_retired else "active"
|
||||||
|
record_history(
|
||||||
|
db=db,
|
||||||
|
unit_id=unit_id,
|
||||||
|
change_type="retired_change",
|
||||||
|
field_name="retired",
|
||||||
|
old_value=old_status_text,
|
||||||
|
new_value=status_text,
|
||||||
|
source="manual"
|
||||||
|
)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Updated", "id": unit_id, "retired": retired}
|
return {"message": "Updated", "id": unit_id, "retired": retired}
|
||||||
|
|
||||||
@@ -235,8 +303,22 @@ def delete_roster_unit(unit_id: str, db: Session = Depends(get_db)):
|
|||||||
@router.post("/set-note/{unit_id}")
|
@router.post("/set-note/{unit_id}")
|
||||||
def set_note(unit_id: str, note: str = Form(""), db: Session = Depends(get_db)):
|
def set_note(unit_id: str, note: str = Form(""), db: Session = Depends(get_db)):
|
||||||
unit = get_or_create_roster_unit(db, unit_id)
|
unit = get_or_create_roster_unit(db, unit_id)
|
||||||
|
old_note = unit.note
|
||||||
unit.note = note
|
unit.note = note
|
||||||
unit.last_updated = datetime.utcnow()
|
unit.last_updated = datetime.utcnow()
|
||||||
|
|
||||||
|
# Record history entry for note change
|
||||||
|
if old_note != note:
|
||||||
|
record_history(
|
||||||
|
db=db,
|
||||||
|
unit_id=unit_id,
|
||||||
|
change_type="note_change",
|
||||||
|
field_name="note",
|
||||||
|
old_value=old_note,
|
||||||
|
new_value=note,
|
||||||
|
source="manual"
|
||||||
|
)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "Updated", "id": unit_id, "note": note}
|
return {"message": "Updated", "id": unit_id, "note": note}
|
||||||
|
|
||||||
@@ -402,3 +484,46 @@ def list_ignored_units(db: Session = Depends(get_db)):
|
|||||||
for unit in ignored_units
|
for unit in ignored_units
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/history/{unit_id}")
|
||||||
|
def get_unit_history(unit_id: str, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Get complete history timeline for a unit.
|
||||||
|
Returns all historical changes ordered by most recent first.
|
||||||
|
"""
|
||||||
|
history_entries = db.query(UnitHistory).filter(
|
||||||
|
UnitHistory.unit_id == unit_id
|
||||||
|
).order_by(UnitHistory.changed_at.desc()).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"unit_id": unit_id,
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"id": entry.id,
|
||||||
|
"change_type": entry.change_type,
|
||||||
|
"field_name": entry.field_name,
|
||||||
|
"old_value": entry.old_value,
|
||||||
|
"new_value": entry.new_value,
|
||||||
|
"changed_at": entry.changed_at.isoformat(),
|
||||||
|
"source": entry.source,
|
||||||
|
"notes": entry.notes
|
||||||
|
}
|
||||||
|
for entry in history_entries
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/history/{history_id}")
|
||||||
|
def delete_history_entry(history_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Delete a specific history entry by ID.
|
||||||
|
Allows manual cleanup of old history entries.
|
||||||
|
"""
|
||||||
|
history_entry = db.query(UnitHistory).filter(UnitHistory.id == history_id).first()
|
||||||
|
if not history_entry:
|
||||||
|
raise HTTPException(status_code=404, detail="History entry not found")
|
||||||
|
|
||||||
|
db.delete(history_entry)
|
||||||
|
db.commit()
|
||||||
|
return {"message": "History entry deleted", "id": history_id}
|
||||||
|
|||||||
@@ -178,6 +178,14 @@
|
|||||||
<p id="viewNote" class="mt-1 text-gray-900 dark:text-white whitespace-pre-wrap">--</p>
|
<p id="viewNote" class="mt-1 text-gray-900 dark:text-white whitespace-pre-wrap">--</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Unit History Timeline -->
|
||||||
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Timeline</h3>
|
||||||
|
<div id="historyTimeline" class="space-y-3">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">Loading history...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Photos -->
|
<!-- Photos -->
|
||||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||||
<div class="flex justify-between items-start mb-4">
|
<div class="flex justify-between items-start mb-4">
|
||||||
@@ -762,9 +770,130 @@ async function uploadPhoto(file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load and display unit history timeline
|
||||||
|
async function loadUnitHistory() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/roster/history/${unitId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load history');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const timeline = document.getElementById('historyTimeline');
|
||||||
|
|
||||||
|
if (data.history && data.history.length > 0) {
|
||||||
|
timeline.innerHTML = '';
|
||||||
|
data.history.forEach(entry => {
|
||||||
|
const timelineEntry = createTimelineEntry(entry);
|
||||||
|
timeline.appendChild(timelineEntry);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
timeline.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400">No history yet. Changes will appear here.</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading history:', error);
|
||||||
|
document.getElementById('historyTimeline').innerHTML = '<p class="text-sm text-red-500">Failed to load history</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a timeline entry element
|
||||||
|
function createTimelineEntry(entry) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex gap-3 p-3 rounded-lg bg-gray-50 dark:bg-slate-700/50';
|
||||||
|
|
||||||
|
// Icon based on change type
|
||||||
|
const icons = {
|
||||||
|
'note_change': `<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||||
|
</svg>`,
|
||||||
|
'deployed_change': `<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>`,
|
||||||
|
'retired_change': `<svg class="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>`
|
||||||
|
};
|
||||||
|
|
||||||
|
const icon = icons[entry.change_type] || `<svg class="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
// Format change description
|
||||||
|
let description = '';
|
||||||
|
if (entry.change_type === 'note_change') {
|
||||||
|
description = `<strong>Note changed</strong>`;
|
||||||
|
if (entry.old_value) {
|
||||||
|
description += `<br><span class="text-xs text-gray-500 dark:text-gray-400">From: "${entry.old_value}"</span>`;
|
||||||
|
}
|
||||||
|
if (entry.new_value) {
|
||||||
|
description += `<br><span class="text-xs text-gray-600 dark:text-gray-300">To: "${entry.new_value}"</span>`;
|
||||||
|
}
|
||||||
|
} else if (entry.change_type === 'deployed_change') {
|
||||||
|
description = `<strong>Status changed to ${entry.new_value}</strong>`;
|
||||||
|
} else if (entry.change_type === 'retired_change') {
|
||||||
|
description = `<strong>Marked as ${entry.new_value}</strong>`;
|
||||||
|
} else {
|
||||||
|
description = `<strong>${entry.field_name} changed</strong>`;
|
||||||
|
if (entry.old_value && entry.new_value) {
|
||||||
|
description += `<br><span class="text-xs text-gray-500 dark:text-gray-400">${entry.old_value} → ${entry.new_value}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format timestamp
|
||||||
|
const timestamp = new Date(entry.changed_at).toLocaleString();
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
${icon}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-sm text-gray-900 dark:text-white">
|
||||||
|
${description}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
${timestamp}
|
||||||
|
${entry.source !== 'manual' ? `<span class="ml-2 px-2 py-0.5 bg-gray-200 dark:bg-gray-600 rounded text-xs">${entry.source}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<button onclick="deleteHistoryEntry(${entry.id})" class="text-gray-400 hover:text-red-500 transition-colors" title="Delete this history entry">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a history entry
|
||||||
|
async function deleteHistoryEntry(historyId) {
|
||||||
|
if (!confirm('Are you sure you want to delete this history entry?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/roster/history/${historyId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Reload history
|
||||||
|
await loadUnitHistory();
|
||||||
|
} else {
|
||||||
|
const result = await response.json();
|
||||||
|
alert(`Error: ${result.detail || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load data when page loads
|
// Load data when page loads
|
||||||
loadUnitData().then(() => {
|
loadUnitData().then(() => {
|
||||||
loadPhotos();
|
loadPhotos();
|
||||||
|
loadUnitHistory();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user