- Added views for listing fixed-price agreements, displaying agreement details, and a reporting dashboard. - Created HTML templates for listing, detailing, and reporting on fixed-price agreements. - Introduced API endpoint to fetch active customers for agreement creation. - Added migration scripts for creating necessary database tables and views for fixed-price agreements, billing periods, and reporting. - Implemented triggers for auto-generating agreement numbers and updating timestamps. - Enhanced ticket management with archived ticket views and filtering capabilities.
617 lines
27 KiB
HTML
617 lines
27 KiB
HTML
{% extends "shared/frontend/base.html" %}
|
|
|
|
{% block title %}Fastpris Aftaler - BMC Hub{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<!-- Header -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h1 class="h3 mb-0">📋 Fastpris Aftaler</h1>
|
|
<p class="text-muted">Månedlige timer aftaler med overtid håndtering</p>
|
|
</div>
|
|
<div class="col-auto">
|
|
<a href="/fixed-price-agreements/reports/dashboard" class="btn btn-outline-primary me-2">
|
|
<i class="bi bi-graph-up"></i> Rapporter
|
|
</a>
|
|
<button class="btn btn-primary" onclick="openCreateModal()">
|
|
<i class="bi bi-plus-circle"></i> Opret Ny Aftale
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Row -->
|
|
<div class="row g-3 mb-4" id="statsCards">
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="rounded-circle bg-success bg-opacity-10 p-3">
|
|
<i class="bi bi-calendar-check text-success fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1 ms-3">
|
|
<p class="text-muted small mb-1">Aktive Aftaler</p>
|
|
<h3 class="mb-0" id="activeCount">-</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="rounded-circle bg-primary bg-opacity-10 p-3">
|
|
<i class="bi bi-currency-dollar text-primary fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1 ms-3">
|
|
<p class="text-muted small mb-1">Total Omsætning</p>
|
|
<h3 class="mb-0" id="totalRevenue">-</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="rounded-circle bg-info bg-opacity-10 p-3">
|
|
<i class="bi bi-pie-chart text-info fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1 ms-3">
|
|
<p class="text-muted small mb-1">Total Profit</p>
|
|
<h3 class="mb-0" id="totalProfit">-</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="rounded-circle bg-warning bg-opacity-10 p-3">
|
|
<i class="bi bi-clock text-warning fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1 ms-3">
|
|
<p class="text-muted small mb-1">Brugte Timer</p>
|
|
<h3 class="mb-0" id="totalHours">-</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agreements Table -->
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<div class="row align-items-center">
|
|
<div class="col">
|
|
<h5 class="mb-0">Alle Aftaler</h5>
|
|
</div>
|
|
<div class="col-auto">
|
|
<input type="text" class="form-control form-control-sm" id="searchInput" placeholder="Søg...">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle" id="agreementsTable">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Aftale Nr.</th>
|
|
<th>Kunde</th>
|
|
<th>Månedlige Timer</th>
|
|
<th>Status</th>
|
|
<th>Denne Måned</th>
|
|
<th>Start</th>
|
|
<th class="text-end">Handlinger</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="agreementsBody">
|
|
<tr>
|
|
<td colspan="7" class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Load customers from server-side render
|
|
const customersCache = {{ customers | tojson }};
|
|
console.log('✅ Loaded customers from template:', customersCache?.length || 0);
|
|
|
|
async function loadAgreements() {
|
|
try {
|
|
// Load stats
|
|
const stats = await fetch('/api/v1/fixed-price-agreements/stats/summary').then(r => r.json());
|
|
document.getElementById('activeCount').textContent = stats.active_agreements || 0;
|
|
document.getElementById('totalRevenue').textContent = formatCurrency(stats.total_revenue || 0);
|
|
document.getElementById('totalProfit').textContent = formatCurrency(stats.total_profit || 0);
|
|
document.getElementById('totalHours').textContent = (stats.total_used_hours || 0).toFixed(1) + ' t';
|
|
|
|
// Load agreements
|
|
const agreements = await fetch('/api/v1/fixed-price-agreements?include_current_period=true').then(r => r.json());
|
|
renderAgreements(agreements);
|
|
} catch (e) {
|
|
console.error('Error loading agreements:', e);
|
|
document.getElementById('agreementsBody').innerHTML = `
|
|
<tr><td colspan="7" class="text-center text-danger py-5">
|
|
<i class="bi bi-exclamation-triangle fs-1 mb-3"></i>
|
|
<p>Fejl ved indlæsning</p>
|
|
</td></tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function renderAgreements(agreements) {
|
|
const tbody = document.getElementById('agreementsBody');
|
|
|
|
if (!agreements || agreements.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr><td colspan="7" class="text-center text-muted py-5">
|
|
<i class="bi bi-inbox fs-1 mb-3"></i>
|
|
<p>Ingen aftaler endnu</p>
|
|
</td></tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = agreements.map(a => {
|
|
const statusBadge = getStatusBadge(a.status);
|
|
const currentPeriod = a.current_period;
|
|
const usedHours = currentPeriod ? parseFloat(currentPeriod.used_hours || 0).toFixed(1) : '0.0';
|
|
const remainingHours = a.remaining_hours_this_month ? a.remaining_hours_this_month.toFixed(1) : parseFloat(a.monthly_hours).toFixed(1);
|
|
|
|
return `
|
|
<tr onclick="window.location.href='/fixed-price-agreements/${a.id}'" style="cursor: pointer;">
|
|
<td><strong>${a.agreement_number}</strong></td>
|
|
<td>${a.customer_name || '-'}</td>
|
|
<td>${parseFloat(a.monthly_hours).toFixed(0)} t/md</td>
|
|
<td>${statusBadge}</td>
|
|
<td>
|
|
<small class="text-muted">${usedHours}t brugt / ${remainingHours}t tilbage</small>
|
|
${currentPeriod && currentPeriod.status === 'pending_approval' ? '<span class="badge bg-warning ms-1">⚠️ Overtid</span>' : ''}
|
|
</td>
|
|
<td>${new Date(a.start_date).toLocaleDateString('da-DK')}</td>
|
|
<td class="text-end" onclick="event.stopPropagation();">
|
|
<a href="/fixed-price-agreements/${a.id}" class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-eye"></i> Detaljer
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function getStatusBadge(status) {
|
|
const badges = {
|
|
'active': '<span class="badge bg-success">Aktiv</span>',
|
|
'suspended': '<span class="badge bg-warning">Suspenderet</span>',
|
|
'expired': '<span class="badge bg-danger">Udløbet</span>',
|
|
'cancelled': '<span class="badge bg-secondary">Annulleret</span>',
|
|
'pending_cancellation': '<span class="badge bg-warning">Opsagt</span>'
|
|
};
|
|
return badges[status] || status;
|
|
}
|
|
|
|
function formatCurrency(amount) {
|
|
return new Intl.NumberFormat('da-DK', {
|
|
style: 'currency',
|
|
currency: 'DKK',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
}).format(amount);
|
|
}
|
|
|
|
function calculateHourlyRate() {
|
|
const monthlyAmount = parseFloat(document.getElementById('createMonthlyAmount')?.value || 0);
|
|
const monthlyHours = parseFloat(document.getElementById('createMonthlyHours')?.value || 1);
|
|
|
|
if (monthlyAmount > 0 && monthlyHours > 0) {
|
|
const hourlyRate = (monthlyAmount / monthlyHours).toFixed(2);
|
|
document.getElementById('calculatedHourlyRate').textContent = `(≈ ${hourlyRate} kr/t)`;
|
|
} else {
|
|
document.getElementById('calculatedHourlyRate').textContent = '';
|
|
}
|
|
}
|
|
|
|
async function openCreateModal() {
|
|
console.log('🚀 openCreateModal() called');
|
|
console.log('📊 Customers cache:', customersCache?.length || 0);
|
|
|
|
// Show loading state
|
|
const customerList = document.getElementById('createCustomerList');
|
|
if (!customerList) {
|
|
console.error('❌ createCustomerList element not found!');
|
|
return;
|
|
}
|
|
console.log('✅ Found createCustomerList element');
|
|
|
|
customerList.innerHTML = '<div class="list-group-item text-center"><span class="spinner-border spinner-border-sm me-2"></span>Indlæser kunder...</div>';
|
|
|
|
// Check if customers are loaded
|
|
if (!customersCache || customersCache.length === 0) {
|
|
console.error('❌ No customers available');
|
|
customerList.innerHTML = '<div class="list-group-item text-danger"><i class="bi bi-exclamation-triangle me-2"></i>Ingen kunder tilgængelige</div>';
|
|
return;
|
|
}
|
|
|
|
console.log('✅ Customers ready, rendering...');
|
|
|
|
// Populate customer list
|
|
const searchInput = document.getElementById('createCustomerSearch');
|
|
|
|
// Reset search and render all customers
|
|
searchInput.value = '';
|
|
renderCustomerOptions(customersCache);
|
|
|
|
// Reset form
|
|
document.getElementById('createAgreementForm').reset();
|
|
|
|
// Set default values
|
|
document.getElementById('createStartDate').value = new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString().split('T')[0];
|
|
|
|
// Calculate initial hourly rate display
|
|
setTimeout(() => calculateHourlyRate(), 100);
|
|
|
|
// Show modal
|
|
console.log('📋 Opening modal...');
|
|
const modalElement = document.getElementById('createAgreementModal');
|
|
if (!modalElement) {
|
|
console.error('❌ Modal element not found!');
|
|
return;
|
|
}
|
|
console.log('✅ Found modal element');
|
|
|
|
const modal = new bootstrap.Modal(modalElement);
|
|
modal.show();
|
|
console.log('✅ Modal.show() called');
|
|
}
|
|
|
|
function renderCustomerOptions(customers) {
|
|
const listGroup = document.getElementById('createCustomerList');
|
|
|
|
console.log('📋 Rendering customers:', customers?.length || 0);
|
|
console.log('First customer:', customers?.[0]);
|
|
|
|
if (!customers || customers.length === 0) {
|
|
console.warn('⚠️ No customers to render');
|
|
listGroup.innerHTML = '<div class="list-group-item text-muted"><i class="bi bi-inbox me-2"></i>Ingen kunder fundet</div>';
|
|
return;
|
|
}
|
|
|
|
console.log('✅ Rendering', customers.length, 'customers');
|
|
listGroup.innerHTML = '';
|
|
customers.forEach((c, idx) => {
|
|
if (idx < 3) console.log(`Customer ${idx}:`, c);
|
|
const item = document.createElement('a');
|
|
item.href = '#';
|
|
item.className = 'list-group-item list-group-item-action';
|
|
item.dataset.customerId = c.id;
|
|
item.dataset.customerName = c.name || 'Ukendt';
|
|
|
|
item.innerHTML = `
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>${escapeHtml(c.name || 'Ukendt')}</strong>
|
|
${c.cvr_number ? `<br><small class="text-muted">CVR: ${escapeHtml(c.cvr_number)}</small>` : ''}
|
|
</div>
|
|
${c.is_active === false ? '<span class="badge bg-secondary">Inaktiv</span>' : ''}
|
|
</div>
|
|
`;
|
|
|
|
item.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
selectCustomer(c.id, c.name || 'Ukendt');
|
|
});
|
|
|
|
listGroup.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function selectCustomer(customerId, customerName) {
|
|
// Set hidden input
|
|
document.getElementById('createCustomerId').value = customerId;
|
|
|
|
// Update display
|
|
const selectedDiv = document.getElementById('selectedCustomerName');
|
|
selectedDiv.innerHTML = '';
|
|
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = 'alert alert-success mb-2';
|
|
alertDiv.innerHTML = `
|
|
<i class="bi bi-check-circle me-2"></i>
|
|
<strong>Valgt kunde:</strong> ${escapeHtml(customerName)}
|
|
`;
|
|
|
|
const clearBtn = document.createElement('button');
|
|
clearBtn.type = 'button';
|
|
clearBtn.className = 'btn btn-sm btn-link float-end text-decoration-none';
|
|
clearBtn.innerHTML = '<i class="bi bi-x"></i> Ryd';
|
|
clearBtn.addEventListener('click', clearCustomerSelection);
|
|
|
|
alertDiv.appendChild(clearBtn);
|
|
selectedDiv.appendChild(alertDiv);
|
|
|
|
// Hide list
|
|
document.getElementById('createCustomerList').style.display = 'none';
|
|
document.getElementById('createCustomerSearch').style.display = 'none';
|
|
}
|
|
|
|
function clearCustomerSelection() {
|
|
document.getElementById('createCustomerId').value = '';
|
|
document.getElementById('selectedCustomerName').innerHTML = '';
|
|
document.getElementById('createCustomerList').style.display = 'block';
|
|
document.getElementById('createCustomerSearch').style.display = 'block';
|
|
renderCustomerOptions(customersCache);
|
|
}
|
|
|
|
function searchCustomers() {
|
|
const searchTerm = document.getElementById('createCustomerSearch').value.toLowerCase();
|
|
const filtered = customersCache.filter(c =>
|
|
(c.name || '').toLowerCase().includes(searchTerm) ||
|
|
(c.cvr_number || '').toLowerCase().includes(searchTerm) ||
|
|
(c.email || '').toLowerCase().includes(searchTerm)
|
|
);
|
|
renderCustomerOptions(filtered);
|
|
}
|
|
|
|
async function submitCreateAgreement(event) {
|
|
event.preventDefault();
|
|
|
|
const form = event.target;
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const originalBtnText = submitBtn.innerHTML;
|
|
|
|
// Get form data
|
|
const customerIdInput = document.getElementById('createCustomerId');
|
|
const customerId = parseInt(customerIdInput.value);
|
|
|
|
// Validate customer selection
|
|
if (!customerId || isNaN(customerId)) {
|
|
alert('⚠️ Vælg venligst en kunde først');
|
|
return;
|
|
}
|
|
|
|
const customer = customersCache.find(c => c.id === customerId);
|
|
const customerName = customer?.name || '';
|
|
|
|
const data = {
|
|
customer_id: customerId,
|
|
customer_name: customerName,
|
|
monthly_hours: parseFloat(form.monthlyHours.value),
|
|
monthly_amount: parseFloat(form.monthlyAmount.value),
|
|
overtime_rate: parseFloat(form.overtimeRate.value),
|
|
rounding_minutes: parseInt(form.roundingMinutes.value),
|
|
start_date: form.startDate.value,
|
|
binding_months: parseInt(form.bindingMonths.value),
|
|
notice_period_days: parseInt(form.noticePeriodDays.value)
|
|
};
|
|
|
|
// Validate
|
|
if (!data.customer_id || data.monthly_hours <= 0 || data.monthly_amount <= 0) {
|
|
alert('Udfyld venligst alle påkrævede felter');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Opretter...';
|
|
|
|
const response = await fetch('/api/v1/fixed-price-agreements', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Fejl ved oprettelse');
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// Close modal
|
|
bootstrap.Modal.getInstance(document.getElementById('createAgreementModal')).hide();
|
|
|
|
// Show success message with details
|
|
const successMsg = `✅ Aftale oprettet!\n\nAftalenummer: ${result.agreement_number}\nKunde: ${customerName}\nMånedlige timer: ${data.monthly_hours}t\n\nAftalen er nu tilgængelig i listen.`;
|
|
|
|
// Reload list to show new agreement
|
|
await loadAgreements();
|
|
|
|
alert(successMsg);
|
|
|
|
} catch (error) {
|
|
console.error('Create error:', error);
|
|
alert('❌ Fejl: ' + error.message);
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = originalBtnText;
|
|
}
|
|
}
|
|
|
|
// Search functionality
|
|
document.getElementById('searchInput')?.addEventListener('input', (e) => {
|
|
const term = e.target.value.toLowerCase();
|
|
const rows = document.querySelectorAll('#agreementsBody tr');
|
|
rows.forEach(row => {
|
|
const text = row.textContent.toLowerCase();
|
|
row.style.display = text.includes(term) ? '' : 'none';
|
|
});
|
|
});
|
|
|
|
// Initialize after DOM is loaded
|
|
loadAgreements();
|
|
|
|
// Setup event listeners after modal is in DOM
|
|
setTimeout(() => {
|
|
// Auto-calculate overtime rate (125%)
|
|
document.getElementById('createHourlyRate')?.addEventListener('input', (e) => {
|
|
const hourlyRate = parseFloat(e.target.value);
|
|
if (hourlyRate > 0) {
|
|
document.getElementById('createOvertimeRate').value = (hourlyRate * 1.25).toFixed(2);
|
|
}
|
|
});
|
|
|
|
// Customer search listener
|
|
const searchInput = document.getElementById('createCustomerSearch');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', searchCustomers);
|
|
}
|
|
}, 100);
|
|
</script>
|
|
|
|
<!-- Create Agreement Modal -->
|
|
<div class="modal fade" id="createAgreementModal" tabindex="-1" aria-labelledby="createAgreementModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="createAgreementModalLabel">
|
|
<i class="bi bi-plus-circle text-primary me-2"></i>Opret Ny Fastpris Aftale
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="createAgreementForm" onsubmit="submitCreateAgreement(event)">
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<!-- Customer Selection -->
|
|
<div class="col-12">
|
|
<label class="form-label">Kunde <span class="text-danger">*</span></label>
|
|
<input type="hidden" id="createCustomerId" name="customerId" required>
|
|
|
|
<!-- Selected Customer Display -->
|
|
<div id="selectedCustomerName"></div>
|
|
|
|
<!-- Search Input -->
|
|
<input type="text"
|
|
class="form-control mb-2"
|
|
id="createCustomerSearch"
|
|
placeholder="🔍 Søg kunde (navn, CVR, email)..."
|
|
autocomplete="off">
|
|
|
|
<!-- Customer List -->
|
|
<div class="border rounded" style="max-height: 300px; overflow-y: auto;">
|
|
<div class="list-group list-group-flush" id="createCustomerList">
|
|
<div class="list-group-item text-center text-muted">
|
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
Indlæser kunder...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Monthly Hours -->
|
|
<div class="col-md-6">
|
|
<label for="createMonthlyHours" class="form-label">Månedlige Timer <span class="text-danger">*</span></label>
|
|
<input type="number" class="form-control" id="createMonthlyHours" name="monthlyHours"
|
|
min="1" step="0.5" value="160" required oninput="calculateHourlyRate()">
|
|
<div class="form-text">Timer inkluderet per måned</div>
|
|
</div>
|
|
|
|
<!-- Rounding Minutes -->
|
|
<div class="col-md-6">
|
|
<label for="createRoundingMinutes" class="form-label">Afrunding <span class="text-danger">*</span></label>
|
|
<select class="form-select" id="createRoundingMinutes" name="roundingMinutes" required>
|
|
<option value="0">Ingen afrunding</option>
|
|
<option value="15" selected>15 minutter</option>
|
|
<option value="30">30 minutter</option>
|
|
<option value="60">60 minutter</option>
|
|
</select>
|
|
<div class="form-text">Afrund tid ved registrering</div>
|
|
</div>
|
|
|
|
<!-- Monthly Amount -->
|
|
<div class="col-md-6">
|
|
<label for="createMonthlyAmount" class="form-label">Månedspris (DKK) <span class="text-danger">*</span></label>
|
|
<input type="number" class="form-control" id="createMonthlyAmount" name="monthlyAmount"
|
|
min="0" step="0.01" value="80000" required oninput="calculateHourlyRate()">
|
|
<div class="form-text">
|
|
Fast pris pr. måned
|
|
<span id="calculatedHourlyRate" class="text-primary fw-bold"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overtime Rate -->
|
|
<div class="col-md-6">
|
|
<label for="createOvertimeRate" class="form-label">Overtid Timepris (DKK) <span class="text-danger">*</span></label>
|
|
<input type="number" class="form-control" id="createOvertimeRate" name="overtimeRate"
|
|
min="0" step="0.01" value="625" required>
|
|
<div class="form-text">Pris for overtid (typisk 125%)</div>
|
|
</div>
|
|
|
|
<!-- Start Date -->
|
|
<div class="col-md-6">
|
|
<label for="createStartDate" class="form-label">Start Dato <span class="text-danger">*</span></label>
|
|
<input type="date" class="form-control" id="createStartDate" name="startDate" required>
|
|
<div class="form-text">Aftalens første dag</div>
|
|
</div>
|
|
|
|
<!-- Binding Months -->
|
|
<div class="col-md-6">
|
|
<label for="createBindingMonths" class="form-label">Binding (måneder) <span class="text-danger">*</span></label>
|
|
<input type="number" class="form-control" id="createBindingMonths" name="bindingMonths"
|
|
min="0" step="1" value="12" required>
|
|
<div class="form-text">0 = ingen binding</div>
|
|
</div>
|
|
|
|
<!-- Notice Period -->
|
|
<div class="col-md-6">
|
|
<label for="createNoticePeriodDays" class="form-label">Opsigelsesfrist (dage)</label>
|
|
<input type="number" class="form-control" id="createNoticePeriodDays" name="noticePeriodDays"
|
|
min="0" step="1" value="30">
|
|
<div class="form-text">Varslingsfrist ved opsigelse</div>
|
|
</div>
|
|
|
|
<!-- End Date (Optional) -->
|
|
<div class="col-md-6">
|
|
<label for="createEndDate" class="form-label">Slut Dato (valgfri)</label>
|
|
<input type="date" class="form-control" id="createEndDate" name="endDate">
|
|
<div class="form-text">Lad blank for løbende aftale</div>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<div class="alert alert-info mb-0">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
<strong>Note:</strong> Aftalen tildeles automatisk et unikt nummer (FPA-YYYYMMDD-XXX) ved oprettelse.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
<i class="bi bi-x-circle me-1"></i>Annuller
|
|
</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-check-circle me-1"></i>Opret Aftale
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|