440 lines
22 KiB
HTML
440 lines
22 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
||
|
||
{% block title %}Rediger {{ location.name }} - BMC Hub{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.relation-card {
|
||
border: 1px solid rgba(15, 76, 117, 0.1);
|
||
border-radius: 1rem;
|
||
background: linear-gradient(180deg, #ffffff 0%, #f8fbfd 100%);
|
||
padding: 1rem;
|
||
}
|
||
|
||
.relation-summary {
|
||
border: 1px dashed rgba(15, 76, 117, 0.22);
|
||
border-radius: 0.85rem;
|
||
background: rgba(15, 76, 117, 0.05);
|
||
padding: 0.85rem 1rem;
|
||
}
|
||
|
||
.relation-summary.empty {
|
||
background: #f8fafc;
|
||
border-style: solid;
|
||
border-color: rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
.relation-path {
|
||
font-weight: 600;
|
||
color: #1f3b53;
|
||
}
|
||
|
||
.relation-meta {
|
||
font-size: 0.8rem;
|
||
color: var(--text-secondary);
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container-fluid px-4 py-4">
|
||
<!-- Breadcrumb -->
|
||
<nav aria-label="breadcrumb" class="mb-4">
|
||
<ol class="breadcrumb">
|
||
<li class="breadcrumb-item"><a href="/" class="text-decoration-none">Hjem</a></li>
|
||
<li class="breadcrumb-item"><a href="/app/locations" class="text-decoration-none">Lokaliteter</a></li>
|
||
<li class="breadcrumb-item"><a href="/app/locations/{{ location.id }}" class="text-decoration-none">{{ location.name }}</a></li>
|
||
<li class="breadcrumb-item active">Rediger</li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<!-- Header -->
|
||
<div class="row mb-4">
|
||
<div class="col-12">
|
||
<h1 class="h2 fw-700 mb-2">Rediger lokation</h1>
|
||
<p class="text-muted small">{{ location.name }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Error Alert -->
|
||
<div id="errorAlert" class="alert alert-danger alert-dismissible fade hide" role="alert">
|
||
<strong>Fejl!</strong> <span id="errorMessage"></span>
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Luk"></button>
|
||
</div>
|
||
|
||
<!-- Form Card -->
|
||
<div class="card border-0 mb-4">
|
||
<div class="card-body p-5">
|
||
<form id="locationForm" method="POST" action="{{ form_action | default('/app/locations/' ~ location.id ~ '/edit') }}" data-no-intercept="true">
|
||
<!-- Section 1: Basic Information -->
|
||
<fieldset class="mb-5">
|
||
<legend class="h5 fw-600 mb-3">Grundlæggende oplysninger</legend>
|
||
|
||
<div class="mb-3">
|
||
<label for="name" class="form-label">Navn *</label>
|
||
<input type="text" class="form-control" id="name" name="name" required maxlength="255" value="{{ location.name }}" placeholder="f.eks. Hovedkontor, Lager Nord">
|
||
<small class="form-text text-muted">Lokationens navn eller betegnelse</small>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="locationType" class="form-label">Type *</label>
|
||
<select class="form-select" id="locationType" name="location_type" required>
|
||
<option value="">Vælg type</option>
|
||
{% if location_types %}
|
||
{% for type_option in location_types %}
|
||
{% set option_value = type_option['value'] if type_option is mapping else type_option %}
|
||
{% set option_label = type_option['label'] if type_option is mapping else type_option %}
|
||
<option value="{{ option_value }}" {% if location.location_type == option_value %}selected{% endif %}>
|
||
{% if option_value == 'kompleks' %}Kompleks{% elif option_value == 'bygning' %}Bygning{% elif option_value == 'etage' %}Etage{% elif option_value == 'customer_site' %}Kundesite{% elif option_value == 'rum' %}Rum{% elif option_value == 'kantine' %}Kantine{% elif option_value == 'moedelokale' %}Mødelokale{% elif option_value == 'vehicle' %}Køretøj{% else %}{{ option_label }}{% endif %}
|
||
</option>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="relation-card mb-3">
|
||
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-3">
|
||
<div>
|
||
<label for="parentLocation" class="form-label mb-1">Placering i hierarki</label>
|
||
<div class="text-muted small">Flyt lokationen ved at vælge en ny overordnet lokation.</div>
|
||
</div>
|
||
{% if selected_parent %}
|
||
<a href="/app/locations/{{ selected_parent.id }}" class="btn btn-outline-secondary btn-sm">
|
||
<i class="bi bi-eye me-2"></i>Åbn valgt parent
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="parentLocationSearch" class="form-label small text-muted">Søg overordnet lokation</label>
|
||
<input type="text" class="form-control" id="parentLocationSearch" placeholder="Søg efter navn, sti eller type...">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<select class="form-select" id="parentLocation" name="parent_location_id">
|
||
<option value="">Ingen (øverste niveau)</option>
|
||
{% if parent_locations %}
|
||
{% for parent in parent_locations %}
|
||
<option
|
||
value="{{ parent.id }}"
|
||
data-path="{{ parent.hierarchy_path }}"
|
||
data-type="{{ parent.type_label }}"
|
||
data-customer-id="{{ parent.customer_id | default('') }}"
|
||
{% if location.parent_location_id == parent.id %}selected{% endif %}>
|
||
{{ parent.display_name }}
|
||
</option>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</select>
|
||
<div class="form-text">Listen viser hele stien, så du ikke skal gætte, hvor lokationen lander.</div>
|
||
</div>
|
||
|
||
<div id="parentSummary" class="relation-summary{% if not selected_parent %} empty{% endif %}">
|
||
{% if selected_parent %}
|
||
<div class="small text-muted mb-1">Nuværende overordnet lokation</div>
|
||
<div class="relation-path">{{ selected_parent.hierarchy_path }}</div>
|
||
<div class="relation-meta">{{ selected_parent.type_label }}</div>
|
||
{% else %}
|
||
<div class="small text-muted">Lokationen ligger i topniveau.</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="customerId" class="form-label">Kunde (valgfri)</label>
|
||
<select class="form-select" id="customerId" name="customer_id">
|
||
<option value="">Ingen</option>
|
||
{% if customers %}
|
||
{% for customer in customers %}
|
||
<option value="{{ customer.id }}" {% if location.customer_id == customer.id %}selected{% endif %}>{{ customer.name }}</option>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</select>
|
||
<div class="form-text">Valgfri – kan knyttes til alle typer.</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="isActive" name="is_active" {% if location.is_active %}checked{% endif %}>
|
||
<label class="form-check-label" for="isActive">Lokation er aktiv</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3" id="crossFieldOption" {% if location.location_type != 'rum' %}hidden{% endif %}>
|
||
<div class="form-check">
|
||
<input class="form-check-input" type="checkbox" id="hasCrossField" name="has_cross_field" {% if location.has_cross_field %}checked{% endif %}>
|
||
<label class="form-check-label" for="hasCrossField">Rummet indeholder krydsfelt</label>
|
||
</div>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<!-- Section 2: Address -->
|
||
<fieldset class="mb-5">
|
||
<legend class="h5 fw-600 mb-3">Adresse</legend>
|
||
|
||
<div class="mb-3">
|
||
<label for="addressStreet" class="form-label">Vejnavn og nummer</label>
|
||
<input type="text" class="form-control" id="addressStreet" name="address_street" value="{{ location.address_street | default('') }}" placeholder="f.eks. Hovedgaden 123">
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label for="addressCity" class="form-label">By</label>
|
||
<input type="text" class="form-control" id="addressCity" name="address_city" value="{{ location.address_city | default('') }}" placeholder="f.eks. København">
|
||
</div>
|
||
<div class="col-md-3 mb-3">
|
||
<label for="addressPostal" class="form-label">Postnummer</label>
|
||
<input type="text" class="form-control" id="addressPostal" name="address_postal_code" value="{{ location.address_postal_code | default('') }}" placeholder="f.eks. 1000">
|
||
</div>
|
||
<div class="col-md-3 mb-3">
|
||
<label for="addressCountry" class="form-label">Land</label>
|
||
<input type="text" class="form-control" id="addressCountry" name="address_country" value="{{ location.address_country | default('DK') }}" placeholder="DK">
|
||
</div>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<!-- Section 3: Contact Information -->
|
||
<fieldset class="mb-5">
|
||
<legend class="h5 fw-600 mb-3">Kontaktoplysninger</legend>
|
||
|
||
<div class="mb-3">
|
||
<label for="phone" class="form-label">Telefon</label>
|
||
<input type="tel" class="form-control" id="phone" name="phone" value="{{ location.phone | default('') }}" placeholder="f.eks. +45 12 34 56 78">
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="email" class="form-label">Email</label>
|
||
<input type="email" class="form-control" id="email" name="email" value="{{ location.email or '' }}" placeholder="f.eks. kontakt@lokation.dk">
|
||
</div>
|
||
</fieldset>
|
||
|
||
<!-- Section 4: Coordinates (Advanced) -->
|
||
<fieldset class="mb-5">
|
||
<legend class="h5 fw-600 mb-3">Koordinater (GPS) <span class="badge bg-secondary">Valgfrit</span></legend>
|
||
<p class="text-muted small">Bruges til kortintegration og lokalisering</p>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label for="latitude" class="form-label">Breddegrad</label>
|
||
<input type="number" class="form-control" id="latitude" name="latitude" step="0.0001" min="-90" max="90" value="{{ location.latitude | default('') }}" placeholder="f.eks. 55.6761">
|
||
<small class="form-text text-muted">-90 til 90</small>
|
||
</div>
|
||
<div class="col-md-6 mb-3">
|
||
<label for="longitude" class="form-label">Længdegrad</label>
|
||
<input type="number" class="form-control" id="longitude" name="longitude" step="0.0001" min="-180" max="180" value="{{ location.longitude | default('') }}" placeholder="f.eks. 12.5683">
|
||
<small class="form-text text-muted">-180 til 180</small>
|
||
</div>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<!-- Section 5: Notes -->
|
||
<fieldset class="mb-5">
|
||
<legend class="h5 fw-600 mb-3">Noter</legend>
|
||
|
||
<div class="mb-3">
|
||
<label for="notes" class="form-label">Noter og kommentarer</label>
|
||
<textarea class="form-control" id="notes" name="notes" rows="4" maxlength="500" placeholder="Eventuelle noter eller særlige oplysninger om lokationen">{{ location.notes | default('') }}</textarea>
|
||
<small class="form-text text-muted"><span id="charCount">{{ (location.notes or '') | length }}</span> / 500 tegn</small>
|
||
</div>
|
||
</fieldset>
|
||
|
||
<!-- Form Buttons -->
|
||
<div class="d-flex gap-2 justify-content-between align-items-center">
|
||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||
<i class="bi bi-trash me-2"></i>Slet lokation
|
||
</button>
|
||
<div class="d-flex gap-2">
|
||
<a href="/app/locations/{{ location.id }}" class="btn btn-outline-secondary">Annuller</a>
|
||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||
<i class="bi bi-check-lg me-2"></i>Gem ændringer
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Delete Confirmation Modal -->
|
||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header border-bottom-0">
|
||
<h5 class="modal-title">Slet lokation?</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Luk"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p class="mb-0">Er du sikker på, at du vil slette <strong>{{ location.name }}</strong>?</p>
|
||
<p class="text-muted small mt-2">Denne handling kan ikke fortrydes. Lokationen vil blive soft-deleted og kan gendannes af en administrator.</p>
|
||
</div>
|
||
<div class="modal-footer border-top-0">
|
||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Annuller</button>
|
||
<button type="button" class="btn btn-danger btn-sm" id="confirmDeleteBtn">
|
||
<i class="bi bi-trash me-2"></i>Slet
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const form = document.getElementById('locationForm');
|
||
const errorAlert = document.getElementById('errorAlert');
|
||
const submitBtn = document.getElementById('submitBtn');
|
||
const notesField = document.getElementById('notes');
|
||
const charCount = document.getElementById('charCount');
|
||
const deleteModalElement = document.getElementById('deleteModal');
|
||
const deleteModal = (window.bootstrap && deleteModalElement) ? new bootstrap.Modal(deleteModalElement) : null;
|
||
const locationId = '{{ location.id }}';
|
||
const parentLocationSelect = document.getElementById('parentLocation');
|
||
const parentLocationSearch = document.getElementById('parentLocationSearch');
|
||
const parentSummary = document.getElementById('parentSummary');
|
||
|
||
// Character counter for notes
|
||
notesField.addEventListener('input', function() {
|
||
charCount.textContent = this.value.length;
|
||
});
|
||
|
||
function escapeHtml(value) {
|
||
return String(value || '')
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
function updateParentSummary() {
|
||
const selectedOption = parentLocationSelect.options[parentLocationSelect.selectedIndex];
|
||
const path = selectedOption?.dataset?.path || '';
|
||
const type = selectedOption?.dataset?.type || '';
|
||
|
||
if (!selectedOption || !selectedOption.value) {
|
||
parentSummary.classList.add('empty');
|
||
parentSummary.innerHTML = '<div class="small text-muted">Lokationen ligger i topniveau.</div>';
|
||
return;
|
||
}
|
||
|
||
parentSummary.classList.remove('empty');
|
||
parentSummary.innerHTML = `
|
||
<div class="small text-muted mb-1">Valgt overordnet lokation</div>
|
||
<div class="relation-path">${escapeHtml(path)}</div>
|
||
<div class="relation-meta">${escapeHtml(type)}</div>
|
||
`;
|
||
}
|
||
|
||
function filterParentLocations() {
|
||
const query = (parentLocationSearch.value || '').trim().toLowerCase();
|
||
Array.from(parentLocationSelect.options).forEach((option, index) => {
|
||
if (index === 0) {
|
||
option.hidden = false;
|
||
return;
|
||
}
|
||
const haystack = `${option.text} ${option.dataset.path || ''} ${option.dataset.type || ''}`.toLowerCase();
|
||
option.hidden = query ? !haystack.includes(query) : false;
|
||
});
|
||
}
|
||
|
||
if (parentLocationSearch) {
|
||
parentLocationSearch.addEventListener('input', filterParentLocations);
|
||
}
|
||
|
||
if (parentLocationSelect) {
|
||
parentLocationSelect.addEventListener('change', updateParentSummary);
|
||
updateParentSummary();
|
||
}
|
||
|
||
// Form submission
|
||
form.addEventListener('submit', async function(e) {
|
||
if (form.dataset.noIntercept === 'true') {
|
||
return;
|
||
}
|
||
e.preventDefault();
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerHTML = '<i class="bi bi-hourglass-split me-2"></i>Gemmer...';
|
||
|
||
const formData = new FormData(form);
|
||
const data = {
|
||
name: formData.get('name'),
|
||
location_type: formData.get('location_type'),
|
||
parent_location_id: formData.get('parent_location_id') ? parseInt(formData.get('parent_location_id')) : null,
|
||
customer_id: formData.get('customer_id') ? parseInt(formData.get('customer_id')) : null,
|
||
is_active: formData.get('is_active') === 'on',
|
||
has_cross_field: formData.get('has_cross_field') === 'on',
|
||
address_street: formData.get('address_street'),
|
||
address_city: formData.get('address_city'),
|
||
address_postal_code: formData.get('address_postal_code'),
|
||
address_country: formData.get('address_country'),
|
||
phone: formData.get('phone'),
|
||
email: formData.get('email'),
|
||
latitude: formData.get('latitude') ? parseFloat(formData.get('latitude')) : null,
|
||
longitude: formData.get('longitude') ? parseFloat(formData.get('longitude')) : null,
|
||
notes: formData.get('notes')
|
||
};
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/locations/${locationId}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (response.ok) {
|
||
window.location.href = `/app/locations/${locationId}`;
|
||
} else {
|
||
const error = await response.json();
|
||
document.getElementById('errorMessage').textContent = error.detail || 'Fejl ved opdatering af lokation';
|
||
errorAlert.classList.remove('hide');
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = '<i class="bi bi-check-lg me-2"></i>Gem ændringer';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
document.getElementById('errorMessage').textContent = 'En fejl opstod. Prøv igen senere.';
|
||
errorAlert.classList.remove('hide');
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = '<i class="bi bi-check-lg me-2"></i>Gem ændringer';
|
||
}
|
||
});
|
||
|
||
const locationType = document.getElementById('locationType');
|
||
const crossFieldOption = document.getElementById('crossFieldOption');
|
||
const hasCrossField = document.getElementById('hasCrossField');
|
||
const updateCrossFieldOption = () => {
|
||
const isRoom = locationType.value === 'rum';
|
||
crossFieldOption.hidden = !isRoom;
|
||
if (!isRoom) hasCrossField.checked = false;
|
||
};
|
||
locationType.addEventListener('change', updateCrossFieldOption);
|
||
updateCrossFieldOption();
|
||
|
||
// Delete location
|
||
document.getElementById('confirmDeleteBtn').addEventListener('click', function() {
|
||
fetch(`/api/v1/locations/${locationId}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json' }
|
||
})
|
||
.then(response => {
|
||
if (response.ok) {
|
||
if (deleteModal) {
|
||
deleteModal.hide();
|
||
}
|
||
setTimeout(() => window.location.href = '/app/locations', 300);
|
||
} else {
|
||
alert('Fejl ved sletning af lokation');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('Fejl ved sletning af lokation');
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
{% endblock %}
|