bmc_hub/app/vendors/frontend/vendor_detail.html
Christian 3a35042788 feat: Implement Vendors API and Frontend
- Added a new API router for managing vendors with endpoints for listing, creating, updating, retrieving, and deleting vendors.
- Implemented frontend views for displaying vendor lists and details using Jinja2 templates.
- Created HTML templates for vendor list and detail pages with responsive design and dynamic content loading.
- Added JavaScript functionality for vendor management, including pagination, filtering, and modal forms for creating new vendors.
- Introduced a settings table in the database for system configuration and extended the users table with additional fields.
- Developed a script to import vendors from an OmniSync database into the PostgreSQL database, handling errors and logging progress.
2025-12-06 11:04:19 +01:00

423 lines
15 KiB
HTML

{% extends "shared/frontend/base.html" %}
{% block title %}Leverandør Detaljer - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.vendor-header {
background: linear-gradient(135deg, var(--accent) 0%, #1e6ba8 100%);
padding: 2rem;
border-radius: var(--border-radius);
color: white;
margin-bottom: 2rem;
}
.vendor-avatar-large {
width: 80px;
height: 80px;
border-radius: 16px;
background: rgba(255,255,255,0.2);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 2rem;
border: 3px solid rgba(255,255,255,0.3);
}
.vertical-nav {
position: sticky;
top: 100px;
}
.vertical-nav .nav-link {
color: var(--text-secondary);
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 0.5rem;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.vertical-nav .nav-link:hover,
.vertical-nav .nav-link.active {
background: var(--accent-light);
color: var(--accent);
border-left-color: var(--accent);
}
.info-row {
padding: 1rem 0;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.info-value {
color: var(--text-primary);
font-size: 1rem;
}
.category-badge-large {
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 1rem;
font-weight: 500;
display: inline-block;
}
.priority-indicator {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.25rem;
}
</style>
{% endblock %}
{% block content %}
<!-- Vendor Header -->
<div class="vendor-header" id="vendorHeader">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col-auto">
<div class="vendor-avatar-large" id="vendorAvatar"></div>
</div>
<div class="col">
<div class="d-flex align-items-center gap-3 mb-2">
<h2 class="mb-0 fw-bold" id="vendorName">Loading...</h2>
<span class="badge bg-white text-dark" id="vendorStatus"></span>
</div>
<div class="d-flex gap-4 text-white-50">
<span id="vendorDomain"></span>
<span id="vendorCategory"></span>
</div>
</div>
<div class="col-auto">
<button class="btn btn-light" onclick="editVendor()">
<i class="bi bi-pencil me-2"></i>Rediger
</button>
</div>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<!-- Vertical Navigation -->
<div class="col-lg-2">
<div class="vertical-nav">
<nav class="nav flex-column">
<a class="nav-link active" href="#oversigt" data-tab="oversigt">
<i class="bi bi-info-circle me-2"></i>Oversigt
</a>
<a class="nav-link" href="#produkter" data-tab="produkter">
<i class="bi bi-box-seam me-2"></i>Produkter
</a>
<a class="nav-link" href="#fakturaer" data-tab="fakturaer">
<i class="bi bi-receipt me-2"></i>Fakturaer
</a>
<a class="nav-link" href="#aktivitet" data-tab="aktivitet">
<i class="bi bi-clock-history me-2"></i>Aktivitet
</a>
</nav>
</div>
</div>
<!-- Content Area -->
<div class="col-lg-10">
<div class="tab-content">
<!-- Oversigt Tab -->
<div class="tab-pane fade show active" id="oversigt">
<div class="row g-4">
<!-- Vendor Information -->
<div class="col-lg-6">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Leverandør Information</h5>
<div id="vendorInfo">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</div>
<!-- Contact & System Info -->
<div class="col-lg-6">
<div class="card p-4 mb-4">
<h5 class="mb-4 fw-bold">Kontakt Information</h5>
<div id="contactInfo">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<div class="card p-4">
<h5 class="mb-4 fw-bold">System Information</h5>
<div id="systemInfo">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Produkter Tab -->
<div class="tab-pane fade" id="produkter">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Produkter fra denne leverandør</h5>
<p class="text-muted">Produkt tracking kommer snart...</p>
</div>
</div>
<!-- Fakturaer Tab -->
<div class="tab-pane fade" id="fakturaer">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Leverandør Fakturaer</h5>
<p class="text-muted">Faktura oversigt kommer snart...</p>
</div>
</div>
<!-- Aktivitet Tab -->
<div class="tab-pane fade" id="aktivitet">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Aktivitetslog</h5>
<p class="text-muted">Aktivitetshistorik kommer snart...</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const vendorId = {{ vendor_id }};
async function loadVendor() {
try {
const response = await fetch(`/api/v1/vendors/${vendorId}`);
if (!response.ok) {
throw new Error('Vendor not found');
}
const vendor = await response.json();
displayVendor(vendor);
} catch (error) {
console.error('Error loading vendor:', error);
document.getElementById('vendorName').textContent = 'Fejl ved indlæsning';
}
}
function displayVendor(vendor) {
// Header
document.getElementById('vendorName').textContent = vendor.name;
document.getElementById('vendorAvatar').textContent = getInitials(vendor.name);
document.getElementById('vendorStatus').textContent = vendor.is_active ? 'Aktiv' : 'Inaktiv';
document.getElementById('vendorStatus').className = `badge ${vendor.is_active ? 'bg-success' : 'bg-secondary'}`;
document.getElementById('vendorDomain').innerHTML = vendor.domain ? `<i class="bi bi-globe me-2"></i>${vendor.domain}` : '';
document.getElementById('vendorCategory').innerHTML = `${getCategoryIcon(vendor.category)} ${vendor.category}`;
// Update page title
document.title = `${vendor.name} - BMC Hub`;
// Vendor Info
document.getElementById('vendorInfo').innerHTML = `
${vendor.cvr_number ? `
<div class="info-row">
<div class="info-label">CVR-nummer</div>
<div class="info-value fw-semibold">${escapeHtml(vendor.cvr_number)}</div>
</div>
` : ''}
<div class="info-row">
<div class="info-label">Kategori</div>
<div class="info-value">
<span class="category-badge-large bg-light">
${getCategoryIcon(vendor.category)} ${escapeHtml(vendor.category)}
</span>
</div>
</div>
<div class="info-row">
<div class="info-label">Prioritet</div>
<div class="info-value">
<div class="priority-indicator ${getPriorityClass(vendor.priority)}">
${vendor.priority}
</div>
</div>
</div>
${vendor.economic_supplier_number ? `
<div class="info-row">
<div class="info-label">e-conomic Leverandør Nr.</div>
<div class="info-value">${vendor.economic_supplier_number}</div>
</div>
` : ''}
${vendor.notes ? `
<div class="info-row">
<div class="info-label">Noter</div>
<div class="info-value">${escapeHtml(vendor.notes)}</div>
</div>
` : ''}
`;
// Contact Info
document.getElementById('contactInfo').innerHTML = `
${vendor.email ? `
<div class="info-row">
<div class="info-label">Email</div>
<div class="info-value">
<a href="mailto:${escapeHtml(vendor.email)}" class="text-decoration-none">
<i class="bi bi-envelope me-2"></i>${escapeHtml(vendor.email)}
</a>
</div>
</div>
` : ''}
${vendor.phone ? `
<div class="info-row">
<div class="info-label">Telefon</div>
<div class="info-value">
<a href="tel:${escapeHtml(vendor.phone)}" class="text-decoration-none">
<i class="bi bi-telephone me-2"></i>${escapeHtml(vendor.phone)}
</a>
</div>
</div>
` : ''}
${vendor.website ? `
<div class="info-row">
<div class="info-label">Website</div>
<div class="info-value">
<a href="${escapeHtml(vendor.website)}" target="_blank" class="text-decoration-none">
<i class="bi bi-globe me-2"></i>${escapeHtml(vendor.website)}
</a>
</div>
</div>
` : ''}
${vendor.address ? `
<div class="info-row">
<div class="info-label">Adresse</div>
<div class="info-value">
<i class="bi bi-geo-alt me-2"></i>
${escapeHtml(vendor.address)}
${vendor.postal_code || vendor.city ? `<br>${vendor.postal_code ? escapeHtml(vendor.postal_code) + ' ' : ''}${vendor.city ? escapeHtml(vendor.city) : ''}` : ''}
${vendor.country && vendor.country !== 'Danmark' ? `<br>${escapeHtml(vendor.country)}` : ''}
</div>
</div>
` : ''}
${vendor.email_pattern ? `
<div class="info-row">
<div class="info-label">Email Pattern</div>
<div class="info-value"><code>${escapeHtml(vendor.email_pattern)}</code></div>
</div>
` : ''}
`;
// System Info
document.getElementById('systemInfo').innerHTML = `
<div class="info-row">
<div class="info-label">Oprettet</div>
<div class="info-value">${formatDate(vendor.created_at)}</div>
</div>
${vendor.updated_at ? `
<div class="info-row">
<div class="info-label">Sidst opdateret</div>
<div class="info-value">${formatDate(vendor.updated_at)}</div>
</div>
` : ''}
<div class="info-row">
<div class="info-label">ID</div>
<div class="info-value"><code>#${vendor.id}</code></div>
</div>
`;
}
function getCategoryIcon(category) {
const icons = {
hardware: '🖥️',
software: '💻',
telecom: '📡',
services: '🛠️',
hosting: '☁️',
general: '📦'
};
return icons[category] || '📦';
}
function getPriorityClass(priority) {
if (priority >= 80) return 'bg-danger text-white';
if (priority >= 60) return 'bg-warning';
if (priority >= 40) return 'bg-info';
return 'bg-secondary text-white';
}
function getInitials(name) {
return name.split(' ').map(word => word[0]).join('').substring(0, 2).toUpperCase();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('da-DK', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function editVendor() {
// TODO: Implement edit modal
alert('Edit funktion kommer snart!');
}
// Tab navigation
document.querySelectorAll('.vertical-nav .nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const tab = link.dataset.tab;
// Update nav
document.querySelectorAll('.vertical-nav .nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Update content
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('show', 'active');
});
document.getElementById(tab).classList.add('show', 'active');
});
});
// Load vendor on page ready
document.addEventListener('DOMContentLoaded', loadVendor);
</script>
{% endblock %}