394 lines
18 KiB
HTML
394 lines
18 KiB
HTML
|
|
{% extends "shared/frontend/base.html" %}
|
||
|
|
|
||
|
|
{% block title %}Wizard: Lokationer - BMC Hub{% endblock %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="container-fluid px-4 py-4">
|
||
|
|
<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 active">Wizard</li>
|
||
|
|
</ol>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<div class="row mb-4">
|
||
|
|
<div class="col-12">
|
||
|
|
<h1 class="h2 fw-700 mb-2">Wizard: Opret lokation</h1>
|
||
|
|
<p class="text-muted small">Opret en adresse med etager og rum i en samlet arbejdsgang</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<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 id="wizardForm">
|
||
|
|
<div class="card border-0 mb-4">
|
||
|
|
<div class="card-body p-4">
|
||
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||
|
|
<h2 class="h5 fw-600 mb-0">Trin 1: Lokation</h2>
|
||
|
|
<span class="badge bg-primary">Adresse</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-lg-6 mb-3">
|
||
|
|
<label for="rootName" class="form-label">Navn *</label>
|
||
|
|
<input type="text" class="form-control" id="rootName" name="root_name" required maxlength="255" placeholder="f.eks. Hovedkontor">
|
||
|
|
</div>
|
||
|
|
<div class="col-lg-6 mb-3">
|
||
|
|
<label for="rootType" class="form-label">Type *</label>
|
||
|
|
<select class="form-select" id="rootType" name="root_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 %}
|
||
|
|
{% if option_value not in ['rum', 'kantine', 'moedelokale'] %}
|
||
|
|
<option value="{{ option_value }}">
|
||
|
|
{% if option_value == 'kompleks' %}Kompleks{% elif option_value == 'bygning' %}Bygning{% elif option_value == 'etage' %}Etage{% elif option_value == 'customer_site' %}Kundesite{% elif option_value == 'vehicle' %}Køretøj{% else %}{{ option_label }}{% endif %}
|
||
|
|
</option>
|
||
|
|
{% endif %}
|
||
|
|
{% endfor %}
|
||
|
|
{% endif %}
|
||
|
|
</select>
|
||
|
|
<div class="form-text">Tip: Vælg "Bygning" for klassisk etage/rum-setup.</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-lg-6 mb-3">
|
||
|
|
<label for="parentLocation" class="form-label">Overordnet lokation</label>
|
||
|
|
<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 }}">
|
||
|
|
{{ parent.name }}{% if parent.location_type %} ({{ parent.location_type }}){% endif %}
|
||
|
|
</option>
|
||
|
|
{% endfor %}
|
||
|
|
{% endif %}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="col-lg-6 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 }}">{{ customer.name }}</option>
|
||
|
|
{% endfor %}
|
||
|
|
{% endif %}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-lg-6 mb-3">
|
||
|
|
<label for="addressStreet" class="form-label">Vejnavn og nummer</label>
|
||
|
|
<input type="text" class="form-control" id="addressStreet" name="address_street" placeholder="f.eks. Hovedgaden 123">
|
||
|
|
</div>
|
||
|
|
<div class="col-lg-3 mb-3">
|
||
|
|
<label for="addressCity" class="form-label">By</label>
|
||
|
|
<input type="text" class="form-control" id="addressCity" name="address_city" placeholder="f.eks. København">
|
||
|
|
</div>
|
||
|
|
<div class="col-lg-3 mb-3">
|
||
|
|
<label for="addressPostal" class="form-label">Postnummer</label>
|
||
|
|
<input type="text" class="form-control" id="addressPostal" name="address_postal_code" placeholder="f.eks. 1000">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-lg-3 mb-3">
|
||
|
|
<label for="addressCountry" class="form-label">Land</label>
|
||
|
|
<input type="text" class="form-control" id="addressCountry" name="address_country" value="DK" placeholder="DK">
|
||
|
|
</div>
|
||
|
|
<div class="col-lg-3 mb-3">
|
||
|
|
<label for="phone" class="form-label">Telefon</label>
|
||
|
|
<input type="tel" class="form-control" id="phone" name="phone" placeholder="+45 12 34 56 78">
|
||
|
|
</div>
|
||
|
|
<div class="col-lg-6 mb-3">
|
||
|
|
<label for="email" class="form-label">Email</label>
|
||
|
|
<input type="email" class="form-control" id="email" name="email" placeholder="kontakt@lokation.dk">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-check">
|
||
|
|
<input class="form-check-input" type="checkbox" id="rootActive" name="root_active" checked>
|
||
|
|
<label class="form-check-label" for="rootActive">Lokation er aktiv</label>
|
||
|
|
</div>
|
||
|
|
<div class="form-check mt-2">
|
||
|
|
<input class="form-check-input" type="checkbox" id="autoPrefix" checked>
|
||
|
|
<label class="form-check-label" for="autoPrefix">Prefiks etager/rum med lokationsnavn</label>
|
||
|
|
<div class="form-text">Hjælper mod navnekonflikter (navne skal være unikke globalt).</div>
|
||
|
|
</div>
|
||
|
|
<div class="form-check mt-2">
|
||
|
|
<input class="form-check-input" type="checkbox" id="autoSuffix" checked>
|
||
|
|
<label class="form-check-label" for="autoSuffix">Tilføj automatisk suffix ved dubletter</label>
|
||
|
|
<div class="form-text">Eksempel: "Stue" bliver til "Stue (2)" hvis navnet findes.</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card border-0 mb-4">
|
||
|
|
<div class="card-body p-4">
|
||
|
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||
|
|
<h2 class="h5 fw-600 mb-0">Trin 2: Etager</h2>
|
||
|
|
<button type="button" class="btn btn-outline-primary btn-sm" id="addFloorBtn">
|
||
|
|
<i class="bi bi-plus-lg me-2"></i>Tilføj etage
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div id="floorsContainer" class="d-flex flex-column gap-3"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="d-flex justify-content-between gap-2">
|
||
|
|
<a href="{{ cancel_url }}" 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>Opret lokation
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
{% block scripts %}
|
||
|
|
<script>
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
const floorsContainer = document.getElementById('floorsContainer');
|
||
|
|
const addFloorBtn = document.getElementById('addFloorBtn');
|
||
|
|
const form = document.getElementById('wizardForm');
|
||
|
|
const submitBtn = document.getElementById('submitBtn');
|
||
|
|
const errorAlert = document.getElementById('errorAlert');
|
||
|
|
const errorMessage = document.getElementById('errorMessage');
|
||
|
|
|
||
|
|
let floorIndex = 0;
|
||
|
|
|
||
|
|
function createRoomRow(roomIndex) {
|
||
|
|
const roomRow = document.createElement('div');
|
||
|
|
roomRow.className = 'row g-2 align-items-center room-row';
|
||
|
|
roomRow.innerHTML = `
|
||
|
|
<div class="col-md-6">
|
||
|
|
<input type="text" class="form-control form-control-sm room-name" placeholder="Rum ${roomIndex + 1}" required>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-4">
|
||
|
|
<select class="form-select form-select-sm room-type">
|
||
|
|
<option value="rum">Rum</option>
|
||
|
|
<option value="kantine">Kantine</option>
|
||
|
|
<option value="moedelokale">Mødelokale</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-2 text-end">
|
||
|
|
<button type="button" class="btn btn-outline-danger btn-sm remove-room">
|
||
|
|
<i class="bi bi-x-lg"></i>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
return roomRow;
|
||
|
|
}
|
||
|
|
|
||
|
|
function addRoom(floorCard) {
|
||
|
|
const roomsContainer = floorCard.querySelector('.rooms-container');
|
||
|
|
const roomIndex = roomsContainer.querySelectorAll('.room-row').length;
|
||
|
|
const roomRow = createRoomRow(roomIndex);
|
||
|
|
roomsContainer.appendChild(roomRow);
|
||
|
|
|
||
|
|
roomRow.querySelector('.remove-room').addEventListener('click', function() {
|
||
|
|
roomRow.remove();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function addFloor() {
|
||
|
|
const floorCard = document.createElement('div');
|
||
|
|
floorCard.className = 'border rounded-3 p-3 bg-light';
|
||
|
|
floorCard.dataset.floorIndex = String(floorIndex);
|
||
|
|
floorCard.innerHTML = `
|
||
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
|
|
<div class="fw-600">Etage</div>
|
||
|
|
<button type="button" class="btn btn-outline-danger btn-sm remove-floor">
|
||
|
|
<i class="bi bi-trash"></i>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div class="row g-2 align-items-center mb-3">
|
||
|
|
<div class="col-md-8">
|
||
|
|
<input type="text" class="form-control floor-name" placeholder="Etage ${floorIndex + 1}" required>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-4">
|
||
|
|
<div class="form-check">
|
||
|
|
<input class="form-check-input floor-active" type="checkbox" checked>
|
||
|
|
<label class="form-check-label">Aktiv</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||
|
|
<div class="text-muted small">Rum</div>
|
||
|
|
<button type="button" class="btn btn-outline-primary btn-sm add-room">
|
||
|
|
<i class="bi bi-plus-lg me-2"></i>Tilføj rum
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div class="rooms-container d-flex flex-column gap-2"></div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
floorCard.querySelector('.remove-floor').addEventListener('click', function() {
|
||
|
|
floorCard.remove();
|
||
|
|
});
|
||
|
|
|
||
|
|
floorCard.querySelector('.add-room').addEventListener('click', function() {
|
||
|
|
addRoom(floorCard);
|
||
|
|
});
|
||
|
|
|
||
|
|
floorsContainer.appendChild(floorCard);
|
||
|
|
addRoom(floorCard);
|
||
|
|
floorIndex += 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
addFloorBtn.addEventListener('click', addFloor);
|
||
|
|
addFloor();
|
||
|
|
|
||
|
|
form.addEventListener('submit', async function(event) {
|
||
|
|
event.preventDefault();
|
||
|
|
errorAlert.classList.add('hide');
|
||
|
|
|
||
|
|
const rootName = document.getElementById('rootName').value.trim();
|
||
|
|
const rootType = document.getElementById('rootType').value;
|
||
|
|
const autoPrefix = document.getElementById('autoPrefix').checked;
|
||
|
|
const autoSuffix = document.getElementById('autoSuffix').checked;
|
||
|
|
|
||
|
|
if (!rootName || !rootType) {
|
||
|
|
errorMessage.textContent = 'Udfyld navn og type for lokationen.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const floorCards = Array.from(document.querySelectorAll('[data-floor-index]'));
|
||
|
|
if (floorCards.length === 0) {
|
||
|
|
errorMessage.textContent = 'Tilføj mindst én etage.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const floorsPayload = [];
|
||
|
|
const nameRegistry = new Set();
|
||
|
|
|
||
|
|
function registerName(name) {
|
||
|
|
const normalized = name.trim().toLowerCase();
|
||
|
|
if (!normalized) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (nameRegistry.has(normalized)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
nameRegistry.add(normalized);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!autoSuffix && !registerName(rootName)) {
|
||
|
|
errorMessage.textContent = 'Der er dublerede navne i lokationen.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const floorCard of floorCards) {
|
||
|
|
const floorNameInput = floorCard.querySelector('.floor-name').value.trim();
|
||
|
|
if (!floorNameInput) {
|
||
|
|
errorMessage.textContent = 'Alle etager skal have et navn.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const floorName = autoPrefix ? `${rootName} - ${floorNameInput}` : floorNameInput;
|
||
|
|
if (!autoSuffix && !registerName(floorName)) {
|
||
|
|
errorMessage.textContent = 'Der er dublerede etagenavne. Skift navne eller brug prefiks.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const roomsContainer = floorCard.querySelector('.rooms-container');
|
||
|
|
const roomRows = Array.from(roomsContainer.querySelectorAll('.room-row'));
|
||
|
|
if (roomRows.length === 0) {
|
||
|
|
errorMessage.textContent = 'Tilføj mindst ét rum til hver etage.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const roomsPayload = [];
|
||
|
|
for (const roomRow of roomRows) {
|
||
|
|
const roomNameInput = roomRow.querySelector('.room-name').value.trim();
|
||
|
|
if (!roomNameInput) {
|
||
|
|
errorMessage.textContent = 'Alle rum skal have et navn.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const roomName = autoPrefix ? `${rootName} - ${floorNameInput} - ${roomNameInput}` : roomNameInput;
|
||
|
|
if (!autoSuffix && !registerName(roomName)) {
|
||
|
|
errorMessage.textContent = 'Der er dublerede rumnavne. Skift navne eller brug prefiks.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
roomsPayload.push({
|
||
|
|
name: roomName,
|
||
|
|
location_type: roomRow.querySelector('.room-type').value,
|
||
|
|
is_active: true
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
floorsPayload.push({
|
||
|
|
name: floorName,
|
||
|
|
location_type: 'etage',
|
||
|
|
is_active: floorCard.querySelector('.floor-active').checked,
|
||
|
|
rooms: roomsPayload
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const payload = {
|
||
|
|
root: {
|
||
|
|
name: rootName,
|
||
|
|
location_type: rootType,
|
||
|
|
parent_location_id: document.getElementById('parentLocation').value || null,
|
||
|
|
customer_id: document.getElementById('customerId').value || null,
|
||
|
|
address_street: document.getElementById('addressStreet').value || null,
|
||
|
|
address_city: document.getElementById('addressCity').value || null,
|
||
|
|
address_postal_code: document.getElementById('addressPostal').value || null,
|
||
|
|
address_country: document.getElementById('addressCountry').value || 'DK',
|
||
|
|
phone: document.getElementById('phone').value || null,
|
||
|
|
email: document.getElementById('email').value || null,
|
||
|
|
notes: null,
|
||
|
|
is_active: document.getElementById('rootActive').checked
|
||
|
|
},
|
||
|
|
floors: floorsPayload,
|
||
|
|
auto_suffix: autoSuffix
|
||
|
|
};
|
||
|
|
|
||
|
|
submitBtn.disabled = true;
|
||
|
|
submitBtn.innerHTML = '<i class="bi bi-hourglass-split me-2"></i>Opretter...';
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/v1/locations/bulk-create', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(payload)
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const result = await response.json();
|
||
|
|
window.location.href = `/app/locations/${result.root_id}`;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const errorData = await response.json();
|
||
|
|
errorMessage.textContent = errorData.detail || 'Fejl ved oprettelse af lokationer.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error:', error);
|
||
|
|
errorMessage.textContent = 'En fejl opstod. Prøv igen senere.';
|
||
|
|
errorAlert.classList.remove('hide');
|
||
|
|
} finally {
|
||
|
|
submitBtn.disabled = false;
|
||
|
|
submitBtn.innerHTML = '<i class="bi bi-check-lg me-2"></i>Opret lokation';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|