Add Material Blue design templates for dashboard and customer overview pages

This commit is contained in:
Christian 2025-12-06 02:22:01 +01:00
parent 731a541f00
commit 050e886f22
63 changed files with 13633 additions and 143 deletions

View File

@ -33,6 +33,24 @@ BMC Hub is a central management system for BMC Networks built with **FastAPI + P
- Log all external API calls before execution - Log all external API calls before execution
- Provide dry-run mode that logs without executing - Provide dry-run mode that logs without executing
## Design System
### "Nordic Top" Standard
The UI/UX is based on the **Nordic Top** design template found in `docs/design_reference/`.
- **Style**: Minimalist, clean, "Nordic" aesthetic.
- **Layout**: Fluid width, top navigation bar, card-based content.
- **Primary Color**: Deep Blue (`#0f4c75`).
### Mandatory Features
All frontend implementations MUST support:
1. **Dark Mode**: A toggle to switch between Light and Dark themes.
2. **Color Themes**: Architecture must allow for dynamic accent color changes (CSS Variables).
3. **Responsive Design**: Mobile-first approach using Bootstrap 5 grid.
### Reference Implementation
- **Components**: See `docs/design_reference/components.html` for the master list of UI elements.
- **Templates**: Use `index.html`, `customers.html`, `form.html` in `docs/design_reference/` as base templates.
## Development Workflows ## Development Workflows
### Local Development Setup ### Local Development Setup
@ -132,6 +150,9 @@ if settings.ECONOMIC_READ_ONLY:
- For health checks: return dict with `status`, `service`, `version` keys - For health checks: return dict with `status`, `service`, `version` keys
### File Organization ### File Organization
- **Feature-Based Structure (Vertical Slices)**: Organize code by domain/feature.
- **Pattern**: `/<feature>/frontend` and `/<feature>/backend`
- **Example**: `Customers_companies/frontend` (Templates, UI) and `Customers_companies/backend` (Routers, Models)
- **One router per domain** - don't create mega-files - **One router per domain** - don't create mega-files
- **Services in `app/services/`** for business logic (e.g., `economic.py` for API integration) - **Services in `app/services/`** for business logic (e.g., `economic.py` for API integration)
- **Jobs in `app/jobs/`** for scheduled tasks - **Jobs in `app/jobs/`** for scheduled tasks

View File

@ -0,0 +1,86 @@
"""
Auth API Router - Login, Logout, Me endpoints
"""
from fastapi import APIRouter, HTTPException, status, Request, Depends
from pydantic import BaseModel
from app.core.auth_service import AuthService
from app.core.auth_dependencies import get_current_user
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
class LoginRequest(BaseModel):
username: str
password: str
class LoginResponse(BaseModel):
access_token: str
token_type: str = "bearer"
user: dict
class LogoutRequest(BaseModel):
token_jti: str
@router.post("/login", response_model=LoginResponse)
async def login(request: Request, credentials: LoginRequest):
"""
Authenticate user and return JWT token
"""
ip_address = request.client.host if request.client else None
# Authenticate user
user = AuthService.authenticate_user(
username=credentials.username,
password=credentials.password,
ip_address=ip_address
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Create access token
access_token = AuthService.create_access_token(
user_id=user['user_id'],
username=user['username'],
is_superadmin=user['is_superadmin']
)
return LoginResponse(
access_token=access_token,
user=user
)
@router.post("/logout")
async def logout(request: LogoutRequest, current_user: dict = Depends(get_current_user)):
"""
Revoke JWT token (logout)
"""
AuthService.revoke_token(request.token_jti, current_user['id'])
return {"message": "Successfully logged out"}
@router.get("/me")
async def get_me(current_user: dict = Depends(get_current_user)):
"""
Get current authenticated user info
"""
return {
"id": current_user['id'],
"username": current_user['username'],
"email": current_user['email'],
"full_name": current_user['full_name'],
"is_superadmin": current_user['is_superadmin'],
"permissions": current_user['permissions']
}

20
app/auth/backend/views.py Normal file
View File

@ -0,0 +1,20 @@
"""
Auth Frontend Views - Login page
"""
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
router = APIRouter()
templates = Jinja2Templates(directory="app")
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
"""
Render login page
"""
return templates.TemplateResponse(
"auth/frontend/login.html",
{"request": request}
)

View File

@ -0,0 +1,199 @@
{% extends "shared/frontend/base.html" %}
{% block title %}Login - BMC Hub{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center align-items-center" style="min-height: 80vh;">
<div class="col-md-5 col-lg-4">
<div class="card shadow-sm">
<div class="card-body p-4">
<div class="text-center mb-4">
<h2 class="fw-bold" style="color: var(--primary-color);">BMC Hub</h2>
<p class="text-muted">Log ind for at fortsætte</p>
</div>
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Brugernavn</label>
<input
type="text"
class="form-control"
id="username"
name="username"
placeholder="Indtast brugernavn"
required
autofocus
>
</div>
<div class="mb-3">
<label for="password" class="form-label">Adgangskode</label>
<input
type="password"
class="form-control"
id="password"
name="password"
placeholder="Indtast adgangskode"
required
>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="rememberMe">
<label class="form-check-label" for="rememberMe">
Husk mig
</label>
</div>
<div id="errorMessage" class="alert alert-danger d-none" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<span id="errorText"></span>
</div>
<button type="submit" class="btn btn-primary w-100 mb-3">
<i class="bi bi-box-arrow-in-right me-2"></i>
Log ind
</button>
</form>
<div class="text-center">
<small class="text-muted">
<a href="#" class="text-decoration-none">Glemt adgangskode?</a>
</small>
</div>
</div>
</div>
<div class="text-center mt-3">
<small class="text-muted">
BMC Networks &copy; 2024
</small>
</div>
</div>
</div>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorMessage = document.getElementById('errorMessage');
const errorText = document.getElementById('errorText');
const submitBtn = e.target.querySelector('button[type="submit"]');
// Disable submit button
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="bi bi-hourglass-split me-2"></i>Logger ind...';
// Hide previous errors
errorMessage.classList.add('d-none');
try {
const response = await fetch('/api/v1/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
// Store token in localStorage
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('user', JSON.stringify(data.user));
// Redirect to dashboard
window.location.href = '/';
} else {
// Show error
errorText.textContent = data.detail || 'Login fejlede. Tjek brugernavn og adgangskode.';
errorMessage.classList.remove('d-none');
// Re-enable submit button
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-box-arrow-in-right me-2"></i>Log ind';
}
} catch (error) {
console.error('Login error:', error);
errorText.textContent = 'Kunne ikke forbinde til serveren. Prøv igen.';
errorMessage.classList.remove('d-none');
// Re-enable submit button
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi bi-box-arrow-in-right me-2"></i>Log ind';
}
});
// Check if already logged in
const token = localStorage.getItem('access_token');
if (token) {
// Verify token is still valid
fetch('/api/v1/auth/me', {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(response => {
if (response.ok) {
// Redirect to dashboard
window.location.href = '/';
} else {
// Token invalid, clear storage
localStorage.removeItem('access_token');
localStorage.removeItem('user');
}
})
.catch(() => {
// Network error, clear storage
localStorage.removeItem('access_token');
localStorage.removeItem('user');
});
}
</script>
<style>
/* Login page specific styles */
.card {
border: none;
border-radius: 12px;
}
.card-body {
padding: 2.5rem 2rem;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(15, 76, 117, 0.15);
}
.btn-primary {
padding: 0.75rem;
font-weight: 500;
border-radius: 8px;
}
/* Dark mode adjustments */
[data-theme="dark"] .card {
background-color: var(--dark-card);
border: 1px solid rgba(255, 255, 255, 0.1);
}
[data-theme="dark"] .form-control {
background-color: var(--dark-surface);
border-color: rgba(255, 255, 255, 0.1);
color: var(--dark-text);
}
[data-theme="dark"] .form-control:focus {
background-color: var(--dark-surface);
border-color: var(--primary-color);
color: var(--dark-text);
}
</style>
{% endblock %}

View File

@ -0,0 +1,308 @@
"""
Contact API Router
Handles contact CRUD operations with multi-company support
"""
from fastapi import APIRouter, HTTPException, Query
from typing import Optional, List
from app.core.database import execute_query, execute_insert, execute_update
from app.models.schemas import Contact, ContactCreate, ContactUpdate, ContactCompanyLink, CompanyInfo
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/contacts", response_model=dict)
async def get_contacts(
search: Optional[str] = None,
customer_id: Optional[int] = None,
is_active: Optional[bool] = None,
limit: int = Query(default=20, le=100),
offset: int = Query(default=0, ge=0)
):
"""
Get all contacts with optional filtering, search, and pagination
"""
try:
# Build WHERE clauses
where_clauses = []
params = []
if search:
where_clauses.append("(c.first_name ILIKE %s OR c.last_name ILIKE %s OR c.email ILIKE %s)")
params.extend([f"%{search}%", f"%{search}%", f"%{search}%"])
if is_active is not None:
where_clauses.append("c.is_active = %s")
params.append(is_active)
if customer_id is not None:
where_clauses.append("EXISTS (SELECT 1 FROM contact_companies cc WHERE cc.contact_id = c.id AND cc.customer_id = %s)")
params.append(customer_id)
where_sql = "WHERE " + " AND ".join(where_clauses) if where_clauses else ""
# Count total
count_query = f"""
SELECT COUNT(DISTINCT c.id)
FROM contacts c
{where_sql}
"""
count_result = execute_query(count_query, tuple(params), fetchone=True)
total = count_result['count'] if count_result else 0
# Get contacts with company count
query = f"""
SELECT
c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile,
c.title, c.department, c.is_active, c.vtiger_id,
c.created_at, c.updated_at,
COUNT(DISTINCT cc.customer_id) as company_count,
ARRAY_AGG(DISTINCT cu.name ORDER BY cu.name) FILTER (WHERE cu.name IS NOT NULL) as company_names
FROM contacts c
LEFT JOIN contact_companies cc ON c.id = cc.contact_id
LEFT JOIN customers cu ON cc.customer_id = cu.id
{where_sql}
GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile,
c.title, c.department, c.is_active, c.vtiger_id, c.created_at, c.updated_at
ORDER BY c.last_name, c.first_name
LIMIT %s OFFSET %s
"""
params.extend([limit, offset])
contacts = execute_query(query, tuple(params)) # Default is fetchall
return {
"contacts": contacts or [],
"total": total,
"limit": limit,
"offset": offset
}
except Exception as e:
logger.error(f"Failed to get contacts: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/contacts/{contact_id}", response_model=dict)
async def get_contact(contact_id: int):
"""
Get a single contact with all linked companies
"""
try:
# Get contact
contact_query = """
SELECT id, first_name, last_name, email, phone, mobile,
title, department, is_active, vtiger_id,
created_at, updated_at
FROM contacts
WHERE id = %s
"""
contact = execute_query(contact_query, (contact_id,), fetchone=True)
if not contact:
raise HTTPException(status_code=404, detail="Contact not found")
# Get linked companies
companies_query = """
SELECT
cu.id, cu.name,
cc.is_primary, cc.role, cc.notes
FROM contact_companies cc
JOIN customers cu ON cc.customer_id = cu.id
WHERE cc.contact_id = %s
ORDER BY cc.is_primary DESC, cu.name
"""
companies = execute_query(companies_query, (contact_id,)) # Default is fetchall
contact['companies'] = companies or []
return contact
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to get contact {contact_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/contacts", response_model=dict)
async def create_contact(contact: ContactCreate):
"""
Create a new contact and link to companies
"""
try:
# Insert contact
insert_query = """
INSERT INTO contacts (first_name, last_name, email, phone, mobile, title, department, is_active)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
"""
contact_id = execute_insert(
insert_query,
(contact.first_name, contact.last_name, contact.email, contact.phone,
contact.mobile, contact.title, contact.department, contact.is_active)
)
# Link to companies
if contact.company_ids:
for idx, customer_id in enumerate(contact.company_ids):
is_primary = idx == 0 and contact.is_primary
link_query = """
INSERT INTO contact_companies (contact_id, customer_id, is_primary, role, notes)
VALUES (%s, %s, %s, %s, %s)
"""
execute_insert(
link_query,
(contact_id, customer_id, is_primary, contact.role, contact.notes)
)
# Return created contact
return await get_contact(contact_id)
except Exception as e:
logger.error(f"Failed to create contact: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.put("/contacts/{contact_id}", response_model=dict)
async def update_contact(contact_id: int, contact: ContactUpdate):
"""
Update a contact
"""
try:
# Check if contact exists
existing = execute_query("SELECT id FROM contacts WHERE id = %s", (contact_id,), fetchone=True)
if not existing:
raise HTTPException(status_code=404, detail="Contact not found")
# Build update query
update_fields = []
params = []
if contact.first_name is not None:
update_fields.append("first_name = %s")
params.append(contact.first_name)
if contact.last_name is not None:
update_fields.append("last_name = %s")
params.append(contact.last_name)
if contact.email is not None:
update_fields.append("email = %s")
params.append(contact.email)
if contact.phone is not None:
update_fields.append("phone = %s")
params.append(contact.phone)
if contact.mobile is not None:
update_fields.append("mobile = %s")
params.append(contact.mobile)
if contact.title is not None:
update_fields.append("title = %s")
params.append(contact.title)
if contact.department is not None:
update_fields.append("department = %s")
params.append(contact.department)
if contact.is_active is not None:
update_fields.append("is_active = %s")
params.append(contact.is_active)
if not update_fields:
return await get_contact(contact_id)
update_query = f"""
UPDATE contacts
SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP
WHERE id = %s
"""
params.append(contact_id)
execute_update(update_query, tuple(params))
return await get_contact(contact_id)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to update contact {contact_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/contacts/{contact_id}")
async def delete_contact(contact_id: int):
"""
Delete a contact (cascade deletes company links)
"""
try:
result = execute_update("DELETE FROM contacts WHERE id = %s", (contact_id,))
if result == 0:
raise HTTPException(status_code=404, detail="Contact not found")
return {"message": "Contact deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to delete contact {contact_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/contacts/{contact_id}/companies")
async def link_contact_to_company(contact_id: int, link: ContactCompanyLink):
"""
Link a contact to a company
"""
try:
# Check if contact exists
contact = execute_query("SELECT id FROM contacts WHERE id = %s", (contact_id,), fetchone=True)
if not contact:
raise HTTPException(status_code=404, detail="Contact not found")
# Check if company exists
customer = execute_query("SELECT id FROM customers WHERE id = %s", (link.customer_id,), fetchone=True)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Insert link (ON CONFLICT updates)
query = """
INSERT INTO contact_companies (contact_id, customer_id, is_primary, role, notes)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (contact_id, customer_id)
DO UPDATE SET is_primary = EXCLUDED.is_primary, role = EXCLUDED.role, notes = EXCLUDED.notes
"""
execute_insert(query, (contact_id, link.customer_id, link.is_primary, link.role, link.notes))
return {"message": "Contact linked to company successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to link contact to company: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/contacts/{contact_id}/companies/{customer_id}")
async def unlink_contact_from_company(contact_id: int, customer_id: int):
"""
Unlink a contact from a company
"""
try:
result = execute_update(
"DELETE FROM contact_companies WHERE contact_id = %s AND customer_id = %s",
(contact_id, customer_id)
)
if result == 0:
raise HTTPException(status_code=404, detail="Link not found")
return {"message": "Contact unlinked from company successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to unlink contact from company: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@ -0,0 +1,28 @@
"""
Contact view routes for rendering HTML pages
"""
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
router = APIRouter()
templates = Jinja2Templates(directory="app")
@router.get("/contacts", response_class=HTMLResponse)
async def contacts_page(request: Request):
"""
Render the contacts list page
"""
return templates.TemplateResponse("contacts/frontend/contacts.html", {"request": request})
@router.get("/contacts/{contact_id}", response_class=HTMLResponse)
async def contact_detail_page(request: Request, contact_id: int):
"""
Render the contact detail page
"""
return templates.TemplateResponse("contacts/frontend/contact_detail.html", {
"request": request,
"contact_id": contact_id
})

View File

@ -0,0 +1,516 @@
{% extends "shared/frontend/base.html" %}
{% block title %}Kontakt Detaljer - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.contact-header {
background: var(--accent);
color: white;
padding: 3rem 2rem;
border-radius: 12px;
margin-bottom: 2rem;
}
.contact-avatar-large {
width: 80px;
height: 80px;
border-radius: 50%;
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);
}
.nav-pills-vertical {
border-right: 1px solid rgba(0, 0, 0, 0.1);
padding-right: 0;
}
.nav-pills-vertical .nav-link {
color: var(--text-secondary);
border-radius: 8px 0 0 8px;
padding: 1rem 1.5rem;
font-weight: 500;
margin-bottom: 0.5rem;
transition: all 0.2s;
text-align: left;
}
.nav-pills-vertical .nav-link:hover {
background: var(--accent-light);
color: var(--accent);
}
.nav-pills-vertical .nav-link.active {
background: var(--accent);
color: white;
}
.nav-pills-vertical .nav-link i {
width: 20px;
margin-right: 0.75rem;
}
.info-card {
background: var(--bg-card);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: 500;
color: var(--text-secondary);
}
.info-value {
color: var(--text-primary);
font-weight: 500;
}
.company-card {
background: var(--bg-card);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.2s;
position: relative;
}
.company-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.remove-company-btn {
position: absolute;
top: 1rem;
right: 1rem;
}
</style>
{% endblock %}
{% block content %}
<!-- Contact Header -->
<div class="contact-header">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex align-items-center">
<div class="contact-avatar-large me-4" id="contactAvatar">?</div>
<div>
<h1 class="fw-bold mb-2" id="contactName">Loading...</h1>
<div class="d-flex gap-3 align-items-center">
<span id="contactTitle"></span>
<span class="badge bg-white bg-opacity-20" id="contactStatus"></span>
</div>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-light btn-sm" onclick="editContact()">
<i class="bi bi-pencil me-2"></i>Rediger
</button>
<button class="btn btn-light btn-sm" onclick="window.location.href='/contacts'">
<i class="bi bi-arrow-left me-2"></i>Tilbage
</button>
</div>
</div>
</div>
<!-- Content Layout with Sidebar Navigation -->
<div class="row">
<div class="col-lg-3 col-md-4">
<!-- Vertical Navigation -->
<ul class="nav nav-pills nav-pills-vertical flex-column" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#overview">
<i class="bi bi-info-circle"></i>Oversigt
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#companies">
<i class="bi bi-building"></i>Firmaer
</a>
</li>
</ul>
</div>
<div class="col-lg-9 col-md-8">
<!-- Tab Content -->
<div class="tab-content">
<!-- Overview Tab -->
<div class="tab-pane fade show active" id="overview">
<div class="row g-4">
<div class="col-lg-6">
<div class="info-card">
<h5 class="fw-bold mb-4">Kontakt Oplysninger</h5>
<div class="info-row">
<span class="info-label">Navn</span>
<span class="info-value" id="fullName">-</span>
</div>
<div class="info-row">
<span class="info-label">Email</span>
<span class="info-value" id="email">-</span>
</div>
<div class="info-row">
<span class="info-label">Telefon</span>
<span class="info-value" id="phone">-</span>
</div>
<div class="info-row">
<span class="info-label">Mobil</span>
<span class="info-value" id="mobile">-</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="info-card">
<h5 class="fw-bold mb-4">Rolle & Stilling</h5>
<div class="info-row">
<span class="info-label">Titel</span>
<span class="info-value" id="title">-</span>
</div>
<div class="info-row">
<span class="info-label">Afdeling</span>
<span class="info-value" id="department">-</span>
</div>
<div class="info-row">
<span class="info-label">Status</span>
<span class="info-value" id="activeStatus">-</span>
</div>
<div class="info-row">
<span class="info-label">Antal Firmaer</span>
<span class="info-value" id="companyCount">-</span>
</div>
</div>
</div>
<div class="col-12">
<div class="info-card">
<h5 class="fw-bold mb-3">System Info</h5>
<div class="row">
<div class="col-md-6">
<div class="info-row">
<span class="info-label">vTiger ID</span>
<span class="info-value" id="vtigerId">-</span>
</div>
</div>
<div class="col-md-6">
<div class="info-row">
<span class="info-label">Oprettet</span>
<span class="info-value" id="createdAt">-</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Companies Tab -->
<div class="tab-pane fade" id="companies">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-0">Tilknyttede Firmaer</h5>
<button class="btn btn-primary btn-sm" onclick="showAddCompanyModal()">
<i class="bi bi-plus-lg me-2"></i>Tilføj Firma
</button>
</div>
<div class="row g-4" id="companiesContainer">
<div class="col-12 text-center py-5">
<div class="spinner-border text-primary"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Company Modal -->
<div class="modal fade" id="addCompanyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tilføj Firma til Kontakt</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addCompanyForm">
<div class="mb-3">
<label class="form-label">Vælg Firma</label>
<select class="form-select" id="companySelectModal" required>
<option value="">Vælg et firma...</option>
<!-- Populated dynamically -->
</select>
</div>
<div class="mb-3">
<label class="form-label">Rolle</label>
<input type="text" class="form-control" id="roleInputModal" placeholder="Primær kontakt, Fakturering...">
</div>
<div class="mb-3">
<label class="form-label">Noter</label>
<textarea class="form-control" id="notesInputModal" rows="3"></textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isPrimaryInputModal">
<label class="form-check-label" for="isPrimaryInputModal">
Primær kontakt for dette firma
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" onclick="addCompanyToContact()">
<i class="bi bi-plus-lg me-2"></i>Tilføj
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const contactId = parseInt(window.location.pathname.split('/').pop());
let contactData = null;
document.addEventListener('DOMContentLoaded', () => {
loadContact();
loadCompaniesForSelect();
// Load companies when tab is shown
document.querySelector('a[href="#companies"]').addEventListener('shown.bs.tab', () => {
loadCompanies();
});
});
async function loadContact() {
try {
const response = await fetch(`/api/v1/contacts/${contactId}`);
if (!response.ok) {
throw new Error('Contact not found');
}
contactData = await response.json();
displayContact(contactData);
} catch (error) {
console.error('Failed to load contact:', error);
alert('Kunne ikke indlæse kontakt');
window.location.href = '/contacts';
}
}
function displayContact(contact) {
// Update page title
document.title = `${contact.first_name} ${contact.last_name} - BMC Hub`;
// Header
const initials = getInitials(contact.first_name, contact.last_name);
document.getElementById('contactAvatar').textContent = initials;
document.getElementById('contactName').textContent = `${contact.first_name} ${contact.last_name}`;
document.getElementById('contactTitle').textContent = contact.title || '';
const statusBadge = contact.is_active
? '<i class="bi bi-check-circle me-1"></i>Aktiv'
: '<i class="bi bi-x-circle me-1"></i>Inaktiv';
document.getElementById('contactStatus').innerHTML = statusBadge;
// Contact Information
document.getElementById('fullName').textContent = `${contact.first_name} ${contact.last_name}`;
document.getElementById('email').textContent = contact.email || '-';
document.getElementById('phone').textContent = contact.phone || '-';
document.getElementById('mobile').textContent = contact.mobile || '-';
// Role & Position
document.getElementById('title').textContent = contact.title || '-';
document.getElementById('department').textContent = contact.department || '-';
document.getElementById('activeStatus').innerHTML = contact.is_active
? '<span class="badge bg-success">Aktiv</span>'
: '<span class="badge bg-secondary">Inaktiv</span>';
document.getElementById('companyCount').textContent = contact.companies ? contact.companies.length : 0;
// System Info
document.getElementById('vtigerId').textContent = contact.vtiger_id || '-';
document.getElementById('createdAt').textContent = new Date(contact.created_at).toLocaleString('da-DK');
// Load companies if tab is active
if (document.querySelector('a[href="#companies"]').classList.contains('active')) {
displayCompanies(contact.companies);
}
}
async function loadCompanies() {
if (!contactData) return;
displayCompanies(contactData.companies);
}
function displayCompanies(companies) {
const container = document.getElementById('companiesContainer');
if (!companies || companies.length === 0) {
container.innerHTML = '<div class="col-12 text-center py-5 text-muted">Ingen firmaer tilknyttet</div>';
return;
}
container.innerHTML = companies.map(company => `
<div class="col-md-6">
<div class="company-card">
<button class="btn btn-sm btn-danger remove-company-btn" onclick="removeCompany(${company.id})" title="Fjern firma">
<i class="bi bi-x-lg"></i>
</button>
<div class="d-flex align-items-start mb-3">
<div class="flex-grow-1">
<h6 class="fw-bold mb-1">
<a href="/customers/${company.id}" class="text-decoration-none">${escapeHtml(company.name)}</a>
</h6>
${company.is_primary ? '<span class="badge bg-primary">Primær Kontakt</span>' : ''}
</div>
</div>
${company.role ? `
<div class="mb-2">
<small class="text-muted">Rolle:</small>
<div>${escapeHtml(company.role)}</div>
</div>
` : ''}
${company.notes ? `
<div>
<small class="text-muted">Noter:</small>
<div class="small">${escapeHtml(company.notes)}</div>
</div>
` : ''}
</div>
</div>
`).join('');
}
async function loadCompaniesForSelect() {
try {
const response = await fetch('/api/v1/customers?limit=1000');
const data = await response.json();
const select = document.getElementById('companySelectModal');
select.innerHTML = '<option value="">Vælg et firma...</option>' +
data.customers.map(c => `<option value="${c.id}">${escapeHtml(c.name)}</option>`).join('');
} catch (error) {
console.error('Failed to load companies:', error);
}
}
function showAddCompanyModal() {
// Reset form
document.getElementById('addCompanyForm').reset();
// Show modal
const modal = new bootstrap.Modal(document.getElementById('addCompanyModal'));
modal.show();
}
async function addCompanyToContact() {
const customerId = parseInt(document.getElementById('companySelectModal').value);
if (!customerId) {
alert('Vælg venligst et firma');
return;
}
const linkData = {
customer_id: customerId,
is_primary: document.getElementById('isPrimaryInputModal').checked,
role: document.getElementById('roleInputModal').value.trim() || null,
notes: document.getElementById('notesInputModal').value.trim() || null
};
try {
const response = await fetch(`/api/v1/contacts/${contactId}/companies`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(linkData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Kunne ikke tilføje firma');
}
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('addCompanyModal'));
modal.hide();
// Reload contact
await loadContact();
// Switch to companies tab
const companiesTab = new bootstrap.Tab(document.querySelector('a[href="#companies"]'));
companiesTab.show();
} catch (error) {
console.error('Failed to add company:', error);
alert('Fejl: ' + error.message);
}
}
async function removeCompany(customerId) {
if (!confirm('Er du sikker på at du vil fjerne dette firma fra kontakten?')) {
return;
}
try {
const response = await fetch(`/api/v1/contacts/${contactId}/companies/${customerId}`, {
method: 'DELETE'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Kunne ikke fjerne firma');
}
// Reload contact
await loadContact();
} catch (error) {
console.error('Failed to remove company:', error);
alert('Fejl: ' + error.message);
}
}
function editContact() {
// TODO: Open edit modal with pre-filled data
console.log('Edit contact:', contactId);
}
function getInitials(firstName, lastName) {
if (!firstName && !lastName) return '?';
const first = firstName ? firstName[0] : '';
const last = lastName ? lastName[0] : '';
return (first + last).toUpperCase();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}

View File

@ -0,0 +1,483 @@
{% extends "shared/frontend/base.html" %}
{% block title %}Kontakter - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.filter-btn {
background: var(--bg-card);
border: 1px solid rgba(0,0,0,0.1);
color: var(--text-secondary);
padding: 0.5rem 1.2rem;
border-radius: 20px;
font-size: 0.9rem;
transition: all 0.2s;
cursor: pointer;
}
.filter-btn:hover, .filter-btn.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.contact-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--accent-light);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
}
.pagination-btn {
border: 1px solid rgba(0,0,0,0.1);
padding: 0.5rem 1rem;
background: var(--bg-card);
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s;
}
.pagination-btn:hover:not(:disabled) {
background: var(--accent-light);
border-color: var(--accent);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Kontakter</h2>
<p class="text-muted mb-0">Administrer kontaktpersoner</p>
</div>
<div class="d-flex gap-3">
<input type="text" id="searchInput" class="header-search" placeholder="Søg navn, email, telefon...">
<button class="btn btn-primary" onclick="showCreateContactModal()">
<i class="bi bi-plus-lg me-2"></i>Opret Kontakt
</button>
</div>
</div>
<div class="mb-4 d-flex gap-2 flex-wrap">
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">
Alle Kontakter <span id="countAll" class="ms-1"></span>
</button>
<button class="filter-btn" data-filter="active" onclick="setFilter('active')">
Aktive <span id="countActive" class="ms-1"></span>
</button>
<button class="filter-btn" data-filter="inactive" onclick="setFilter('inactive')">
Inaktive <span id="countInactive" class="ms-1"></span>
</button>
</div>
<div class="card p-4">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Navn</th>
<th>Kontakt Info</th>
<th>Titel</th>
<th>Firmaer</th>
<th>Status</th>
<th class="text-end">Handlinger</th>
</tr>
</thead>
<tbody id="contactsTableBody">
<tr>
<td colspan="6" 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>
<!-- Pagination -->
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted small">
Viser <span id="showingStart">0</span>-<span id="showingEnd">0</span> af <span id="totalCount">0</span> kontakter
</div>
<div class="d-flex gap-2">
<button class="pagination-btn" id="prevBtn" onclick="previousPage()">
<i class="bi bi-chevron-left"></i> Forrige
</button>
<button class="pagination-btn" id="nextBtn" onclick="nextPage()">
Næste <i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
</div>
<!-- Create Contact Modal -->
<div class="modal fade" id="createContactModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Opret Ny Kontakt</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createContactForm">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Fornavn <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="firstNameInput" required>
</div>
<div class="col-md-6">
<label class="form-label">Efternavn <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="lastNameInput" required>
</div>
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="emailInput">
</div>
<div class="col-md-6">
<label class="form-label">Telefon</label>
<input type="text" class="form-control" id="phoneInput">
</div>
<div class="col-md-6">
<label class="form-label">Mobil</label>
<input type="text" class="form-control" id="mobileInput">
</div>
<div class="col-md-6">
<label class="form-label">Titel</label>
<input type="text" class="form-control" id="titleInput" placeholder="CEO, CTO, Manager...">
</div>
<div class="col-md-6">
<label class="form-label">Afdeling</label>
<input type="text" class="form-control" id="departmentInput">
</div>
<div class="col-md-6">
<label class="form-label">Rolle</label>
<input type="text" class="form-control" id="roleInput" placeholder="Primær kontakt, Fakturering...">
</div>
<div class="col-12">
<label class="form-label">Firmaer</label>
<select class="form-select" id="companySelect" multiple size="5">
<!-- Populated dynamically -->
</select>
<div class="form-text">Hold Ctrl/Cmd nede for at vælge flere firmaer</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isPrimaryInput">
<label class="form-check-label" for="isPrimaryInput">
Primær kontakt (for første valgte firma)
</label>
</div>
</div>
<div class="col-12">
<label class="form-label">Noter</label>
<textarea class="form-control" id="notesInput" rows="3"></textarea>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isActiveInput" checked>
<label class="form-check-label" for="isActiveInput">
Aktiv kontakt
</label>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" onclick="createContact()">
<i class="bi bi-plus-lg me-2"></i>Opret Kontakt
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 0;
let pageSize = 20;
let currentFilter = 'all';
let searchQuery = '';
let totalContacts = 0;
// Load contacts on page load
document.addEventListener('DOMContentLoaded', () => {
loadContacts();
loadCompaniesForSelect();
// Search with debounce
let searchTimeout;
document.getElementById('searchInput').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchQuery = e.target.value;
currentPage = 0;
loadContacts();
}, 300);
});
});
function setFilter(filter) {
currentFilter = filter;
currentPage = 0;
// Update active button
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
loadContacts();
}
async function loadContacts() {
const tbody = document.getElementById('contactsTableBody');
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-5"><div class="spinner-border text-primary"></div></td></tr>';
try {
// Build query parameters
let params = new URLSearchParams({
limit: pageSize,
offset: currentPage * pageSize
});
if (searchQuery) {
params.append('search', searchQuery);
}
if (currentFilter === 'active') {
params.append('is_active', 'true');
} else if (currentFilter === 'inactive') {
params.append('is_active', 'false');
}
const response = await fetch(`/api/v1/contacts?${params}`);
const data = await response.json();
totalContacts = data.total;
displayContacts(data.contacts);
updatePagination(data.total);
} catch (error) {
console.error('Failed to load contacts:', error);
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-5 text-danger">Kunne ikke indlæse kontakter</td></tr>';
}
}
function displayContacts(contacts) {
const tbody = document.getElementById('contactsTableBody');
if (!contacts || contacts.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center py-5 text-muted">Ingen kontakter fundet</td></tr>';
return;
}
tbody.innerHTML = contacts.map(contact => {
const initials = getInitials(contact.first_name, contact.last_name);
const statusBadge = contact.is_active
? '<span class="badge bg-success bg-opacity-10 text-success">Aktiv</span>'
: '<span class="badge bg-secondary bg-opacity-10 text-secondary">Inaktiv</span>';
const companyCount = contact.company_count || 0;
const companyNames = contact.company_names || [];
const companyDisplay = companyNames.length > 0
? companyNames.slice(0, 2).join(', ') + (companyNames.length > 2 ? '...' : '')
: '-';
return `
<tr style="cursor: pointer;" onclick="viewContact(${contact.id})">
<td>
<div class="d-flex align-items-center">
<div class="contact-avatar me-3">${initials}</div>
<div>
<div class="fw-bold">${escapeHtml(contact.first_name + ' ' + contact.last_name)}</div>
<div class="small text-muted">${contact.department || '-'}</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">${contact.email || '-'}</div>
<div class="small text-muted">${contact.mobile || contact.phone || '-'}</div>
</td>
<td class="text-muted">${contact.title || '-'}</td>
<td>
<span class="badge bg-light text-dark border" title="${companyNames.join(', ')}">
<i class="bi bi-building me-1"></i>${companyCount}
</span>
${companyDisplay !== '-' ? '<div class="small text-muted">' + companyDisplay + '</div>' : ''}
</td>
<td>${statusBadge}</td>
<td class="text-end">
<div class="btn-group">
<button class="btn btn-sm btn-light" onclick="event.stopPropagation(); viewContact(${contact.id})">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-light" onclick="event.stopPropagation(); editContact(${contact.id})">
<i class="bi bi-pencil"></i>
</button>
</div>
</td>
</tr>
`;
}).join('');
}
function updatePagination(total) {
const start = currentPage * pageSize + 1;
const end = Math.min((currentPage + 1) * pageSize, total);
document.getElementById('showingStart').textContent = total > 0 ? start : 0;
document.getElementById('showingEnd').textContent = end;
document.getElementById('totalCount').textContent = total;
// Update buttons
document.getElementById('prevBtn').disabled = currentPage === 0;
document.getElementById('nextBtn').disabled = end >= total;
}
function previousPage() {
if (currentPage > 0) {
currentPage--;
loadContacts();
}
}
function nextPage() {
if ((currentPage + 1) * pageSize < totalContacts) {
currentPage++;
loadContacts();
}
}
function viewContact(contactId) {
window.location.href = `/contacts/${contactId}`;
}
function editContact(contactId) {
// TODO: Open edit modal
console.log('Edit contact:', contactId);
}
async function loadCompaniesForSelect() {
try {
const response = await fetch('/api/v1/customers?limit=1000');
const data = await response.json();
const select = document.getElementById('companySelect');
select.innerHTML = data.customers.map(c =>
`<option value="${c.id}">${escapeHtml(c.name)}</option>`
).join('');
} catch (error) {
console.error('Failed to load companies:', error);
}
}
function showCreateContactModal() {
// Reset form
document.getElementById('createContactForm').reset();
document.getElementById('isActiveInput').checked = true;
// Show modal
const modal = new bootstrap.Modal(document.getElementById('createContactModal'));
modal.show();
}
async function createContact() {
const firstName = document.getElementById('firstNameInput').value.trim();
const lastName = document.getElementById('lastNameInput').value.trim();
if (!firstName || !lastName) {
alert('Fornavn og efternavn er påkrævet');
return;
}
// Get selected company IDs
const companySelect = document.getElementById('companySelect');
const companyIds = Array.from(companySelect.selectedOptions).map(opt => parseInt(opt.value));
const contactData = {
first_name: firstName,
last_name: lastName,
email: document.getElementById('emailInput').value.trim() || null,
phone: document.getElementById('phoneInput').value.trim() || null,
mobile: document.getElementById('mobileInput').value.trim() || null,
title: document.getElementById('titleInput').value.trim() || null,
department: document.getElementById('departmentInput').value.trim() || null,
company_ids: companyIds,
is_primary: document.getElementById('isPrimaryInput').checked,
role: document.getElementById('roleInput').value.trim() || null,
notes: document.getElementById('notesInput').value.trim() || null,
is_active: document.getElementById('isActiveInput').checked
};
try {
const response = await fetch('/api/v1/contacts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(contactData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Kunne ikke oprette kontakt');
}
const newContact = await response.json();
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('createContactModal'));
modal.hide();
// Reload contact list
await loadContacts();
// Show success message
alert('Kontakt oprettet succesfuldt!');
} catch (error) {
console.error('Failed to create contact:', error);
alert('Fejl ved oprettelse af kontakt: ' + error.message);
}
}
function getInitials(firstName, lastName) {
if (!firstName && !lastName) return '?';
const first = firstName ? firstName[0] : '';
const last = lastName ? lastName[0] : '';
return (first + last).toUpperCase();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}

View File

@ -0,0 +1,220 @@
"""
FastAPI dependencies for authentication and authorization
Adapted from OmniSync for BMC Hub
"""
from fastapi import Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Optional
from app.core.auth_service import AuthService
import logging
logger = logging.getLogger(__name__)
security = HTTPBearer()
async def get_current_user(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> dict:
"""
Dependency to get current authenticated user from JWT token
Usage:
@router.get("/endpoint")
async def my_endpoint(current_user: dict = Depends(get_current_user)):
...
"""
token = credentials.credentials
# Verify token
payload = AuthService.verify_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
# Get user ID
user_id = int(payload.get("sub"))
username = payload.get("username")
is_superadmin = payload.get("is_superadmin", False)
# Add IP address to user info
ip_address = request.client.host if request.client else None
# Get additional user details from database
from app.core.database import execute_query
user_details = execute_query(
"SELECT email, full_name FROM users WHERE id = %s",
(user_id,),
fetchone=True
)
return {
"id": user_id,
"username": username,
"email": user_details.get('email') if user_details else None,
"full_name": user_details.get('full_name') if user_details else None,
"is_superadmin": is_superadmin,
"ip_address": ip_address,
"permissions": AuthService.get_user_permissions(user_id)
}
async def get_optional_user(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
) -> Optional[dict]:
"""
Dependency to get current user if authenticated, None otherwise
Allows endpoints that work both with and without authentication
"""
if not credentials:
return None
try:
return await get_current_user(request, credentials)
except HTTPException:
return None
def require_permission(permission: str):
"""
Dependency factory to require specific permission
Usage:
@router.post("/products", dependencies=[Depends(require_permission("products.create"))])
async def create_product(...):
...
Or with user access:
@router.post("/products")
async def create_product(
current_user: dict = Depends(require_permission("products.create"))
):
...
"""
async def permission_checker(current_user: dict = Depends(get_current_user)) -> dict:
user_id = current_user["id"]
username = current_user["username"]
# Superadmins have all permissions
if current_user.get("is_superadmin"):
return current_user
# Check permission
if not AuthService.user_has_permission(user_id, permission):
logger.warning(
f"⚠️ Permission denied: {username} attempted {permission}"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing required permission: {permission}"
)
return current_user
return permission_checker
def require_superadmin(current_user: dict = Depends(get_current_user)) -> dict:
"""
Dependency to require superadmin access
Usage:
@router.post("/admin/users")
async def create_user(current_user: dict = Depends(require_superadmin)):
...
"""
if not current_user.get("is_superadmin"):
logger.warning(
f"⚠️ Superadmin required: {current_user['username']} attempted admin access"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Superadmin access required"
)
return current_user
def require_any_permission(*permissions: str):
"""
Dependency factory to require ANY of the specified permissions
Usage:
@router.get("/reports")
async def get_reports(
current_user: dict = Depends(require_any_permission("reports.view", "reports.admin"))
):
...
"""
async def permission_checker(current_user: dict = Depends(get_current_user)) -> dict:
user_id = current_user["id"]
# Superadmins have all permissions
if current_user.get("is_superadmin"):
return current_user
# Check if user has ANY of the permissions
for permission in permissions:
if AuthService.user_has_permission(user_id, permission):
return current_user
# None of the permissions matched
logger.warning(
f"⚠️ Permission denied: {current_user['username']} "
f"attempted one of: {', '.join(permissions)}"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing required permission. Need one of: {', '.join(permissions)}"
)
return permission_checker
def require_all_permissions(*permissions: str):
"""
Dependency factory to require ALL of the specified permissions
Usage:
@router.post("/sensitive-operation")
async def sensitive_op(
current_user: dict = Depends(require_all_permissions("admin.access", "data.export"))
):
...
"""
async def permission_checker(current_user: dict = Depends(get_current_user)) -> dict:
user_id = current_user["id"]
# Superadmins have all permissions
if current_user.get("is_superadmin"):
return current_user
# Check if user has ALL permissions
missing_permissions = []
for permission in permissions:
if not AuthService.user_has_permission(user_id, permission):
missing_permissions.append(permission)
if missing_permissions:
logger.warning(
f"⚠️ Permission denied: {current_user['username']} "
f"missing: {', '.join(missing_permissions)}"
)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing required permissions: {', '.join(missing_permissions)}"
)
return current_user
return permission_checker

313
app/core/auth_service.py Normal file
View File

@ -0,0 +1,313 @@
"""
Authentication Service - Håndterer login, JWT tokens, password hashing
Adapted from OmniSync for BMC Hub
"""
from typing import Optional, Dict, List
from datetime import datetime, timedelta
import hashlib
import secrets
import jwt
from app.core.database import execute_query, execute_insert, execute_update
from app.core.config import settings
import logging
logger = logging.getLogger(__name__)
# JWT Settings
SECRET_KEY = getattr(settings, 'JWT_SECRET_KEY', 'your-secret-key-change-in-production')
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 8 # 8 timer
class AuthService:
"""Service for authentication and authorization"""
@staticmethod
def hash_password(password: str) -> str:
"""
Hash password using SHA256
I produktion: Brug bcrypt eller argon2!
"""
return hashlib.sha256(password.encode()).hexdigest()
@staticmethod
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify password against hash"""
return AuthService.hash_password(plain_password) == hashed_password
@staticmethod
def create_access_token(user_id: int, username: str, is_superadmin: bool = False) -> str:
"""
Create JWT access token
Args:
user_id: User ID
username: Username
is_superadmin: Whether user is superadmin
Returns:
JWT token string
"""
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
jti = secrets.token_urlsafe(32) # JWT ID for token revocation
payload = {
"sub": str(user_id),
"username": username,
"is_superadmin": is_superadmin,
"exp": expire,
"iat": datetime.utcnow(),
"jti": jti
}
token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
# Store session for token revocation
execute_insert(
"""INSERT INTO sessions (user_id, token_jti, expires_at)
VALUES (%s, %s, %s)""",
(user_id, jti, expire)
)
return token
@staticmethod
def verify_token(token: str) -> Optional[Dict]:
"""
Verify and decode JWT token
Returns:
Dict with user info or None if invalid
"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# Check if token is revoked
jti = payload.get('jti')
if jti:
session = execute_query(
"SELECT revoked FROM sessions WHERE token_jti = %s",
(jti,),
fetchone=True
)
if session and session.get('revoked'):
logger.warning(f"⚠️ Revoked token used: {jti[:10]}...")
return None
return payload
except jwt.ExpiredSignatureError:
logger.warning("⚠️ Expired token")
return None
except jwt.InvalidTokenError as e:
logger.warning(f"⚠️ Invalid token: {e}")
return None
@staticmethod
def authenticate_user(username: str, password: str, ip_address: Optional[str] = None) -> Optional[Dict]:
"""
Authenticate user with username/password
Args:
username: Username
password: Plain text password
ip_address: Client IP address (for logging)
Returns:
User dict if successful, None otherwise
"""
# Get user
user = execute_query(
"""SELECT id, username, email, password_hash, full_name,
is_active, is_superadmin, failed_login_attempts, locked_until
FROM users
WHERE username = %s OR email = %s""",
(username, username),
fetchone=True
)
if not user:
logger.warning(f"❌ Login failed: User not found - {username}")
return None
# Check if account is active
if not user['is_active']:
logger.warning(f"❌ Login failed: Account disabled - {username}")
return None
# Check if account is locked
if user['locked_until']:
locked_until = user['locked_until']
if datetime.now() < locked_until:
logger.warning(f"❌ Login failed: Account locked - {username}")
return None
else:
# Unlock account
execute_update(
"UPDATE users SET locked_until = NULL, failed_login_attempts = 0 WHERE id = %s",
(user['id'],)
)
# Verify password
if not AuthService.verify_password(password, user['password_hash']):
# Increment failed attempts
failed_attempts = user['failed_login_attempts'] + 1
if failed_attempts >= 5:
# Lock account for 30 minutes
locked_until = datetime.now() + timedelta(minutes=30)
execute_update(
"""UPDATE users
SET failed_login_attempts = %s, locked_until = %s
WHERE id = %s""",
(failed_attempts, locked_until, user['id'])
)
logger.warning(f"🔒 Account locked due to failed attempts: {username}")
else:
execute_update(
"UPDATE users SET failed_login_attempts = %s WHERE id = %s",
(failed_attempts, user['id'])
)
logger.warning(f"❌ Login failed: Invalid password - {username} (attempt {failed_attempts})")
return None
# Success! Reset failed attempts and update last login
execute_update(
"""UPDATE users
SET failed_login_attempts = 0,
locked_until = NULL,
last_login_at = CURRENT_TIMESTAMP
WHERE id = %s""",
(user['id'],)
)
logger.info(f"✅ User logged in: {username} from IP: {ip_address}")
return {
'user_id': user['id'],
'username': user['username'],
'email': user['email'],
'full_name': user['full_name'],
'is_superadmin': bool(user['is_superadmin'])
}
@staticmethod
def revoke_token(jti: str, user_id: int):
"""Revoke a JWT token"""
execute_update(
"UPDATE sessions SET revoked = TRUE WHERE token_jti = %s AND user_id = %s",
(jti, user_id)
)
logger.info(f"🔒 Token revoked for user {user_id}")
@staticmethod
def get_user_permissions(user_id: int) -> List[str]:
"""
Get all permissions for a user (through their groups)
Args:
user_id: User ID
Returns:
List of permission codes
"""
# Check if user is superadmin first
user = execute_query(
"SELECT is_superadmin FROM users WHERE id = %s",
(user_id,),
fetchone=True
)
# Superadmins have all permissions
if user and user['is_superadmin']:
all_perms = execute_query("SELECT code FROM permissions")
return [p['code'] for p in all_perms] if all_perms else []
# Get permissions through groups
perms = execute_query("""
SELECT DISTINCT p.code
FROM permissions p
JOIN group_permissions gp ON p.id = gp.permission_id
JOIN user_groups ug ON gp.group_id = ug.group_id
WHERE ug.user_id = %s
""", (user_id,))
return [p['code'] for p in perms] if perms else []
@staticmethod
def user_has_permission(user_id: int, permission_code: str) -> bool:
"""
Check if user has specific permission
Args:
user_id: User ID
permission_code: Permission code (e.g., 'customers.view')
Returns:
True if user has permission
"""
# Superadmins have all permissions
user = execute_query(
"SELECT is_superadmin FROM users WHERE id = %s",
(user_id,),
fetchone=True
)
if user and user['is_superadmin']:
return True
# Check if user has permission through groups
result = execute_query("""
SELECT COUNT(*) as cnt
FROM permissions p
JOIN group_permissions gp ON p.id = gp.permission_id
JOIN user_groups ug ON gp.group_id = ug.group_id
WHERE ug.user_id = %s AND p.code = %s
""", (user_id, permission_code), fetchone=True)
return bool(result and result['cnt'] > 0)
@staticmethod
def create_user(
username: str,
email: str,
password: str,
full_name: Optional[str] = None,
is_superadmin: bool = False
) -> Optional[int]:
"""
Create a new user
Returns:
New user ID or None if failed
"""
password_hash = AuthService.hash_password(password)
user_id = execute_insert(
"""INSERT INTO users
(username, email, password_hash, full_name, is_superadmin)
VALUES (%s, %s, %s, %s, %s) RETURNING id""",
(username, email, password_hash, full_name, is_superadmin)
)
logger.info(f"👤 User created: {username} (ID: {user_id})")
return user_id
@staticmethod
def change_password(user_id: int, new_password: str):
"""Change user password"""
password_hash = AuthService.hash_password(new_password)
execute_update(
"UPDATE users SET password_hash = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
(password_hash, user_id)
)
# Revoke all existing sessions
execute_update(
"UPDATE sessions SET revoked = TRUE WHERE user_id = %s",
(user_id,)
)
logger.info(f"🔑 Password changed for user {user_id}")

View File

@ -55,19 +55,94 @@ def get_db():
release_db_connection(conn) release_db_connection(conn)
def execute_query(query: str, params: tuple = None, fetch: bool = True): def execute_query(query: str, params: tuple = None, fetchone: bool = False):
"""Execute a SQL query and return results""" """
Execute a SQL query and return results
Args:
query: SQL query string
params: Query parameters tuple
fetchone: If True, return single row dict, otherwise list of dicts
Returns:
Single dict if fetchone=True, otherwise list of dicts
"""
conn = get_db_connection() conn = get_db_connection()
try: try:
with conn.cursor(cursor_factory=RealDictCursor) as cursor: with conn.cursor(cursor_factory=RealDictCursor) as cursor:
cursor.execute(query, params) cursor.execute(query, params or ())
if fetch: if fetchone:
return cursor.fetchall() row = cursor.fetchone()
conn.commit() return dict(row) if row else None
return cursor.rowcount else:
rows = cursor.fetchall()
return [dict(row) for row in rows]
except Exception as e: except Exception as e:
conn.rollback() conn.rollback()
logger.error(f"Query error: {e}") logger.error(f"Query error: {e}")
raise raise
finally: finally:
release_db_connection(conn) release_db_connection(conn)
def execute_insert(query: str, params: tuple = ()) -> Optional[int]:
"""
Execute an INSERT query and return last row id
Args:
query: SQL INSERT query (will add RETURNING id if not present)
params: Query parameters tuple
Returns:
Last inserted row ID or None
"""
conn = get_db_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
# PostgreSQL requires RETURNING clause
if "RETURNING" not in query.upper():
query = query.rstrip(";") + " RETURNING id"
cursor.execute(query, params)
result = cursor.fetchone()
conn.commit()
# If result exists, return the first column value (typically ID)
if result:
# If it's a dict, get first value
if isinstance(result, dict):
return list(result.values())[0]
# If it's a tuple/list, get first element
return result[0]
return None
except Exception as e:
conn.rollback()
logger.error(f"Insert error: {e}")
raise
finally:
release_db_connection(conn)
def execute_update(query: str, params: tuple = ()) -> int:
"""
Execute an UPDATE/DELETE query and return affected rows
Args:
query: SQL UPDATE/DELETE query
params: Query parameters tuple
Returns:
Number of affected rows
"""
conn = get_db_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
cursor.execute(query, params)
rowcount = cursor.rowcount
conn.commit()
return rowcount
except Exception as e:
conn.rollback()
logger.error(f"Update error: {e}")
raise
finally:
release_db_connection(conn)

View File

@ -0,0 +1,357 @@
"""
Customers Router
API endpoints for customer management
Adapted from OmniSync for BMC Hub
"""
from fastapi import APIRouter, HTTPException, Query
from typing import List, Optional, Dict
from pydantic import BaseModel
import logging
from app.core.database import execute_query, execute_insert, execute_update
from app.services.cvr_service import get_cvr_service
logger = logging.getLogger(__name__)
router = APIRouter()
# Pydantic Models
class CustomerBase(BaseModel):
name: str
cvr_number: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
address: Optional[str] = None
city: Optional[str] = None
postal_code: Optional[str] = None
country: Optional[str] = "DK"
website: Optional[str] = None
is_active: Optional[bool] = True
invoice_email: Optional[str] = None
mobile_phone: Optional[str] = None
class CustomerCreate(CustomerBase):
pass
class CustomerUpdate(BaseModel):
name: Optional[str] = None
cvr_number: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
address: Optional[str] = None
city: Optional[str] = None
postal_code: Optional[str] = None
country: Optional[str] = None
website: Optional[str] = None
is_active: Optional[bool] = None
invoice_email: Optional[str] = None
mobile_phone: Optional[str] = None
class ContactCreate(BaseModel):
first_name: str
last_name: str
email: Optional[str] = None
phone: Optional[str] = None
mobile: Optional[str] = None
title: Optional[str] = None
department: Optional[str] = None
is_primary: Optional[bool] = False
role: Optional[str] = None
@router.get("/customers")
async def list_customers(
limit: int = Query(default=50, ge=1, le=1000),
offset: int = Query(default=0, ge=0),
search: Optional[str] = Query(default=None),
source: Optional[str] = Query(default=None), # 'vtiger', 'local', or None
is_active: Optional[bool] = Query(default=None)
):
"""
List customers with pagination and filtering
Args:
limit: Maximum number of customers to return
offset: Number of customers to skip
search: Search term for name, email, cvr, phone, city
source: Filter by source ('vtiger' or 'local')
is_active: Filter by active status
"""
# Build query
query = """
SELECT
c.*,
COUNT(DISTINCT cc.contact_id) as contact_count
FROM customers c
LEFT JOIN contact_companies cc ON cc.customer_id = c.id
WHERE 1=1
"""
params = []
# Add search filter
if search:
query += """ AND (
c.name ILIKE %s OR
c.email ILIKE %s OR
c.cvr_number ILIKE %s OR
c.phone ILIKE %s OR
c.city ILIKE %s
)"""
search_term = f"%{search}%"
params.extend([search_term] * 5)
# Add source filter
if source == 'vtiger':
query += " AND c.vtiger_id IS NOT NULL"
elif source == 'local':
query += " AND c.vtiger_id IS NULL"
# Add active filter
if is_active is not None:
query += " AND c.is_active = %s"
params.append(is_active)
query += """
GROUP BY c.id
ORDER BY c.name
LIMIT %s OFFSET %s
"""
params.extend([limit, offset])
rows = execute_query(query, tuple(params))
# Get total count
count_query = "SELECT COUNT(*) as total FROM customers WHERE 1=1"
count_params = []
if search:
count_query += """ AND (
name ILIKE %s OR
email ILIKE %s OR
cvr_number ILIKE %s OR
phone ILIKE %s OR
city ILIKE %s
)"""
count_params.extend([search_term] * 5)
if source == 'vtiger':
count_query += " AND vtiger_id IS NOT NULL"
elif source == 'local':
count_query += " AND vtiger_id IS NULL"
if is_active is not None:
count_query += " AND is_active = %s"
count_params.append(is_active)
count_result = execute_query(count_query, tuple(count_params), fetchone=True)
total = count_result['total'] if count_result else 0
return {
"customers": rows or [],
"total": total,
"limit": limit,
"offset": offset
}
@router.get("/customers/{customer_id}")
async def get_customer(customer_id: int):
"""Get single customer by ID with contact count"""
# Get customer
customer = execute_query(
"SELECT * FROM customers WHERE id = %s",
(customer_id,),
fetchone=True
)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Get contact count
contact_count_result = execute_query(
"SELECT COUNT(*) as count FROM contact_companies WHERE customer_id = %s",
(customer_id,),
fetchone=True
)
contact_count = contact_count_result['count'] if contact_count_result else 0
return {
**customer,
'contact_count': contact_count
}
@router.post("/customers")
async def create_customer(customer: CustomerCreate):
"""Create a new customer"""
try:
customer_id = execute_insert(
"""INSERT INTO customers
(name, cvr_number, email, phone, address, city, postal_code,
country, website, is_active, invoice_email, mobile_phone)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id""",
(
customer.name,
customer.cvr_number,
customer.email,
customer.phone,
customer.address,
customer.city,
customer.postal_code,
customer.country,
customer.website,
customer.is_active,
customer.invoice_email,
customer.mobile_phone
)
)
logger.info(f"✅ Created customer {customer_id}: {customer.name}")
# Fetch and return created customer
created = execute_query(
"SELECT * FROM customers WHERE id = %s",
(customer_id,),
fetchone=True
)
return created
except Exception as e:
logger.error(f"❌ Failed to create customer: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.put("/customers/{customer_id}")
async def update_customer(customer_id: int, update: CustomerUpdate):
"""Update customer information"""
# Verify customer exists
existing = execute_query(
"SELECT id FROM customers WHERE id = %s",
(customer_id,),
fetchone=True
)
if not existing:
raise HTTPException(status_code=404, detail="Customer not found")
# Build dynamic UPDATE query
updates = []
params = []
update_dict = update.dict(exclude_unset=True)
for field, value in update_dict.items():
updates.append(f"{field} = %s")
params.append(value)
if not updates:
raise HTTPException(status_code=400, detail="No fields to update")
params.append(customer_id)
query = f"UPDATE customers SET {', '.join(updates)}, updated_at = CURRENT_TIMESTAMP WHERE id = %s"
try:
execute_update(query, tuple(params))
logger.info(f"✅ Updated customer {customer_id}")
# Fetch and return updated customer
updated = execute_query(
"SELECT * FROM customers WHERE id = %s",
(customer_id,),
fetchone=True
)
return updated
except Exception as e:
logger.error(f"❌ Failed to update customer {customer_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/customers/{customer_id}/contacts")
async def get_customer_contacts(customer_id: int):
"""Get all contacts for a specific customer"""
rows = execute_query("""
SELECT
c.*,
cc.is_primary,
cc.role,
cc.notes
FROM contacts c
JOIN contact_companies cc ON c.id = cc.contact_id
WHERE cc.customer_id = %s AND c.is_active = TRUE
ORDER BY cc.is_primary DESC, c.first_name, c.last_name
""", (customer_id,))
return rows or []
@router.post("/customers/{customer_id}/contacts")
async def create_customer_contact(customer_id: int, contact: ContactCreate):
"""Create a new contact for a customer"""
# Verify customer exists
customer = execute_query(
"SELECT id FROM customers WHERE id = %s",
(customer_id,),
fetchone=True
)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
try:
# Create contact
contact_id = execute_insert(
"""INSERT INTO contacts
(first_name, last_name, email, phone, mobile, title, department)
VALUES (%s, %s, %s, %s, %s, %s, %s)
RETURNING id""",
(
contact.first_name,
contact.last_name,
contact.email,
contact.phone,
contact.mobile,
contact.title,
contact.department
)
)
# Link contact to customer
execute_insert(
"""INSERT INTO contact_companies
(contact_id, customer_id, is_primary, role)
VALUES (%s, %s, %s, %s)""",
(contact_id, customer_id, contact.is_primary, contact.role)
)
logger.info(f"✅ Created contact {contact_id} for customer {customer_id}")
# Fetch and return created contact
created = execute_query(
"SELECT * FROM contacts WHERE id = %s",
(contact_id,),
fetchone=True
)
return created
except Exception as e:
logger.error(f"❌ Failed to create contact: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/cvr/{cvr_number}")
async def lookup_cvr(cvr_number: str):
"""Lookup company information by CVR number"""
cvr_service = get_cvr_service()
result = await cvr_service.lookup_by_cvr(cvr_number)
if not result:
raise HTTPException(status_code=404, detail="CVR number not found")
return result

View File

@ -0,0 +1,24 @@
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
router = APIRouter()
templates = Jinja2Templates(directory="app")
@router.get("/customers", response_class=HTMLResponse)
async def customers_page(request: Request):
"""
Render the customers list page
"""
return templates.TemplateResponse("customers/frontend/customers.html", {"request": request})
@router.get("/customers/{customer_id}", response_class=HTMLResponse)
async def customer_detail_page(request: Request, customer_id: int):
"""
Render the customer detail page
"""
return templates.TemplateResponse("customers/frontend/customer_detail.html", {
"request": request,
"customer_id": customer_id
})

View File

@ -0,0 +1,494 @@
{% extends "shared/frontend/base.html" %}
{% block title %}Kunde Detaljer - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.customer-header {
background: var(--accent);
color: white;
padding: 3rem 2rem;
border-radius: 12px;
margin-bottom: 2rem;
}
.customer-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);
}
.nav-pills-vertical {
border-right: 1px solid rgba(0, 0, 0, 0.1);
padding-right: 0;
}
.nav-pills-vertical .nav-link {
color: var(--text-secondary);
border-radius: 8px 0 0 8px;
padding: 1rem 1.5rem;
font-weight: 500;
margin-bottom: 0.5rem;
transition: all 0.2s;
text-align: left;
}
.nav-pills-vertical .nav-link:hover {
background: var(--accent-light);
color: var(--accent);
}
.nav-pills-vertical .nav-link.active {
background: var(--accent);
color: white;
}
.nav-pills-vertical .nav-link i {
width: 20px;
margin-right: 0.75rem;
}
.info-card {
background: var(--bg-card);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: 500;
color: var(--text-secondary);
}
.info-value {
color: var(--text-primary);
font-weight: 500;
}
.contact-card {
background: var(--bg-card);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.2s;
}
.contact-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.activity-item {
padding: 1.5rem;
border-left: 3px solid var(--accent-light);
margin-left: 1rem;
position: relative;
}
.activity-item::before {
content: '';
position: absolute;
left: -8px;
top: 1.5rem;
width: 12px;
height: 12px;
background: var(--accent);
border-radius: 50%;
border: 3px solid var(--bg-body);
}
</style>
{% endblock %}
{% block content %}
<!-- Customer Header -->
<div class="customer-header">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex align-items-center">
<div class="customer-avatar-large me-4" id="customerAvatar">?</div>
<div>
<h1 class="fw-bold mb-2" id="customerName">Loading...</h1>
<div class="d-flex gap-3 align-items-center">
<span id="customerCity"></span>
<span class="badge bg-white bg-opacity-20" id="customerStatus"></span>
<span class="badge bg-white bg-opacity-20" id="customerSource"></span>
</div>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-light btn-sm" onclick="editCustomer()">
<i class="bi bi-pencil me-2"></i>Rediger
</button>
<button class="btn btn-light btn-sm" onclick="window.location.href='/customers'">
<i class="bi bi-arrow-left me-2"></i>Tilbage
</button>
</div>
</div>
</div>
<!-- Content Layout with Sidebar Navigation -->
<div class="row">
<div class="col-lg-3 col-md-4">
<!-- Vertical Navigation -->
<ul class="nav nav-pills nav-pills-vertical flex-column" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#overview">
<i class="bi bi-info-circle"></i>Oversigt
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#contacts">
<i class="bi bi-people"></i>Kontakter
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#invoices">
<i class="bi bi-receipt"></i>Fakturaer
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#hardware">
<i class="bi bi-hdd"></i>Hardware
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#activity">
<i class="bi bi-clock-history"></i>Aktivitet
</a>
</li>
</ul>
</div>
<div class="col-lg-9 col-md-8">
<!-- Tab Content -->
<div class="tab-content">
<!-- Overview Tab -->
<div class="tab-pane fade show active" id="overview">
<div class="row g-4">
<div class="col-lg-6">
<div class="info-card">
<h5 class="fw-bold mb-4">Virksomhedsoplysninger</h5>
<div class="info-row">
<span class="info-label">CVR-nummer</span>
<span class="info-value" id="cvrNumber">-</span>
</div>
<div class="info-row">
<span class="info-label">Adresse</span>
<span class="info-value" id="address">-</span>
</div>
<div class="info-row">
<span class="info-label">Postnummer & By</span>
<span class="info-value" id="postalCity">-</span>
</div>
<div class="info-row">
<span class="info-label">Email</span>
<span class="info-value" id="email">-</span>
</div>
<div class="info-row">
<span class="info-label">Telefon</span>
<span class="info-value" id="phone">-</span>
</div>
<div class="info-row">
<span class="info-label">Hjemmeside</span>
<span class="info-value" id="website">-</span>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="info-card">
<h5 class="fw-bold mb-4">Økonomiske Oplysninger</h5>
<div class="info-row">
<span class="info-label">e-conomic Kundenr.</span>
<span class="info-value" id="economicNumber">-</span>
</div>
<div class="info-row">
<span class="info-label">Betalingsbetingelser</span>
<span class="info-value" id="paymentTerms">-</span>
</div>
<div class="info-row">
<span class="info-label">Moms Zone</span>
<span class="info-value" id="vatZone">-</span>
</div>
<div class="info-row">
<span class="info-label">Valuta</span>
<span class="info-value" id="currency">-</span>
</div>
<div class="info-row">
<span class="info-label">EAN-nummer</span>
<span class="info-value" id="ean">-</span>
</div>
<div class="info-row">
<span class="info-label">Spærret</span>
<span class="info-value" id="barred">-</span>
</div>
</div>
</div>
<div class="col-12">
<div class="info-card">
<h5 class="fw-bold mb-3">Integration</h5>
<div class="row">
<div class="col-md-6">
<div class="info-row">
<span class="info-label">vTiger ID</span>
<span class="info-value" id="vtigerId">-</span>
</div>
<div class="info-row">
<span class="info-label">vTiger Sidst Synkroniseret</span>
<span class="info-value" id="vtigerSyncAt">-</span>
</div>
</div>
<div class="col-md-6">
<div class="info-row">
<span class="info-label">e-conomic Sidst Synkroniseret</span>
<span class="info-value" id="economicSyncAt">-</span>
</div>
<div class="info-row">
<span class="info-label">Oprettet</span>
<span class="info-value" id="createdAt">-</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Contacts Tab -->
<div class="tab-pane fade" id="contacts">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-0">Kontaktpersoner</h5>
<button class="btn btn-primary btn-sm" onclick="showAddContactModal()">
<i class="bi bi-plus-lg me-2"></i>Tilføj Kontakt
</button>
</div>
<div class="row g-4" id="contactsContainer">
<div class="col-12 text-center py-5">
<div class="spinner-border text-primary"></div>
</div>
</div>
</div>
<!-- Invoices Tab -->
<div class="tab-pane fade" id="invoices">
<h5 class="fw-bold mb-4">Fakturaer</h5>
<div class="text-muted text-center py-5">
Fakturamodul kommer snart...
</div>
</div>
<!-- Hardware Tab -->
<div class="tab-pane fade" id="hardware">
<h5 class="fw-bold mb-4">Hardware</h5>
<div class="text-muted text-center py-5">
Hardwaremodul kommer snart...
</div>
</div>
<!-- Activity Tab -->
<div class="tab-pane fade" id="activity">
<h5 class="fw-bold mb-4">Aktivitet</h5>
<div id="activityContainer">
<div class="text-center py-5">
<div class="spinner-border text-primary"></div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const customerId = parseInt(window.location.pathname.split('/').pop());
let customerData = null;
document.addEventListener('DOMContentLoaded', () => {
loadCustomer();
// Load contacts when tab is shown
document.querySelector('a[href="#contacts"]').addEventListener('shown.bs.tab', () => {
loadContacts();
});
// Load activity when tab is shown
document.querySelector('a[href="#activity"]').addEventListener('shown.bs.tab', () => {
loadActivity();
});
});
async function loadCustomer() {
try {
const response = await fetch(`/api/v1/customers/${customerId}`);
if (!response.ok) {
throw new Error('Customer not found');
}
customerData = await response.json();
displayCustomer(customerData);
} catch (error) {
console.error('Failed to load customer:', error);
alert('Kunne ikke indlæse kunde');
window.location.href = '/customers';
}
}
function displayCustomer(customer) {
// Update page title
document.title = `${customer.name} - BMC Hub`;
// Header
document.getElementById('customerAvatar').textContent = getInitials(customer.name);
document.getElementById('customerName').textContent = customer.name;
document.getElementById('customerCity').textContent = customer.city || '';
const statusBadge = customer.is_active
? '<i class="bi bi-check-circle me-1"></i>Aktiv'
: '<i class="bi bi-x-circle me-1"></i>Inaktiv';
document.getElementById('customerStatus').innerHTML = statusBadge;
const sourceBadge = customer.vtiger_id
? '<i class="bi bi-cloud me-1"></i>vTiger'
: '<i class="bi bi-hdd me-1"></i>Lokal';
document.getElementById('customerSource').innerHTML = sourceBadge;
// Company Information
document.getElementById('cvrNumber').textContent = customer.cvr_number || '-';
document.getElementById('address').textContent = customer.address || '-';
document.getElementById('postalCity').textContent = customer.postal_code && customer.city
? `${customer.postal_code} ${customer.city}`
: customer.city || '-';
document.getElementById('email').textContent = customer.email || '-';
document.getElementById('phone').textContent = customer.phone || '-';
document.getElementById('website').textContent = customer.website || '-';
// Economic Information
document.getElementById('economicNumber').textContent = customer.economic_customer_number || '-';
document.getElementById('paymentTerms').textContent = customer.payment_terms_days
? `${customer.payment_terms_days} dage netto`
: '-';
document.getElementById('vatZone').textContent = customer.vat_zone || '-';
document.getElementById('currency').textContent = customer.currency_code || 'DKK';
document.getElementById('ean').textContent = customer.ean || '-';
document.getElementById('barred').innerHTML = customer.barred
? '<span class="badge bg-danger">Ja</span>'
: '<span class="badge bg-success">Nej</span>';
// Integration
document.getElementById('vtigerId').textContent = customer.vtiger_id || '-';
document.getElementById('vtigerSyncAt').textContent = customer.vtiger_last_sync_at
? new Date(customer.vtiger_last_sync_at).toLocaleString('da-DK')
: '-';
document.getElementById('economicSyncAt').textContent = customer.economic_last_sync_at
? new Date(customer.economic_last_sync_at).toLocaleString('da-DK')
: '-';
document.getElementById('createdAt').textContent = new Date(customer.created_at).toLocaleString('da-DK');
}
async function loadContacts() {
const container = document.getElementById('contactsContainer');
container.innerHTML = '<div class="col-12 text-center py-5"><div class="spinner-border text-primary"></div></div>';
try {
const response = await fetch(`/api/v1/customers/${customerId}/contacts`);
const contacts = await response.json();
if (!contacts || contacts.length === 0) {
container.innerHTML = '<div class="col-12 text-center py-5 text-muted">Ingen kontakter endnu</div>';
return;
}
container.innerHTML = contacts.map(contact => `
<div class="col-md-6">
<div class="contact-card">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="fw-bold mb-1">${escapeHtml(contact.name)}</h6>
<div class="text-muted small">${contact.title || 'Kontakt'}</div>
</div>
${contact.is_primary ? '<span class="badge bg-primary">Primær</span>' : ''}
</div>
<div class="d-flex flex-column gap-2">
${contact.email ? `
<div class="d-flex align-items-center">
<i class="bi bi-envelope me-2 text-muted"></i>
<a href="mailto:${contact.email}">${contact.email}</a>
</div>
` : ''}
${contact.phone ? `
<div class="d-flex align-items-center">
<i class="bi bi-telephone me-2 text-muted"></i>
<a href="tel:${contact.phone}">${contact.phone}</a>
</div>
` : ''}
${contact.mobile ? `
<div class="d-flex align-items-center">
<i class="bi bi-phone me-2 text-muted"></i>
<a href="tel:${contact.mobile}">${contact.mobile}</a>
</div>
` : ''}
</div>
</div>
</div>
`).join('');
} catch (error) {
console.error('Failed to load contacts:', error);
container.innerHTML = '<div class="col-12 text-center py-5 text-danger">Kunne ikke indlæse kontakter</div>';
}
}
async function loadActivity() {
const container = document.getElementById('activityContainer');
container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary"></div></div>';
// TODO: Implement activity log API endpoint
setTimeout(() => {
container.innerHTML = '<div class="text-muted text-center py-5">Ingen aktivitet at vise</div>';
}, 500);
}
function editCustomer() {
// TODO: Open edit modal with pre-filled data
console.log('Edit customer:', customerId);
}
function showAddContactModal() {
// TODO: Open add contact modal
console.log('Add contact for customer:', customerId);
}
function getInitials(name) {
if (!name) return '?';
const words = name.trim().split(' ');
if (words.length === 1) return words[0].substring(0, 2).toUpperCase();
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}

View File

@ -0,0 +1,502 @@
{% extends "shared/frontend/base.html" %}
{% block title %}Kunder - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.filter-btn {
background: var(--bg-card);
border: 1px solid rgba(0,0,0,0.1);
color: var(--text-secondary);
padding: 0.5rem 1.2rem;
border-radius: 20px;
font-size: 0.9rem;
transition: all 0.2s;
cursor: pointer;
}
.filter-btn:hover, .filter-btn.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.customer-avatar {
width: 40px;
height: 40px;
border-radius: 8px;
background: var(--accent-light);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
}
.pagination-btn {
border: 1px solid rgba(0,0,0,0.1);
padding: 0.5rem 1rem;
background: var(--bg-card);
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s;
}
.pagination-btn:hover:not(:disabled) {
background: var(--accent-light);
border-color: var(--accent);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Kunder</h2>
<p class="text-muted mb-0">Administrer dine kunder</p>
</div>
<div class="d-flex gap-3">
<input type="text" id="searchInput" class="header-search" placeholder="Søg kunde, CVR, email...">
<button class="btn btn-primary" onclick="showCreateCustomerModal()">
<i class="bi bi-plus-lg me-2"></i>Opret Kunde
</button>
</div>
</div>
<div class="mb-4 d-flex gap-2 flex-wrap">
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">
Alle Kunder <span id="countAll" class="ms-1"></span>
</button>
<button class="filter-btn" data-filter="active" onclick="setFilter('active')">
Aktive <span id="countActive" class="ms-1"></span>
</button>
<button class="filter-btn" data-filter="inactive" onclick="setFilter('inactive')">
Inaktive <span id="countInactive" class="ms-1"></span>
</button>
<button class="filter-btn" data-filter="vtiger" onclick="setFilter('vtiger')">
<i class="bi bi-cloud me-1"></i>vTiger
</button>
<button class="filter-btn" data-filter="local" onclick="setFilter('local')">
<i class="bi bi-hdd me-1"></i>Lokal
</button>
</div>
<div class="card p-4">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Virksomhed</th>
<th>Kontakt Info</th>
<th>CVR</th>
<th>Kilde</th>
<th>Status</th>
<th>Kontakter</th>
<th class="text-end">Handlinger</th>
</tr>
</thead>
<tbody id="customersTableBody">
<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>
<!-- Pagination -->
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted small">
Viser <span id="showingStart">0</span>-<span id="showingEnd">0</span> af <span id="totalCount">0</span> kunder
</div>
<div class="d-flex gap-2">
<button class="pagination-btn" id="prevBtn" onclick="previousPage()">
<i class="bi bi-chevron-left"></i> Forrige
</button>
<button class="pagination-btn" id="nextBtn" onclick="nextPage()">
Næste <i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
</div>
<!-- Create Customer Modal -->
<div class="modal fade" id="createCustomerModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Opret Ny Kunde</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createCustomerForm">
<!-- CVR Lookup Section -->
<div class="mb-4">
<label class="form-label">CVR-nummer</label>
<div class="input-group">
<input type="text" class="form-control" id="cvrInput" placeholder="12345678" maxlength="8">
<button class="btn btn-primary" type="button" id="cvrLookupBtn" onclick="lookupCVR()">
<i class="bi bi-search me-2"></i>Søg CVR
</button>
</div>
<div class="form-text">Indtast CVR-nummer for automatisk udfyldning</div>
<div id="cvrLookupStatus" class="mt-2"></div>
</div>
<hr class="my-4">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Virksomhedsnavn <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="nameInput" required>
</div>
<div class="col-md-8">
<label class="form-label">Adresse</label>
<input type="text" class="form-control" id="addressInput">
</div>
<div class="col-md-4">
<label class="form-label">Postnummer</label>
<input type="text" class="form-control" id="postalCodeInput">
</div>
<div class="col-md-6">
<label class="form-label">By</label>
<input type="text" class="form-control" id="cityInput">
</div>
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="emailInput">
</div>
<div class="col-md-6">
<label class="form-label">Telefon</label>
<input type="text" class="form-control" id="phoneInput">
</div>
<div class="col-md-6">
<label class="form-label">Hjemmeside</label>
<input type="url" class="form-control" id="websiteInput" placeholder="https://">
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isActiveInput" checked>
<label class="form-check-label" for="isActiveInput">
Aktiv kunde
</label>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" onclick="createCustomer()">
<i class="bi bi-plus-lg me-2"></i>Opret Kunde
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 0;
let pageSize = 20;
let currentFilter = 'all';
let searchQuery = '';
let totalCustomers = 0;
// Load customers on page load
document.addEventListener('DOMContentLoaded', () => {
loadCustomers();
// Search with debounce
let searchTimeout;
document.getElementById('searchInput').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchQuery = e.target.value;
currentPage = 0;
loadCustomers();
}, 300);
});
});
function setFilter(filter) {
currentFilter = filter;
currentPage = 0;
// Update active button
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
loadCustomers();
}
async function loadCustomers() {
const tbody = document.getElementById('customersTableBody');
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5"><div class="spinner-border text-primary"></div></td></tr>';
try {
// Build query parameters
let params = new URLSearchParams({
limit: pageSize,
offset: currentPage * pageSize
});
if (searchQuery) {
params.append('search', searchQuery);
}
if (currentFilter === 'active') {
params.append('is_active', 'true');
} else if (currentFilter === 'inactive') {
params.append('is_active', 'false');
} else if (currentFilter === 'vtiger' || currentFilter === 'local') {
params.append('source', currentFilter);
}
const response = await fetch(`/api/v1/customers?${params}`);
const data = await response.json();
totalCustomers = data.total;
displayCustomers(data.customers);
updatePagination(data.total);
} catch (error) {
console.error('Failed to load customers:', error);
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-danger">Kunne ikke indlæse kunder</td></tr>';
}
}
function displayCustomers(customers) {
const tbody = document.getElementById('customersTableBody');
if (!customers || customers.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">Ingen kunder fundet</td></tr>';
return;
}
tbody.innerHTML = customers.map(customer => {
const initials = getInitials(customer.name);
const statusBadge = customer.is_active
? '<span class="badge bg-success bg-opacity-10 text-success">Aktiv</span>'
: '<span class="badge bg-secondary bg-opacity-10 text-secondary">Inaktiv</span>';
const sourceBadge = customer.vtiger_id
? '<span class="badge bg-primary bg-opacity-10 text-primary"><i class="bi bi-cloud me-1"></i>vTiger</span>'
: '<span class="badge bg-secondary bg-opacity-10 text-secondary"><i class="bi bi-hdd me-1"></i>Lokal</span>';
const contactCount = customer.contact_count || 0;
return `
<tr style="cursor: pointer;" onclick="viewCustomer(${customer.id})">
<td>
<div class="d-flex align-items-center">
<div class="customer-avatar me-3">${initials}</div>
<div>
<div class="fw-bold">${escapeHtml(customer.name)}</div>
<div class="small text-muted">${customer.city || customer.address || '-'}</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">${customer.email || '-'}</div>
<div class="small text-muted">${customer.phone || '-'}</div>
</td>
<td class="text-muted">${customer.cvr_number || '-'}</td>
<td>${sourceBadge}</td>
<td>${statusBadge}</td>
<td>
<span class="badge bg-light text-dark border">
<i class="bi bi-person me-1"></i>${contactCount}
</span>
</td>
<td class="text-end">
<div class="btn-group">
<button class="btn btn-sm btn-light" onclick="event.stopPropagation(); viewCustomer(${customer.id})">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-light" onclick="event.stopPropagation(); editCustomer(${customer.id})">
<i class="bi bi-pencil"></i>
</button>
</div>
</td>
</tr>
`;
}).join('');
}
function updatePagination(total) {
const start = currentPage * pageSize + 1;
const end = Math.min((currentPage + 1) * pageSize, total);
document.getElementById('showingStart').textContent = total > 0 ? start : 0;
document.getElementById('showingEnd').textContent = end;
document.getElementById('totalCount').textContent = total;
// Update buttons
document.getElementById('prevBtn').disabled = currentPage === 0;
document.getElementById('nextBtn').disabled = end >= total;
}
function previousPage() {
if (currentPage > 0) {
currentPage--;
loadCustomers();
}
}
function nextPage() {
if ((currentPage + 1) * pageSize < totalCustomers) {
currentPage++;
loadCustomers();
}
}
function viewCustomer(customerId) {
window.location.href = `/customers/${customerId}`;
}
function editCustomer(customerId) {
// TODO: Open edit modal
console.log('Edit customer:', customerId);
}
function showCreateCustomerModal() {
// Reset form
document.getElementById('createCustomerForm').reset();
document.getElementById('cvrLookupStatus').innerHTML = '';
document.getElementById('isActiveInput').checked = true;
// Show modal
const modal = new bootstrap.Modal(document.getElementById('createCustomerModal'));
modal.show();
}
async function lookupCVR() {
const cvrInput = document.getElementById('cvrInput');
const cvr = cvrInput.value.trim();
const statusDiv = document.getElementById('cvrLookupStatus');
const lookupBtn = document.getElementById('cvrLookupBtn');
if (!cvr || cvr.length !== 8) {
statusDiv.innerHTML = '<div class="alert alert-warning mb-0"><i class="bi bi-exclamation-triangle me-2"></i>Indtast et gyldigt 8-cifret CVR-nummer</div>';
return;
}
// Show loading state
lookupBtn.disabled = true;
lookupBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Søger...';
statusDiv.innerHTML = '<div class="alert alert-info mb-0"><i class="bi bi-hourglass-split me-2"></i>Henter virksomhedsoplysninger...</div>';
try {
const response = await fetch(`/api/v1/cvr/${cvr}`);
if (!response.ok) {
throw new Error('CVR ikke fundet');
}
const data = await response.json();
// Auto-fill form fields
document.getElementById('nameInput').value = data.name || '';
document.getElementById('addressInput').value = data.address || '';
document.getElementById('postalCodeInput').value = data.postal_code || '';
document.getElementById('cityInput').value = data.city || '';
document.getElementById('phoneInput').value = data.phone || '';
document.getElementById('emailInput').value = data.email || '';
statusDiv.innerHTML = '<div class="alert alert-success mb-0"><i class="bi bi-check-circle me-2"></i>Virksomhedsoplysninger hentet fra CVR-registeret</div>';
} catch (error) {
console.error('CVR lookup failed:', error);
statusDiv.innerHTML = '<div class="alert alert-danger mb-0"><i class="bi bi-x-circle me-2"></i>Kunne ikke finde virksomhed med CVR-nummer ' + cvr + '</div>';
} finally {
lookupBtn.disabled = false;
lookupBtn.innerHTML = '<i class="bi bi-search me-2"></i>Søg CVR';
}
}
async function createCustomer() {
const name = document.getElementById('nameInput').value.trim();
if (!name) {
alert('Virksomhedsnavn er påkrævet');
return;
}
const customerData = {
name: name,
cvr_number: document.getElementById('cvrInput').value.trim() || null,
address: document.getElementById('addressInput').value.trim() || null,
postal_code: document.getElementById('postalCodeInput').value.trim() || null,
city: document.getElementById('cityInput').value.trim() || null,
email: document.getElementById('emailInput').value.trim() || null,
phone: document.getElementById('phoneInput').value.trim() || null,
website: document.getElementById('websiteInput').value.trim() || null,
is_active: document.getElementById('isActiveInput').checked
};
try {
const response = await fetch('/api/v1/customers', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(customerData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Kunne ikke oprette kunde');
}
const newCustomer = await response.json();
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('createCustomerModal'));
modal.hide();
// Reload customer list
await loadCustomers();
// Show success message (optional)
alert('Kunde oprettet succesfuldt!');
} catch (error) {
console.error('Failed to create customer:', error);
alert('Fejl ved oprettelse af kunde: ' + error.message);
}
}
function getInitials(name) {
if (!name) return '?';
const words = name.trim().split(' ');
if (words.length === 1) return words[0].substring(0, 2).toUpperCase();
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}

View File

@ -0,0 +1,13 @@
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
router = APIRouter()
templates = Jinja2Templates(directory="app")
@router.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
"""
Render the dashboard page
"""
return templates.TemplateResponse("dashboard/frontend/index.html", {"request": request})

View File

@ -0,0 +1,131 @@
{% extends "shared/frontend/base.html" %}
{% block title %}Dashboard - BMC Hub{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Dashboard</h2>
<p class="text-muted mb-0">Velkommen tilbage, Christian</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="header-search" placeholder="Søg...">
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Ny Opgave</button>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-3">
<div class="card stat-card p-4 h-100">
<div class="d-flex justify-content-between mb-2">
<p>Aktive Kunder</p>
<i class="bi bi-people text-primary" style="color: var(--accent) !important;"></i>
</div>
<h3>124</h3>
<small class="text-success"><i class="bi bi-arrow-up-short"></i> 12% denne måned</small>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card p-4 h-100">
<div class="d-flex justify-content-between mb-2">
<p>Hardware</p>
<i class="bi bi-hdd text-primary" style="color: var(--accent) !important;"></i>
</div>
<h3>856</h3>
<small class="text-muted">Enheder online</small>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card p-4 h-100">
<div class="d-flex justify-content-between mb-2">
<p>Support</p>
<i class="bi bi-ticket text-primary" style="color: var(--accent) !important;"></i>
</div>
<h3>12</h3>
<small class="text-warning">3 kræver handling</small>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card p-4 h-100">
<div class="d-flex justify-content-between mb-2">
<p>Omsætning</p>
<i class="bi bi-currency-dollar text-primary" style="color: var(--accent) !important;"></i>
</div>
<h3>450k</h3>
<small class="text-success">Over budget</small>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card p-4">
<h5 class="fw-bold mb-4">Seneste Aktiviteter</h5>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Kunde</th>
<th>Handling</th>
<th>Status</th>
<th class="text-end">Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td class="fw-bold">Advokatgruppen A/S</td>
<td>Firewall konfiguration</td>
<td><span class="badge bg-success bg-opacity-10 text-success">Fuldført</span></td>
<td class="text-end text-muted">10:23</td>
</tr>
<tr>
<td class="fw-bold">Byg & Bo ApS</td>
<td>Licens fornyelse</td>
<td><span class="badge bg-warning bg-opacity-10 text-warning">Afventer</span></td>
<td class="text-end text-muted">I går</td>
</tr>
<tr>
<td class="fw-bold">Cafe Møller</td>
<td>Netværksnedbrud</td>
<td><span class="badge bg-danger bg-opacity-10 text-danger">Kritisk</span></td>
<td class="text-end text-muted">I går</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card p-4 h-100">
<h5 class="fw-bold mb-4">System Status</h5>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="small fw-bold text-muted">CPU LOAD</span>
<span class="small fw-bold">24%</span>
</div>
<div class="progress" style="height: 8px; background-color: var(--accent-light);">
<div class="progress-bar" style="width: 24%; background-color: var(--accent);"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="small fw-bold text-muted">MEMORY</span>
<span class="small fw-bold">56%</span>
</div>
<div class="progress" style="height: 8px; background-color: var(--accent-light);">
<div class="progress-bar" style="width: 56%; background-color: var(--accent);"></div>
</div>
</div>
<div class="mt-auto p-3 rounded" style="background-color: var(--accent-light);">
<div class="d-flex">
<i class="bi bi-check-circle-fill text-success me-2"></i>
<small class="fw-bold" style="color: var(--accent)">Alle systemer kører optimalt.</small>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -3,10 +3,11 @@ Pydantic Models and Schemas
""" """
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Optional, List
from datetime import datetime from datetime import datetime
# Customer Schemas
class CustomerBase(BaseModel): class CustomerBase(BaseModel):
"""Base customer schema""" """Base customer schema"""
name: str name: str
@ -15,9 +16,30 @@ class CustomerBase(BaseModel):
address: Optional[str] = None address: Optional[str] = None
class CustomerCreate(CustomerBase): class CustomerCreate(BaseModel):
"""Schema for creating a customer""" """Schema for creating a customer"""
pass name: str
cvr_number: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
address: Optional[str] = None
postal_code: Optional[str] = None
city: Optional[str] = None
website: Optional[str] = None
is_active: bool = True
class CustomerUpdate(BaseModel):
"""Schema for updating a customer"""
name: Optional[str] = None
cvr_number: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
address: Optional[str] = None
postal_code: Optional[str] = None
city: Optional[str] = None
website: Optional[str] = None
is_active: Optional[bool] = None
class Customer(CustomerBase): class Customer(CustomerBase):
@ -30,6 +52,70 @@ class Customer(CustomerBase):
from_attributes = True from_attributes = True
# Contact Schemas
class ContactBase(BaseModel):
"""Base contact schema"""
first_name: str
last_name: str
email: Optional[str] = None
phone: Optional[str] = None
mobile: Optional[str] = None
title: Optional[str] = None
department: Optional[str] = None
class ContactCreate(ContactBase):
"""Schema for creating a contact"""
company_ids: List[int] = [] # List of customer IDs to link to
is_primary: bool = False # Whether this is the primary contact for first company
role: Optional[str] = None
notes: Optional[str] = None
is_active: bool = True
class ContactUpdate(BaseModel):
"""Schema for updating a contact"""
first_name: Optional[str] = None
last_name: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
mobile: Optional[str] = None
title: Optional[str] = None
department: Optional[str] = None
is_active: Optional[bool] = None
class ContactCompanyLink(BaseModel):
"""Schema for linking/unlinking a contact to a company"""
customer_id: int
is_primary: bool = False
role: Optional[str] = None
notes: Optional[str] = None
class CompanyInfo(BaseModel):
"""Schema for company information in contact context"""
id: int
name: str
is_primary: bool
role: Optional[str] = None
notes: Optional[str] = None
class Contact(ContactBase):
"""Full contact schema"""
id: int
is_active: bool
vtiger_id: Optional[str] = None
created_at: datetime
updated_at: Optional[datetime] = None
companies: List[CompanyInfo] = [] # List of linked companies
class Config:
from_attributes = True
# Hardware Schemas
class HardwareBase(BaseModel): class HardwareBase(BaseModel):
"""Base hardware schema""" """Base hardware schema"""
serial_number: str serial_number: str
@ -49,3 +135,69 @@ class Hardware(HardwareBase):
class Config: class Config:
from_attributes = True from_attributes = True
# Vendor Schemas
class VendorBase(BaseModel):
"""Base vendor schema"""
name: str
email: Optional[str] = None
phone: Optional[str] = None
class VendorCreate(BaseModel):
"""Schema for creating a vendor"""
name: str
cvr_number: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
address: Optional[str] = None
postal_code: Optional[str] = None
city: Optional[str] = None
website: Optional[str] = None
domain: Optional[str] = None
email_pattern: Optional[str] = None
category: str = 'general'
priority: int = 50
notes: Optional[str] = None
is_active: bool = True
class VendorUpdate(BaseModel):
"""Schema for updating a vendor"""
name: Optional[str] = None
cvr_number: Optional[str] = None
email: Optional[str] = None
phone: Optional[str] = None
address: Optional[str] = None
postal_code: Optional[str] = None
city: Optional[str] = None
website: Optional[str] = None
domain: Optional[str] = None
email_pattern: Optional[str] = None
category: Optional[str] = None
priority: Optional[int] = None
notes: Optional[str] = None
is_active: Optional[bool] = None
class Vendor(VendorBase):
"""Full vendor schema"""
id: int
cvr_number: Optional[str] = None
address: Optional[str] = None
postal_code: Optional[str] = None
city: Optional[str] = None
country: Optional[str] = None
website: Optional[str] = None
domain: Optional[str] = None
category: str
priority: int
notes: Optional[str] = None
is_active: bool
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True

View File

@ -1 +0,0 @@
"""Routers package"""

View File

@ -1,47 +0,0 @@
"""
Customers Router
API endpoints for customer management
"""
from fastapi import APIRouter, HTTPException
from typing import List
from app.models.schemas import Customer, CustomerCreate
from app.core.database import execute_query
router = APIRouter()
@router.get("/customers", response_model=List[Customer])
async def list_customers():
"""List all customers"""
query = "SELECT * FROM customers ORDER BY created_at DESC"
customers = execute_query(query)
return customers
@router.get("/customers/{customer_id}", response_model=Customer)
async def get_customer(customer_id: int):
"""Get a specific customer"""
query = "SELECT * FROM customers WHERE id = %s"
customers = execute_query(query, (customer_id,))
if not customers:
raise HTTPException(status_code=404, detail="Customer not found")
return customers[0]
@router.post("/customers", response_model=Customer)
async def create_customer(customer: CustomerCreate):
"""Create a new customer"""
query = """
INSERT INTO customers (name, email, phone, address)
VALUES (%s, %s, %s, %s)
RETURNING *
"""
result = execute_query(
query,
(customer.name, customer.email, customer.phone, customer.address)
)
return result[0]

136
app/services/cvr_service.py Normal file
View File

@ -0,0 +1,136 @@
"""
CVR.dk API service for looking up Danish company information
Free public API - no authentication required
Adapted from OmniSync for BMC Hub
"""
import asyncio
import aiohttp
import logging
from typing import Optional, Dict
logger = logging.getLogger(__name__)
class CVRService:
"""Service for CVR.dk API lookups"""
BASE_URL = "https://cvrapi.dk/api"
async def lookup_by_name(self, company_name: str) -> Optional[Dict]:
"""
Lookup company by name using CVR.dk API
Args:
company_name: Company name to search for
Returns:
Company data dict or None if not found
"""
if not company_name or len(company_name) < 3:
return None
# Clean company name
clean_name = company_name.strip()
try:
params = {
'search': clean_name,
'country': 'dk'
}
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.BASE_URL}",
params=params,
timeout=aiohttp.ClientTimeout(total=10)
) as response:
if response.status == 200:
data = await response.json()
if data and 'vat' in data:
logger.info(f"✅ Found CVR {data['vat']} for '{company_name}'")
return {
'cvr': data.get('vat'),
'name': data.get('name'),
'address': data.get('address'),
'city': data.get('city'),
'zipcode': data.get('zipcode'),
'country': data.get('country'),
'phone': data.get('phone'),
'email': data.get('email'),
'vat': data.get('vat'),
'status': data.get('status')
}
elif response.status == 404:
logger.warning(f"⚠️ No CVR found for '{company_name}'")
return None
else:
logger.error(f"❌ CVR API error {response.status} for '{company_name}'")
return None
except asyncio.TimeoutError:
logger.error(f"⏱️ CVR API timeout for '{company_name}'")
return None
except Exception as e:
logger.error(f"❌ CVR lookup error for '{company_name}': {e}")
return None
async def lookup_by_cvr(self, cvr_number: str) -> Optional[Dict]:
"""
Lookup company by CVR number
Args:
cvr_number: CVR number (8 digits)
Returns:
Company data dict or None if not found
"""
if not cvr_number:
return None
# Extract only digits
cvr_clean = ''.join(filter(str.isdigit, str(cvr_number)))
if len(cvr_clean) != 8:
logger.warning(f"⚠️ Invalid CVR number format: {cvr_number}")
return None
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{self.BASE_URL}",
params={'vat': cvr_clean, 'country': 'dk'},
timeout=aiohttp.ClientTimeout(total=10)
) as response:
if response.status == 200:
data = await response.json()
if data and 'vat' in data:
logger.info(f"✅ Validated CVR {cvr_clean}")
return {
'cvr': data.get('vat'),
'name': data.get('name'),
'address': data.get('address'),
'city': data.get('city'),
'zipcode': data.get('zipcode'),
'postal_code': data.get('zipcode'), # Alias for consistency
'country': data.get('country'),
'phone': data.get('phone'),
'email': data.get('email'),
'vat': data.get('vat'),
'status': data.get('status')
}
return None
except Exception as e:
logger.error(f"❌ CVR validation error for {cvr_number}: {e}")
return None
def get_cvr_service() -> CVRService:
"""Get CVR service instance"""
return CVRService()

View File

@ -0,0 +1,603 @@
<!DOCTYPE html>
<html lang="da" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}BMC Hub{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
--border-radius: 12px;
}
[data-bs-theme="dark"] {
--bg-body: #212529;
--bg-card: #2c3034;
--text-primary: #f8f9fa;
--text-secondary: #adb5bd;
--accent: #3d8bfd; /* Lighter blue for dark mode */
--accent-light: #373b3e;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding-top: 80px;
transition: background-color 0.3s, color 0.3s;
}
.navbar {
background: var(--bg-card);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
padding: 1rem 0;
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.navbar-brand {
font-weight: 700;
color: var(--accent);
font-size: 1.25rem;
}
.nav-link {
color: var(--text-secondary);
padding: 0.6rem 1.2rem !important;
border-radius: var(--border-radius);
transition: all 0.2s;
font-weight: 500;
margin: 0 0.2rem;
}
.nav-link:hover, .nav-link.active {
background-color: var(--accent-light);
color: var(--accent);
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
transition: transform 0.2s, background-color 0.3s;
background: var(--bg-card);
}
.card:hover {
transform: translateY(-2px);
}
.stat-card h3 {
font-size: 2rem;
font-weight: 700;
color: var(--accent);
margin-bottom: 0;
}
.stat-card p {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.table {
color: var(--text-primary);
}
.table th {
font-weight: 600;
color: var(--text-secondary);
border-bottom-width: 1px;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background-color: var(--accent);
border-color: var(--accent);
padding: 0.6rem 1.5rem;
border-radius: 8px;
}
.btn-primary:hover {
background-color: #0a3655;
border-color: #0a3655;
}
.header-search {
background: var(--bg-body);
border: 1px solid rgba(0,0,0,0.1);
padding: 0.6rem 1.2rem;
border-radius: 8px;
width: 300px;
color: var(--text-primary);
}
.header-search:focus {
outline: none;
border-color: var(--accent);
}
.dropdown-menu {
border: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border-radius: 12px;
padding: 0.5rem;
background-color: var(--bg-card);
}
.dropdown-item {
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.dropdown-item:hover {
background-color: var(--accent-light);
color: var(--accent);
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center" href="/">
<div class="bg-primary text-white rounded p-1 me-2 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; background-color: var(--accent) !important;">
<i class="bi bi-hdd-network-fill" style="font-size: 16px;"></i>
</div>
BMC Hub
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-people me-2"></i>CRM
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="/customers">Kunder</a></li>
<li><a class="dropdown-item py-2" href="/contacts">Kontakter</a></li>
<li><a class="dropdown-item py-2" href="#">Leverandører</a></li>
<li><a class="dropdown-item py-2" href="#">Leads</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2" href="#">Rapporter</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-headset me-2"></i>Support
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Sager</a></li>
<li><a class="dropdown-item py-2" href="#">Ny Sag</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2" href="#">Knowledge Base</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-cart3 me-2"></i>Salg
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Tilbud</a></li>
<li><a class="dropdown-item py-2" href="#">Ordre</a></li>
<li><a class="dropdown-item py-2" href="#">Produkter</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2" href="#">Pipeline</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-currency-dollar me-2"></i>Økonomi
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Fakturaer</a></li>
<li><a class="dropdown-item py-2" href="#">Abonnementer</a></li>
<li><a class="dropdown-item py-2" href="#">Betalinger</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2" href="#">Rapporter</a></li>
</ul>
</li>
</ul>
<div class="d-flex align-items-center gap-3">
<button class="btn btn-light rounded-circle border-0" id="darkModeToggle" style="background: var(--accent-light); color: var(--accent);">
<i class="bi bi-moon-fill"></i>
</button>
<button class="btn btn-light rounded-circle border-0" style="background: var(--accent-light); color: var(--accent);"><i class="bi bi-bell"></i></button>
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none text-dark dropdown-toggle" data-bs-toggle="dropdown">
<img src="https://ui-avatars.com/api/?name=CT&background=0f4c75&color=fff" class="rounded-circle me-2" width="32">
<span class="small fw-bold" style="color: var(--text-primary)">Christian</span>
</a>
<ul class="dropdown-menu dropdown-menu-end mt-2">
<li><a class="dropdown-item py-2" href="#">Profil</a></li>
<li><a class="dropdown-item py-2" href="#">Indstillinger</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2 text-danger" href="#">Log ud</a></li>
</ul>
</div>
</div>
</div>
</div>
</nav>
<!-- Global Search Modal (Cmd+K) -->
<div class="modal fade" id="globalSearchModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" style="max-width: 85vw; width: 85vw;">
<div class="modal-content" style="border: none; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); height: 85vh;">
<div class="modal-body p-0 d-flex flex-column" style="height: 100%;">
<div class="p-4 border-bottom" style="background: var(--bg-card);">
<div class="position-relative">
<i class="bi bi-search position-absolute" style="left: 20px; top: 50%; transform: translateY(-50%); font-size: 1.5rem; color: var(--text-secondary);"></i>
<input
type="text"
id="globalSearchInput"
class="form-control form-control-lg ps-5"
placeholder="Søg efter kunder, sager, produkter... (tryk Esc for at lukke)"
style="border: none; background: var(--bg-body); font-size: 1.25rem; padding: 1rem 1rem 1rem 4rem; border-radius: 12px;"
autofocus
>
</div>
<div class="d-flex gap-2 mt-3">
<span class="badge bg-secondary bg-opacity-10 text-secondary">⌘K for at åbne</span>
<span class="badge bg-secondary bg-opacity-10 text-secondary">ESC for at lukke</span>
<span class="badge bg-secondary bg-opacity-10 text-secondary">↑↓ for at navigere</span>
</div>
</div>
<div class="row g-0 flex-grow-1" style="overflow-y: auto;">
<!-- Search Results Area (3/4 width) -->
<div class="col-lg-9 p-4" style="border-right: 1px solid rgba(0,0,0,0.1);">
<div id="searchResults">
<!-- Empty State -->
<div id="emptyState" class="text-center py-5">
<i class="bi bi-search text-muted" style="font-size: 4rem; opacity: 0.3;"></i>
<p class="text-muted mt-3">Begynd at skrive for at søge...</p>
</div>
<!-- CRM Results -->
<div id="crmResults" class="result-section mb-4" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="text-muted text-uppercase small fw-bold mb-0">
<i class="bi bi-people me-2"></i>CRM
</h6>
<a href="/crm/workflow" class="btn btn-sm btn-outline-primary">
<i class="bi bi-diagram-3 me-1"></i>Se Workflow
</a>
</div>
<div class="result-items">
<!-- Dynamic results will be inserted here -->
</div>
</div>
<!-- Support Results -->
<div id="supportResults" class="result-section mb-4" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="text-muted text-uppercase small fw-bold mb-0">
<i class="bi bi-headset me-2"></i>Support
</h6>
<a href="/support/workflow" class="btn btn-sm btn-outline-primary">
<i class="bi bi-diagram-3 me-1"></i>Se Workflow
</a>
</div>
<div class="result-items">
<!-- Dynamic results will be inserted here -->
</div>
</div>
<!-- Sales Results -->
<div id="salesResults" class="result-section mb-4" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="text-muted text-uppercase small fw-bold mb-0">
<i class="bi bi-cart3 me-2"></i>Salg
</h6>
<a href="/sales/workflow" class="btn btn-sm btn-outline-primary">
<i class="bi bi-diagram-3 me-1"></i>Se Workflow
</a>
</div>
<div class="result-items">
<!-- Dynamic results will be inserted here -->
</div>
</div>
<!-- Finance Results -->
<div id="financeResults" class="result-section mb-4" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="text-muted text-uppercase small fw-bold mb-0">
<i class="bi bi-currency-dollar me-2"></i>Økonomi
</h6>
<a href="/finance/workflow" class="btn btn-sm btn-outline-primary">
<i class="bi bi-diagram-3 me-1"></i>Se Workflow
</a>
</div>
<div class="result-items">
<!-- Dynamic results will be inserted here -->
</div>
</div>
</div>
</div>
<!-- Activity Sidebar (1/4 width) -->
<div class="col-lg-3 p-4" style="background: var(--accent-light);">
<h6 class="text-uppercase small fw-bold mb-3" style="color: var(--accent);">
<i class="bi bi-clock-history me-2"></i>Seneste Aktivitet
</h6>
<div id="recentActivity">
<div class="activity-item mb-3 p-2 rounded" style="background: var(--bg-card);">
<div class="d-flex align-items-start">
<i class="bi bi-person-circle text-primary me-2" style="font-size: 1.2rem;"></i>
<div class="flex-grow-1">
<p class="mb-0 small fw-bold">Advokatgruppen A/S</p>
<p class="mb-0 small text-muted">Opdateret for 2 min siden</p>
</div>
</div>
</div>
<div class="activity-item mb-3 p-2 rounded" style="background: var(--bg-card);">
<div class="d-flex align-items-start">
<i class="bi bi-ticket-detailed text-warning me-2" style="font-size: 1.2rem;"></i>
<div class="flex-grow-1">
<p class="mb-0 small fw-bold">Sag #1234 lukket</p>
<p class="mb-0 small text-muted">For 15 min siden</p>
</div>
</div>
</div>
<div class="activity-item mb-3 p-2 rounded" style="background: var(--bg-card);">
<div class="d-flex align-items-start">
<i class="bi bi-receipt text-success me-2" style="font-size: 1.2rem;"></i>
<div class="flex-grow-1">
<p class="mb-0 small fw-bold">Faktura #5678 betalt</p>
<p class="mb-0 small text-muted">I dag kl. 14:30</p>
</div>
</div>
</div>
<div class="activity-item mb-3 p-2 rounded" style="background: var(--bg-card);">
<div class="d-flex align-items-start">
<i class="bi bi-box-seam text-info me-2" style="font-size: 1.2rem;"></i>
<div class="flex-grow-1">
<p class="mb-0 small fw-bold">Ny ordre oprettet</p>
<p class="mb-0 small text-muted">I går kl. 16:45</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container-fluid px-4 py-4">
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Dark Mode Toggle Logic
const darkModeToggle = document.getElementById('darkModeToggle');
const htmlElement = document.documentElement;
const icon = darkModeToggle.querySelector('i');
// Check local storage
if (localStorage.getItem('theme') === 'dark') {
htmlElement.setAttribute('data-bs-theme', 'dark');
icon.classList.replace('bi-moon-fill', 'bi-sun-fill');
}
darkModeToggle.addEventListener('click', () => {
if (htmlElement.getAttribute('data-bs-theme') === 'dark') {
htmlElement.setAttribute('data-bs-theme', 'light');
localStorage.setItem('theme', 'light');
icon.classList.replace('bi-sun-fill', 'bi-moon-fill');
} else {
htmlElement.setAttribute('data-bs-theme', 'dark');
localStorage.setItem('theme', 'dark');
icon.classList.replace('bi-moon-fill', 'bi-sun-fill');
}
});
// Global Search Modal (Cmd+K)
const searchModal = new bootstrap.Modal(document.getElementById('globalSearchModal'));
const searchInput = document.getElementById('globalSearchInput');
// Keyboard shortcut: Cmd+K or Ctrl+K
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
searchModal.show();
setTimeout(() => searchInput.focus(), 300);
}
// ESC to close
if (e.key === 'Escape') {
searchModal.hide();
}
});
// Mock search data
const mockData = {
crm: [
{ name: 'Advokatgruppen A/S', type: 'Kunde', id: '001', url: '/customers/001' },
{ name: 'Byg & Bo ApS', type: 'Kunde', id: '002', url: '/customers/002' },
{ name: 'Cafe Møller', type: 'Kunde', id: '003', url: '/customers/003' },
{ name: 'ABC Leverandør', type: 'Leverandør', id: 'v001', url: '/vendors/v001' },
{ name: 'Tech Solutions A/S', type: 'Leverandør', id: 'v002', url: '/vendors/v002' },
],
support: [
{ title: 'Netværksnedbrud - Cafe Møller', status: 'Kritisk', id: '#1234', created: '2 timer siden' },
{ title: 'Licens fornyelse - Byg & Bo', status: 'Afventer', id: '#1235', created: 'I går' },
{ title: 'Firewall config - Advokatgruppen', status: 'Løst', id: '#1236', created: 'I går' },
],
sales: [
{ title: 'Tilbud - Firewall Upgrade', customer: 'Advokatgruppen A/S', amount: '45.000 kr', status: 'Afventer' },
{ title: 'Ordre - Switch 48-port', customer: 'Byg & Bo ApS', amount: '12.000 kr', status: 'Godkendt' },
],
finance: [
{ title: 'Faktura #5678', customer: 'Advokatgruppen A/S', amount: '25.000 kr', status: 'Betalt' },
{ title: 'Faktura #5679', customer: 'Cafe Møller', amount: '8.500 kr', status: 'Ubetalt' },
{ title: 'Abonnement - Monitoring', customer: 'Byg & Bo ApS', amount: '2.500 kr/md', status: 'Aktiv' },
]
};
// Search function
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
if (query.length === 0) {
document.getElementById('emptyState').style.display = 'block';
document.getElementById('crmResults').style.display = 'none';
document.getElementById('supportResults').style.display = 'none';
document.getElementById('salesResults').style.display = 'none';
document.getElementById('financeResults').style.display = 'none';
return;
}
document.getElementById('emptyState').style.display = 'none';
// Filter CRM results
const crmMatches = mockData.crm.filter(item =>
item.name.toLowerCase().includes(query) ||
item.type.toLowerCase().includes(query)
);
const crmSection = document.getElementById('crmResults');
if (crmMatches.length > 0) {
crmSection.style.display = 'block';
crmSection.querySelector('.result-items').innerHTML = crmMatches.map(item => `
<a href="${item.url}" class="result-item d-block p-3 mb-2 rounded text-decoration-none" style="background: var(--bg-card); border: 1px solid transparent; transition: all 0.2s;">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="mb-1 fw-bold" style="color: var(--text-primary);">${item.name}</p>
<p class="mb-0 small text-muted">${item.type} • ID: ${item.id}</p>
</div>
<i class="bi bi-arrow-right" style="color: var(--accent);"></i>
</div>
</a>
`).join('');
} else {
crmSection.style.display = 'none';
}
// Filter Support results
const supportMatches = mockData.support.filter(item =>
item.title.toLowerCase().includes(query) ||
item.status.toLowerCase().includes(query)
);
const supportSection = document.getElementById('supportResults');
if (supportMatches.length > 0) {
supportSection.style.display = 'block';
supportSection.querySelector('.result-items').innerHTML = supportMatches.map(item => {
const statusColor = item.status === 'Kritisk' ? 'danger' : item.status === 'Afventer' ? 'warning' : 'success';
return `
<a href="/support/${item.id}" class="result-item d-block p-3 mb-2 rounded text-decoration-none" style="background: var(--bg-card); border: 1px solid transparent; transition: all 0.2s;">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<p class="mb-1 fw-bold" style="color: var(--text-primary);">${item.title}</p>
<p class="mb-0 small text-muted">${item.id} • ${item.created}</p>
</div>
<span class="badge bg-${statusColor} bg-opacity-10 text-${statusColor}">${item.status}</span>
</div>
</a>
`;
}).join('');
} else {
supportSection.style.display = 'none';
}
// Filter Sales results
const salesMatches = mockData.sales.filter(item =>
item.title.toLowerCase().includes(query) ||
item.customer.toLowerCase().includes(query)
);
const salesSection = document.getElementById('salesResults');
if (salesMatches.length > 0) {
salesSection.style.display = 'block';
salesSection.querySelector('.result-items').innerHTML = salesMatches.map(item => `
<a href="/sales" class="result-item d-block p-3 mb-2 rounded text-decoration-none" style="background: var(--bg-card); border: 1px solid transparent; transition: all 0.2s;">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<p class="mb-1 fw-bold" style="color: var(--text-primary);">${item.title}</p>
<p class="mb-0 small text-muted">${item.customer}</p>
</div>
<div class="text-end">
<p class="mb-0 fw-bold" style="color: var(--accent);">${item.amount}</p>
<p class="mb-0 small text-muted">${item.status}</p>
</div>
</div>
</a>
`).join('');
} else {
salesSection.style.display = 'none';
}
// Filter Finance results
const financeMatches = mockData.finance.filter(item =>
item.title.toLowerCase().includes(query) ||
item.customer.toLowerCase().includes(query)
);
const financeSection = document.getElementById('financeResults');
if (financeMatches.length > 0) {
financeSection.style.display = 'block';
financeSection.querySelector('.result-items').innerHTML = financeMatches.map(item => {
const statusColor = item.status === 'Betalt' ? 'success' : item.status === 'Ubetalt' ? 'danger' : 'primary';
return `
<a href="/finance" class="result-item d-block p-3 mb-2 rounded text-decoration-none" style="background: var(--bg-card); border: 1px solid transparent; transition: all 0.2s;">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<p class="mb-1 fw-bold" style="color: var(--text-primary);">${item.title}</p>
<p class="mb-0 small text-muted">${item.customer}</p>
</div>
<div class="text-end">
<p class="mb-0 fw-bold" style="color: var(--accent);">${item.amount}</p>
<span class="badge bg-${statusColor} bg-opacity-10 text-${statusColor}">${item.status}</span>
</div>
</div>
</a>
`;
}).join('');
} else {
financeSection.style.display = 'none';
}
});
// Hover effects for result items
document.addEventListener('DOMContentLoaded', () => {
const style = document.createElement('style');
style.textContent = `
.result-item:hover {
border-color: var(--accent) !important;
background: var(--accent-light) !important;
}
`;
document.head.appendChild(style);
});
// Reset search when modal is closed
document.getElementById('globalSearchModal').addEventListener('hidden.bs.modal', () => {
searchInput.value = '';
document.getElementById('emptyState').style.display = 'block';
document.getElementById('crmResults').style.display = 'none';
document.getElementById('supportResults').style.display = 'none';
document.getElementById('salesResults').style.display = 'none';
document.getElementById('financeResults').style.display = 'none';
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@ -42,6 +42,9 @@ services:
# Mount for local development - live code reload # Mount for local development - live code reload
- ./app:/app/app:ro - ./app:/app/app:ro
- ./main.py:/app/main.py:ro - ./main.py:/app/main.py:ro
- ./scripts:/app/scripts:ro
# Mount OmniSync database for import (read-only)
- /Users/christianthomas/pakkemodtagelse/data:/omnisync_data:ro
env_file: env_file:
- .env - .env
environment: environment:

View File

@ -0,0 +1,562 @@
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Design System & Komponenter</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--accent: #0f4c75;
--accent-light: #eef2f5;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--bg-body: #f8f9fa;
--bg-card: #ffffff;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
/* Navbar Styling */
.navbar {
background-color: var(--bg-card);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
padding: 0.75rem 1.5rem;
}
.navbar-brand {
font-weight: 700;
color: var(--accent);
font-size: 1.25rem;
}
.nav-link {
color: var(--text-secondary);
font-weight: 500;
padding: 0.5rem 1rem;
border-radius: 6px;
transition: all 0.2s;
}
.nav-link:hover, .nav-link.active {
color: var(--accent);
background-color: var(--accent-light);
}
/* Card Styling */
.card {
border: none;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.03);
margin-bottom: 1.5rem;
}
.card-header {
background-color: transparent;
border-bottom: 1px solid rgba(0,0,0,0.05);
padding: 1.25rem;
font-weight: 600;
}
.card-body {
padding: 1.5rem;
}
/* Buttons */
.btn-primary {
background-color: var(--accent);
border-color: var(--accent);
}
.btn-primary:hover {
background-color: #0b3a5b;
border-color: #0b3a5b;
}
.btn-outline-primary {
color: var(--accent);
border-color: var(--accent);
}
.btn-outline-primary:hover {
background-color: var(--accent);
color: white;
}
/* Section Headers */
.section-title {
color: var(--text-secondary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 600;
margin-bottom: 1rem;
margin-top: 2rem;
}
.color-swatch {
width: 100%;
height: 60px;
border-radius: 8px;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 500;
font-size: 0.8rem;
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
</style>
</head>
<body>
<!-- Top Navigation -->
<nav class="navbar navbar-expand-lg sticky-top">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<i class="bi bi-grid-3x3-gap-fill me-2"></i>BMC Hub
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="index.html">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="customers.html">Kunder</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="components.html">Design System</a>
</li>
</ul>
<div class="d-flex align-items-center gap-3">
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle text-dark" data-bs-toggle="dropdown">
<div class="bg-light rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
<i class="bi bi-person-fill text-secondary"></i>
</div>
<span class="small fw-medium">Admin</span>
</a>
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-sm">
<li><a class="dropdown-item" href="settings.html"><i class="bi bi-gear me-2"></i>Indstillinger</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="login.html"><i class="bi bi-box-arrow-right me-2"></i>Log ud</a></li>
</ul>
</div>
</div>
</div>
</div>
</nav>
<div class="container-fluid py-4">
<div class="row mb-4">
<div class="col-12">
<h1 class="h3 fw-bold text-dark">Design System & Komponenter</h1>
<p class="text-secondary">En komplet oversigt over alle UI elementer i "Nordic Top" temaet.</p>
</div>
</div>
<!-- Farver & Typografi -->
<div class="row">
<div class="col-12">
<h6 class="section-title">Farver & Typografi</h6>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Farvepalette</div>
<div class="card-body">
<div class="row g-3">
<div class="col-6 col-md-3">
<div class="color-swatch" style="background-color: #0f4c75;">#0f4c75</div>
<div class="small text-center fw-bold">Accent</div>
</div>
<div class="col-6 col-md-3">
<div class="color-swatch text-dark" style="background-color: #eef2f5;">#eef2f5</div>
<div class="small text-center fw-bold">Accent Light</div>
</div>
<div class="col-6 col-md-3">
<div class="color-swatch" style="background-color: #2c3e50;">#2c3e50</div>
<div class="small text-center fw-bold">Text Primary</div>
</div>
<div class="col-6 col-md-3">
<div class="color-swatch" style="background-color: #6c757d;">#6c757d</div>
<div class="small text-center fw-bold">Text Secondary</div>
</div>
<div class="col-6 col-md-3">
<div class="color-swatch" style="background-color: #198754;">#198754</div>
<div class="small text-center fw-bold">Success</div>
</div>
<div class="col-6 col-md-3">
<div class="color-swatch" style="background-color: #ffc107; color: #000;">#ffc107</div>
<div class="small text-center fw-bold">Warning</div>
</div>
<div class="col-6 col-md-3">
<div class="color-swatch" style="background-color: #dc3545;">#dc3545</div>
<div class="small text-center fw-bold">Danger</div>
</div>
<div class="col-6 col-md-3">
<div class="color-swatch" style="background-color: #0dcaf0;">#0dcaf0</div>
<div class="small text-center fw-bold">Info</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Typografi</div>
<div class="card-body">
<h1>H1 Overskrift (2.5rem)</h1>
<h2>H2 Overskrift (2rem)</h2>
<h3>H3 Overskrift (1.75rem)</h3>
<h4>H4 Overskrift (1.5rem)</h4>
<h5>H5 Overskrift (1.25rem)</h5>
<h6>H6 Overskrift (1rem)</h6>
<hr>
<p class="lead">Dette er en lead paragraph, der bruges til intro tekst.</p>
<p>Dette er almindelig brødtekst. Lorem ipsum dolor sit amet, consectetur adipiscing elit. <small class="text-muted">Dette er lille tekst.</small></p>
</div>
</div>
</div>
</div>
<!-- Knapper & Badges -->
<div class="row">
<div class="col-12">
<h6 class="section-title">Knapper & Badges</h6>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">Knapper</div>
<div class="card-body">
<div class="mb-3">
<button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-secondary">Secondary</button>
<button type="button" class="btn btn-success">Success</button>
<button type="button" class="btn btn-danger">Danger</button>
<button type="button" class="btn btn-warning">Warning</button>
<button type="button" class="btn btn-info">Info</button>
<button type="button" class="btn btn-light">Light</button>
<button type="button" class="btn btn-dark">Dark</button>
<button type="button" class="btn btn-link">Link</button>
</div>
<div class="mb-3">
<button type="button" class="btn btn-outline-primary">Outline Primary</button>
<button type="button" class="btn btn-outline-secondary">Outline Secondary</button>
<button type="button" class="btn btn-outline-success">Outline Success</button>
<button type="button" class="btn btn-outline-danger">Outline Danger</button>
</div>
<div class="mb-3">
<button type="button" class="btn btn-primary btn-lg">Large Button</button>
<button type="button" class="btn btn-primary">Default Button</button>
<button type="button" class="btn btn-primary btn-sm">Small Button</button>
</div>
<div>
<button type="button" class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Med Ikon</button>
<button type="button" class="btn btn-light border"><i class="bi bi-filter me-2"></i>Filter</button>
<button type="button" class="btn btn-light border rounded-circle p-2"><i class="bi bi-three-dots-vertical"></i></button>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-header">Badges & Alerts</div>
<div class="card-body">
<div class="mb-3">
<span class="badge bg-primary">Primary</span>
<span class="badge bg-secondary">Secondary</span>
<span class="badge bg-success">Success</span>
<span class="badge bg-danger">Danger</span>
<span class="badge bg-warning text-dark">Warning</span>
<span class="badge bg-info text-dark">Info</span>
</div>
<div class="mb-3">
<span class="badge rounded-pill bg-primary">Pille Form</span>
<span class="badge rounded-pill bg-success">Aktiv</span>
<span class="badge rounded-pill bg-light text-dark border">Kladde</span>
</div>
<div class="alert alert-success py-2 mb-2" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>Handling gennemført!
</div>
<div class="alert alert-warning py-2 mb-0" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>Tjek venligst input.
</div>
</div>
</div>
</div>
</div>
<!-- Formularer -->
<div class="row">
<div class="col-12">
<h6 class="section-title">Formular Elementer</h6>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Input Felter</div>
<div class="card-body">
<div class="mb-3">
<label for="exampleInputEmail1" class="form-label">Email adresse</label>
<input type="email" class="form-control" id="exampleInputEmail1" placeholder="navn@eksempel.dk">
<div class="form-text">Vi deler aldrig din email med andre.</div>
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">Adgangskode</label>
<input type="password" class="form-control" id="exampleInputPassword1">
</div>
<div class="mb-3">
<label for="disabledInput" class="form-label">Deaktiveret input</label>
<input class="form-control" id="disabledInput" type="text" placeholder="Kan ikke ændres..." disabled>
</div>
<div class="mb-3">
<label class="form-label">Select Menu</label>
<select class="form-select">
<option selected>Vælg en mulighed</option>
<option value="1">Mulighed 1</option>
<option value="2">Mulighed 2</option>
<option value="3">Mulighed 3</option>
</select>
</div>
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Tekstområde</label>
<textarea class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Checkboxes, Radios & Switches</div>
<div class="card-body">
<div class="mb-4">
<h6 class="small text-uppercase text-muted fw-bold">Checkboxes</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="flexCheckDefault">
<label class="form-check-label" for="flexCheckDefault">
Standard checkbox
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="flexCheckChecked" checked>
<label class="form-check-label" for="flexCheckChecked">
Valgt checkbox
</label>
</div>
</div>
<div class="mb-4">
<h6 class="small text-uppercase text-muted fw-bold">Radio Buttons</h6>
<div class="form-check">
<input class="form-check-input" type="radio" name="flexRadioDefault" id="flexRadioDefault1">
<label class="form-check-label" for="flexRadioDefault1">
Mulighed 1
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="flexRadioDefault" id="flexRadioDefault2" checked>
<label class="form-check-label" for="flexRadioDefault2">
Mulighed 2 (Valgt)
</label>
</div>
</div>
<div class="mb-4">
<h6 class="small text-uppercase text-muted fw-bold">Switches</h6>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckDefault">
<label class="form-check-label" for="flexSwitchCheckDefault">Standard switch</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="flexSwitchCheckChecked" checked>
<label class="form-check-label" for="flexSwitchCheckChecked">Aktiv switch</label>
</div>
</div>
<div>
<h6 class="small text-uppercase text-muted fw-bold">Input Groups</h6>
<div class="input-group mb-3">
<span class="input-group-text" id="basic-addon1">@</span>
<input type="text" class="form-control" placeholder="Brugernavn" aria-label="Username">
</div>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Beløb">
<span class="input-group-text">DKK</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabeller -->
<div class="row">
<div class="col-12">
<h6 class="section-title">Tabeller</h6>
</div>
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Data Tabel</span>
<button class="btn btn-sm btn-outline-primary"><i class="bi bi-download me-1"></i>Eksportér</button>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Kunde</th>
<th>Status</th>
<th>Oprettet</th>
<th>Beløb</th>
<th class="text-end pe-4">Handling</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 text-primary rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px; font-weight: 600;">
BN
</div>
<div>
<div class="fw-bold">BMC Networks</div>
<div class="small text-muted">CVR: 12345678</div>
</div>
</div>
</td>
<td><span class="badge bg-success bg-opacity-10 text-success rounded-pill px-3">Aktiv</span></td>
<td>01. Dec 2023</td>
<td>4.500 DKK</td>
<td class="text-end pe-4">
<button class="btn btn-sm btn-light border"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-light border text-danger"><i class="bi bi-trash"></i></button>
</td>
</tr>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="bg-warning bg-opacity-10 text-warning rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px; font-weight: 600;">
TC
</div>
<div>
<div class="fw-bold">Tech Corp ApS</div>
<div class="small text-muted">CVR: 87654321</div>
</div>
</div>
</td>
<td><span class="badge bg-warning bg-opacity-10 text-warning rounded-pill px-3">Afventer</span></td>
<td>28. Nov 2023</td>
<td>12.000 DKK</td>
<td class="text-end pe-4">
<button class="btn btn-sm btn-light border"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-light border text-danger"><i class="bi bi-trash"></i></button>
</td>
</tr>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="bg-secondary bg-opacity-10 text-secondary rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px; font-weight: 600;">
LL
</div>
<div>
<div class="fw-bold">Lokal Lageret</div>
<div class="small text-muted">CVR: 11223344</div>
</div>
</div>
</td>
<td><span class="badge bg-danger bg-opacity-10 text-danger rounded-pill px-3">Opsagt</span></td>
<td>15. Okt 2023</td>
<td>2.100 DKK</td>
<td class="text-end pe-4">
<button class="btn btn-sm btn-light border"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-light border text-danger"><i class="bi bi-trash"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer bg-white border-top-0 py-3">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mb-0">
<li class="page-item disabled"><a class="page-link" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<!-- Modals & Toasts -->
<div class="row">
<div class="col-12">
<h6 class="section-title">Modals & Overlays</h6>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Modal Eksempel</div>
<div class="card-body text-center py-5">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
Åbn Demo Modal
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Kort Varianter</div>
<div class="card-body">
<div class="card bg-primary text-white mb-3">
<div class="card-body">
<h5 class="card-title">Primary Card</h5>
<p class="card-text">Kort med accent farve baggrund.</p>
</div>
</div>
<div class="card border-primary mb-0">
<div class="card-body text-primary">
<h5 class="card-title">Border Card</h5>
<p class="card-text">Kort med farvet kant.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal Titel</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Dette er en standard modal boks. Den bruges til bekræftelser, detaljer eller formularer der ligger "ovenpå" siden.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Luk</button>
<button type="button" class="btn btn-primary">Gem ændringer</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,321 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Nordic Topbar Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
--border-radius: 12px;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding-top: 80px;
}
.navbar {
background: var(--bg-card);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
padding: 1rem 0;
border-bottom: 1px solid #eee;
}
.navbar-brand {
font-weight: 700;
color: var(--accent);
font-size: 1.25rem;
}
.nav-link {
color: var(--text-secondary);
padding: 0.6rem 1.2rem !important;
border-radius: var(--border-radius);
transition: all 0.2s;
font-weight: 500;
margin: 0 0.2rem;
}
.nav-link:hover, .nav-link.active {
background-color: var(--accent-light);
color: var(--accent);
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
transition: transform 0.2s;
background: var(--bg-card);
}
.table th {
font-weight: 600;
color: var(--text-secondary);
border-bottom-width: 1px;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background-color: var(--accent);
border-color: var(--accent);
padding: 0.6rem 1.5rem;
border-radius: 8px;
}
.btn-primary:hover {
background-color: #0a3655;
border-color: #0a3655;
}
.header-search {
background: var(--bg-body);
border: 1px solid #eee;
padding: 0.6rem 1.2rem;
border-radius: 8px;
width: 300px;
color: var(--text-primary);
}
.header-search:focus {
outline: none;
border-color: var(--accent);
}
.filter-btn {
background: white;
border: 1px solid #eee;
color: var(--text-secondary);
padding: 0.5rem 1.2rem;
border-radius: 20px;
font-size: 0.9rem;
transition: all 0.2s;
}
.filter-btn:hover, .filter-btn.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.dropdown-menu {
border: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border-radius: 12px;
padding: 0.5rem;
}
.dropdown-item {
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.dropdown-item:hover {
background-color: var(--accent-light);
color: var(--accent);
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center" href="#">
<div class="bg-primary text-white rounded p-1 me-2 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; background-color: var(--accent) !important;">
<i class="bi bi-hdd-network-fill" style="font-size: 16px;"></i>
</div>
BMC Hub
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto">
<li class="nav-item">
<a class="nav-link" href="index.html"><i class="bi bi-grid me-2"></i>Dashboard</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link active dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-people me-2"></i>Kunder
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="customers.html">Oversigt</a></li>
<li><a class="dropdown-item py-2" href="#">Opret ny kunde</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2" href="#">Rapporter</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-hdd-network me-2"></i>Hardware
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Alle Enheder</a></li>
<li><a class="dropdown-item py-2" href="#">Switches</a></li>
<li><a class="dropdown-item py-2" href="#">Firewalls</a></li>
<li><a class="dropdown-item py-2" href="#">Access Points</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-receipt me-2"></i>Fakturering
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Fakturaer</a></li>
<li><a class="dropdown-item py-2" href="#">Abonnementer</a></li>
<li><a class="dropdown-item py-2" href="#">Prisliste</a></li>
</ul>
</li>
</ul>
<div class="d-flex align-items-center gap-3">
<button class="btn btn-light rounded-circle border-0" style="background: var(--accent-light); color: var(--accent);"><i class="bi bi-bell"></i></button>
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none text-dark dropdown-toggle" data-bs-toggle="dropdown">
<img src="https://ui-avatars.com/api/?name=CT&background=0f4c75&color=fff" class="rounded-circle me-2" width="32">
<span class="small fw-bold" style="color: var(--text-primary)">Christian</span>
</a>
</div>
</div>
</div>
</div>
</nav>
<div class="container-fluid px-4 py-4">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Kunder</h2>
<p class="text-muted mb-0">Administrer dine kunder</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="header-search" placeholder="Søg kunde...">
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Opret Kunde</button>
</div>
</div>
<div class="mb-4 d-flex gap-2">
<button class="filter-btn active">Alle Kunder</button>
<button class="filter-btn">Aktive</button>
<button class="filter-btn">Inaktive</button>
<button class="filter-btn">VIP</button>
</div>
<div class="card p-4">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Virksomhed</th>
<th>Kontakt</th>
<th>CVR</th>
<th>Status</th>
<th>Hardware</th>
<th class="text-end"></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded bg-light d-flex align-items-center justify-content-center me-3 fw-bold" style="width: 40px; height: 40px; color: var(--accent);">AG</div>
<div>
<div class="fw-bold">Advokatgruppen A/S</div>
<div class="small text-muted">København K</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Jens Jensen</div>
<div class="small text-muted">jens@advokat.dk</div>
</td>
<td class="text-muted">12345678</td>
<td><span class="badge bg-success bg-opacity-10 text-success">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border fw-normal">Firewall</span>
<span class="badge bg-light text-dark border fw-normal">Switch</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded bg-light d-flex align-items-center justify-content-center me-3 fw-bold" style="width: 40px; height: 40px; color: var(--accent);">BB</div>
<div>
<div class="fw-bold">Byg & Bo ApS</div>
<div class="small text-muted">Aarhus C</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Mette Hansen</div>
<div class="small text-muted">mh@bygbo.dk</div>
</td>
<td class="text-muted">87654321</td>
<td><span class="badge bg-success bg-opacity-10 text-success">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border fw-normal">Router</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded bg-light d-flex align-items-center justify-content-center me-3 fw-bold" style="width: 40px; height: 40px; color: var(--accent);">CM</div>
<div>
<div class="fw-bold">Cafe Møller</div>
<div class="small text-muted">Odense M</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Peter Møller</div>
<div class="small text-muted">pm@cafe.dk</div>
</td>
<td class="text-muted">11223344</td>
<td><span class="badge bg-warning bg-opacity-10 text-warning">Afventer</span></td>
<td>
<span class="text-muted">-</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-center mt-4">
<nav>
<ul class="pagination">
<li class="page-item disabled"><a class="page-link border-0" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link border-0 rounded-circle mx-1" style="background-color: var(--accent);" href="#">1</a></li>
<li class="page-item"><a class="page-link border-0 text-dark" href="#">2</a></li>
<li class="page-item"><a class="page-link border-0 text-dark" href="#">3</a></li>
<li class="page-item"><a class="page-link border-0 text-dark" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,276 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Opret Kunde</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
--border-radius: 12px;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding-top: 80px;
}
.navbar {
background: var(--bg-card);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
padding: 1rem 0;
border-bottom: 1px solid #eee;
}
.navbar-brand {
font-weight: 700;
color: var(--accent);
font-size: 1.25rem;
}
.nav-link {
color: var(--text-secondary);
padding: 0.6rem 1.2rem !important;
border-radius: var(--border-radius);
transition: all 0.2s;
font-weight: 500;
margin: 0 0.2rem;
}
.nav-link:hover, .nav-link.active {
background-color: var(--accent-light);
color: var(--accent);
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
transition: transform 0.2s;
background: var(--bg-card);
}
.btn-primary {
background-color: var(--accent);
border-color: var(--accent);
padding: 0.6rem 1.5rem;
border-radius: 8px;
}
.btn-primary:hover {
background-color: #0a3655;
border-color: #0a3655;
}
.form-control, .form-select {
padding: 0.7rem 1rem;
border-radius: 8px;
border: 1px solid #eee;
}
.form-control:focus, .form-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(15, 76, 117, 0.1);
}
.form-label {
font-weight: 500;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.dropdown-menu {
border: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border-radius: 12px;
padding: 0.5rem;
}
.dropdown-item {
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.dropdown-item:hover {
background-color: var(--accent-light);
color: var(--accent);
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center" href="#">
<div class="bg-primary text-white rounded p-1 me-2 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; background-color: var(--accent) !important;">
<i class="bi bi-hdd-network-fill" style="font-size: 16px;"></i>
</div>
BMC Hub
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto">
<li class="nav-item">
<a class="nav-link" href="index.html"><i class="bi bi-grid me-2"></i>Dashboard</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link active dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-people me-2"></i>Kunder
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="customers.html">Oversigt</a></li>
<li><a class="dropdown-item py-2" href="#">Opret ny kunde</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2" href="#">Rapporter</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-hdd-network me-2"></i>Hardware
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Alle Enheder</a></li>
<li><a class="dropdown-item py-2" href="#">Switches</a></li>
<li><a class="dropdown-item py-2" href="#">Firewalls</a></li>
<li><a class="dropdown-item py-2" href="#">Access Points</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-receipt me-2"></i>Fakturering
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Fakturaer</a></li>
<li><a class="dropdown-item py-2" href="#">Abonnementer</a></li>
<li><a class="dropdown-item py-2" href="#">Prisliste</a></li>
</ul>
</li>
</ul>
<div class="d-flex align-items-center gap-3">
<button class="btn btn-light rounded-circle border-0" style="background: var(--accent-light); color: var(--accent);"><i class="bi bi-bell"></i></button>
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none text-dark dropdown-toggle" data-bs-toggle="dropdown">
<img src="https://ui-avatars.com/api/?name=CT&background=0f4c75&color=fff" class="rounded-circle me-2" width="32">
<span class="small fw-bold" style="color: var(--text-primary)">Christian</span>
</a>
</div>
</div>
</div>
</div>
</nav>
<div class="container-fluid px-4 py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">Opret Ny Kunde</h2>
<p class="text-muted mb-0">Udfyld oplysningerne herunder</p>
</div>
<a href="customers.html" class="btn btn-light"><i class="bi bi-arrow-left me-2"></i>Tilbage</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card p-4">
<form>
<h5 class="fw-bold mb-4 text-primary" style="color: var(--accent) !important;">Virksomhedsoplysninger</h5>
<div class="row g-3 mb-4">
<div class="col-md-12">
<label class="form-label">Virksomhedsnavn</label>
<input type="text" class="form-control" placeholder="F.eks. BMC Networks ApS">
</div>
<div class="col-md-6">
<label class="form-label">CVR Nummer</label>
<input type="text" class="form-control" placeholder="12345678">
</div>
<div class="col-md-6">
<label class="form-label">Branche</label>
<select class="form-select">
<option selected>Vælg branche...</option>
<option>IT & Teknologi</option>
<option>Håndværk</option>
<option>Detailhandel</option>
<option>Rådgivning</option>
</select>
</div>
</div>
<h5 class="fw-bold mb-4 text-primary" style="color: var(--accent) !important;">Kontaktperson</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Navn</label>
<input type="text" class="form-control" placeholder="Kontaktpersonens navn">
</div>
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" class="form-control" placeholder="mail@virksomhed.dk">
</div>
<div class="col-md-6">
<label class="form-label">Telefon</label>
<input type="tel" class="form-control" placeholder="+45 12 34 56 78">
</div>
<div class="col-md-6">
<label class="form-label">Stilling</label>
<input type="text" class="form-control" placeholder="F.eks. Direktør">
</div>
</div>
<h5 class="fw-bold mb-4 text-primary" style="color: var(--accent) !important;">Abonnement</h5>
<div class="row g-3 mb-4">
<div class="col-md-12">
<div class="card bg-light border-0 p-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="plan" id="plan1" checked>
<label class="form-check-label fw-bold" for="plan1">
Standard Support
</label>
<div class="small text-muted">Inkluderer telefon og email support i åbningstiden.</div>
</div>
</div>
</div>
<div class="col-md-12">
<div class="card bg-light border-0 p-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="plan" id="plan2">
<label class="form-check-label fw-bold" for="plan2">
Premium Support (24/7)
</label>
<div class="small text-muted">Døgnet rundt support og overvågning.</div>
</div>
</div>
</div>
</div>
<div class="mb-4">
<label class="form-label">Noter</label>
<textarea class="form-control" rows="3" placeholder="Eventuelle bemærkninger..."></textarea>
</div>
<hr class="my-4">
<div class="d-flex justify-content-end gap-2">
<button type="button" class="btn btn-light">Annuller</button>
<button type="submit" class="btn btn-primary px-4">Opret Kunde</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,329 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Nordic Topbar</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
--border-radius: 12px;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding-top: 80px;
}
.navbar {
background: var(--bg-card);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
padding: 1rem 0;
border-bottom: 1px solid #eee;
}
.navbar-brand {
font-weight: 700;
color: var(--accent);
font-size: 1.25rem;
}
.nav-link {
color: var(--text-secondary);
padding: 0.6rem 1.2rem !important;
border-radius: var(--border-radius);
transition: all 0.2s;
font-weight: 500;
margin: 0 0.2rem;
}
.nav-link:hover, .nav-link.active {
background-color: var(--accent-light);
color: var(--accent);
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
transition: transform 0.2s;
background: var(--bg-card);
}
.card:hover {
transform: translateY(-2px);
}
.stat-card h3 {
font-size: 2rem;
font-weight: 700;
color: var(--accent);
margin-bottom: 0;
}
.stat-card p {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.table th {
font-weight: 600;
color: var(--text-secondary);
border-bottom-width: 1px;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background-color: var(--accent);
border-color: var(--accent);
padding: 0.6rem 1.5rem;
border-radius: 8px;
}
.btn-primary:hover {
background-color: #0a3655;
border-color: #0a3655;
}
.header-search {
background: var(--bg-body);
border: 1px solid #eee;
padding: 0.6rem 1.2rem;
border-radius: 8px;
width: 300px;
color: var(--text-primary);
}
.header-search:focus {
outline: none;
border-color: var(--accent);
}
.dropdown-menu {
border: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border-radius: 12px;
padding: 0.5rem;
}
.dropdown-item {
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.dropdown-item:hover {
background-color: var(--accent-light);
color: var(--accent);
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center" href="#">
<div class="bg-primary text-white rounded p-1 me-2 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; background-color: var(--accent) !important;">
<i class="bi bi-hdd-network-fill" style="font-size: 16px;"></i>
</div>
BMC Hub
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto">
<li class="nav-item">
<a class="nav-link active" href="index.html"><i class="bi bi-grid me-2"></i>Dashboard</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-people me-2"></i>Kunder
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="customers.html">Oversigt</a></li>
<li><a class="dropdown-item py-2" href="#">Opret ny kunde</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2" href="#">Rapporter</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-hdd-network me-2"></i>Hardware
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Alle Enheder</a></li>
<li><a class="dropdown-item py-2" href="#">Switches</a></li>
<li><a class="dropdown-item py-2" href="#">Firewalls</a></li>
<li><a class="dropdown-item py-2" href="#">Access Points</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-receipt me-2"></i>Fakturering
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Fakturaer</a></li>
<li><a class="dropdown-item py-2" href="#">Abonnementer</a></li>
<li><a class="dropdown-item py-2" href="#">Prisliste</a></li>
</ul>
</li>
</ul>
<div class="d-flex align-items-center gap-3">
<button class="btn btn-light rounded-circle border-0" style="background: var(--accent-light); color: var(--accent);"><i class="bi bi-bell"></i></button>
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none text-dark dropdown-toggle" data-bs-toggle="dropdown">
<img src="https://ui-avatars.com/api/?name=CT&background=0f4c75&color=fff" class="rounded-circle me-2" width="32">
<span class="small fw-bold" style="color: var(--text-primary)">Christian</span>
</a>
</div>
</div>
</div>
</div>
</nav>
<div class="container-fluid px-4 py-4">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Dashboard</h2>
<p class="text-muted mb-0">Velkommen tilbage, Christian</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="header-search" placeholder="Søg...">
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Ny Opgave</button>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-3">
<div class="card stat-card p-4 h-100">
<div class="d-flex justify-content-between mb-2">
<p>Aktive Kunder</p>
<i class="bi bi-people text-primary" style="color: var(--accent) !important;"></i>
</div>
<h3>124</h3>
<small class="text-success"><i class="bi bi-arrow-up-short"></i> 12% denne måned</small>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card p-4 h-100">
<div class="d-flex justify-content-between mb-2">
<p>Hardware</p>
<i class="bi bi-hdd text-primary" style="color: var(--accent) !important;"></i>
</div>
<h3>856</h3>
<small class="text-muted">Enheder online</small>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card p-4 h-100">
<div class="d-flex justify-content-between mb-2">
<p>Support</p>
<i class="bi bi-ticket text-primary" style="color: var(--accent) !important;"></i>
</div>
<h3>12</h3>
<small class="text-warning">3 kræver handling</small>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card p-4 h-100">
<div class="d-flex justify-content-between mb-2">
<p>Omsætning</p>
<i class="bi bi-currency-dollar text-primary" style="color: var(--accent) !important;"></i>
</div>
<h3>450k</h3>
<small class="text-success">Over budget</small>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card p-4">
<h5 class="fw-bold mb-4">Seneste Aktiviteter</h5>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Kunde</th>
<th>Handling</th>
<th>Status</th>
<th class="text-end">Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td class="fw-bold">Advokatgruppen A/S</td>
<td>Firewall konfiguration</td>
<td><span class="badge bg-success bg-opacity-10 text-success">Fuldført</span></td>
<td class="text-end text-muted">10:23</td>
</tr>
<tr>
<td class="fw-bold">Byg & Bo ApS</td>
<td>Licens fornyelse</td>
<td><span class="badge bg-warning bg-opacity-10 text-warning">Afventer</span></td>
<td class="text-end text-muted">I går</td>
</tr>
<tr>
<td class="fw-bold">Cafe Møller</td>
<td>Netværksnedbrud</td>
<td><span class="badge bg-danger bg-opacity-10 text-danger">Kritisk</span></td>
<td class="text-end text-muted">I går</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card p-4 h-100">
<h5 class="fw-bold mb-4">System Status</h5>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="small fw-bold text-muted">CPU LOAD</span>
<span class="small fw-bold">24%</span>
</div>
<div class="progress" style="height: 8px; background-color: var(--accent-light);">
<div class="progress-bar" style="width: 24%; background-color: var(--accent);"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="small fw-bold text-muted">MEMORY</span>
<span class="small fw-bold">56%</span>
</div>
<div class="progress" style="height: 8px; background-color: var(--accent-light);">
<div class="progress-bar" style="width: 56%; background-color: var(--accent);"></div>
</div>
</div>
<div class="mt-auto p-3 rounded" style="background-color: var(--accent-light);">
<div class="d-flex">
<i class="bi bi-check-circle-fill text-success me-2"></i>
<small class="fw-bold" style="color: var(--accent)">Alle systemer kører optimalt.</small>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
--border-radius: 12px;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
background: var(--bg-card);
border: none;
border-radius: var(--border-radius);
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
width: 100%;
max-width: 400px;
padding: 2.5rem;
}
.brand-logo {
width: 48px;
height: 48px;
background-color: var(--accent);
color: white;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: 1.5rem;
}
.form-control {
padding: 0.8rem 1rem;
border-radius: 8px;
border: 1px solid #eee;
background-color: #fcfcfc;
}
.form-control:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(15, 76, 117, 0.1);
}
.btn-primary {
background-color: var(--accent);
border-color: var(--accent);
padding: 0.8rem;
border-radius: 8px;
font-weight: 600;
width: 100%;
margin-top: 1rem;
}
.btn-primary:hover {
background-color: #0a3655;
border-color: #0a3655;
}
.form-label {
font-weight: 500;
font-size: 0.9rem;
color: var(--text-primary);
}
</style>
</head>
<body>
<div class="login-card">
<div class="d-flex flex-column align-items-center text-center mb-4">
<div class="brand-logo">
<i class="bi bi-hdd-network-fill"></i>
</div>
<h4 class="fw-bold mb-1">Velkommen tilbage</h4>
<p class="text-muted small">Log ind for at få adgang til BMC Hub</p>
</div>
<form>
<div class="mb-3">
<label class="form-label">Email adresse</label>
<input type="email" class="form-control" placeholder="navn@bmcnetworks.dk" value="christian@bmcnetworks.dk">
</div>
<div class="mb-3">
<label class="form-label">Adgangskode</label>
<input type="password" class="form-control" value="password123">
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="remember">
<label class="form-check-label small text-muted" for="remember">Husk mig</label>
</div>
<a href="#" class="small text-decoration-none" style="color: var(--accent)">Glemt kode?</a>
</div>
<button type="submit" class="btn btn-primary" onclick="window.location.href='index.html'; return false;">Log Ind</button>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Indstillinger</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
--border-radius: 12px;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding-top: 80px;
}
.navbar {
background: var(--bg-card);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
padding: 1rem 0;
border-bottom: 1px solid #eee;
}
.navbar-brand {
font-weight: 700;
color: var(--accent);
font-size: 1.25rem;
}
.nav-link {
color: var(--text-secondary);
padding: 0.6rem 1.2rem !important;
border-radius: var(--border-radius);
transition: all 0.2s;
font-weight: 500;
margin: 0 0.2rem;
}
.nav-link:hover, .nav-link.active {
background-color: var(--accent-light);
color: var(--accent);
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
transition: transform 0.2s;
background: var(--bg-card);
}
.btn-primary {
background-color: var(--accent);
border-color: var(--accent);
padding: 0.6rem 1.5rem;
border-radius: 8px;
}
.btn-primary:hover {
background-color: #0a3655;
border-color: #0a3655;
}
.settings-nav .nav-link {
display: flex;
align-items: center;
padding: 0.8rem 1rem !important;
margin-bottom: 0.5rem;
color: var(--text-primary);
border-radius: 8px;
}
.settings-nav .nav-link:hover {
background-color: var(--bg-body);
}
.settings-nav .nav-link.active {
background-color: var(--accent-light);
color: var(--accent);
font-weight: 600;
}
.form-control, .form-select {
padding: 0.7rem 1rem;
border-radius: 8px;
border: 1px solid #eee;
}
.form-control:focus, .form-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(15, 76, 117, 0.1);
}
.dropdown-menu {
border: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border-radius: 12px;
padding: 0.5rem;
}
.dropdown-item {
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.dropdown-item:hover {
background-color: var(--accent-light);
color: var(--accent);
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center" href="#">
<div class="bg-primary text-white rounded p-1 me-2 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px; background-color: var(--accent) !important;">
<i class="bi bi-hdd-network-fill" style="font-size: 16px;"></i>
</div>
BMC Hub
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto">
<li class="nav-item">
<a class="nav-link" href="index.html"><i class="bi bi-grid me-2"></i>Dashboard</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-people me-2"></i>Kunder
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="customers.html">Oversigt</a></li>
<li><a class="dropdown-item py-2" href="#">Opret ny kunde</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item py-2" href="#">Rapporter</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-hdd-network me-2"></i>Hardware
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Alle Enheder</a></li>
<li><a class="dropdown-item py-2" href="#">Switches</a></li>
<li><a class="dropdown-item py-2" href="#">Firewalls</a></li>
<li><a class="dropdown-item py-2" href="#">Access Points</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-receipt me-2"></i>Fakturering
</a>
<ul class="dropdown-menu mt-2">
<li><a class="dropdown-item py-2" href="#">Fakturaer</a></li>
<li><a class="dropdown-item py-2" href="#">Abonnementer</a></li>
<li><a class="dropdown-item py-2" href="#">Prisliste</a></li>
</ul>
</li>
</ul>
<div class="d-flex align-items-center gap-3">
<button class="btn btn-light rounded-circle border-0" style="background: var(--accent-light); color: var(--accent);"><i class="bi bi-bell"></i></button>
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none text-dark dropdown-toggle" data-bs-toggle="dropdown">
<img src="https://ui-avatars.com/api/?name=CT&background=0f4c75&color=fff" class="rounded-circle me-2" width="32">
<span class="small fw-bold" style="color: var(--text-primary)">Christian</span>
</a>
</div>
</div>
</div>
</div>
</nav>
<div class="container-fluid px-4 py-4">
<div class="row g-4">
<div class="col-lg-3">
<div class="card p-3">
<div class="nav flex-column settings-nav">
<a class="nav-link active" href="#"><i class="bi bi-person me-3"></i>Min Profil</a>
<a class="nav-link" href="#"><i class="bi bi-shield-lock me-3"></i>Sikkerhed</a>
<a class="nav-link" href="#"><i class="bi bi-bell me-3"></i>Notifikationer</a>
<a class="nav-link" href="#"><i class="bi bi-palette me-3"></i>Udseende</a>
<hr class="my-2">
<a class="nav-link text-danger" href="login.html"><i class="bi bi-box-arrow-right me-3"></i>Log ud</a>
</div>
</div>
</div>
<div class="col-lg-9">
<div class="card p-4 mb-4">
<h4 class="fw-bold mb-4">Min Profil</h4>
<div class="d-flex align-items-center mb-4">
<img src="https://ui-avatars.com/api/?name=CT&background=0f4c75&color=fff&size=128" class="rounded-circle me-4" width="80">
<div>
<button class="btn btn-outline-secondary btn-sm me-2">Skift Billede</button>
<button class="btn btn-link btn-sm text-danger text-decoration-none">Fjern</button>
</div>
</div>
<form>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Fornavn</label>
<input type="text" class="form-control" value="Christian">
</div>
<div class="col-md-6">
<label class="form-label">Efternavn</label>
<input type="text" class="form-control" value="Thomas">
</div>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" value="christian@bmcnetworks.dk">
</div>
<div class="mb-3">
<label class="form-label">Rolle</label>
<input type="text" class="form-control" value="Administrator" disabled>
</div>
<div class="d-flex justify-content-end mt-4">
<button type="button" class="btn btn-light me-2">Annuller</button>
<button type="submit" class="btn btn-primary">Gem Ændringer</button>
</div>
</form>
</div>
<div class="card p-4">
<h4 class="fw-bold mb-4">Præferencer</h4>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="emailNotif" checked>
<label class="form-check-label" for="emailNotif">Modtag email notifikationer</label>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="darkMode">
<label class="form-check-label" for="darkMode">Dark Mode (Beta)</label>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

139
docs/designplan.md Normal file
View File

@ -0,0 +1,139 @@
# BMC Hub - Design System & UI Plan
## 1. Design Filosofi: "Nordic Top"
Dette designdokument beskriver UI/UX-strategien for BMC Hub, baseret på templaten **10_nordic_top**. Designet er forankret i skandinavisk minimalisme med fokus på funktionalitet, luft og overskuelighed.
**Nøgleprincipper:**
* **Fuld Bredde (Fluid Layout):** Udnytter hele skærmens bredde for maksimalt overblik, ideelt til store datatabeller og dashboards.
* **Top-Navigation:** En fastgjort topbar frigiver plads i siderne og giver en velkendt, hierarkisk navigationsstruktur med dropdowns.
* **Kort-baseret UI:** Indhold grupperes i "cards" med bløde skygger og rundede hjørner for at skabe visuelt hierarki.
* **Accent-farver:** En dyb blå (`#0f4c75`) bruges konsekvent til handlinger og branding, suppleret af lyse baggrunde.
---
## 2. Visuel Identitet
### Farvepalette
Designet benytter en stram palette defineret via CSS variabler for nem vedligeholdelse.
| Variabel | Farvekode | Anvendelse |
| :--- | :--- | :--- |
| `--accent` | `#0f4c75` (Dyb Blå) | Primære knapper, aktiv navigation, branding, ikoner |
| `--accent-light` | `#eef2f5` (Lys Blå/Grå) | Hover-effekter, baggrund for ikoner, aktive menupunkter |
| `--text-primary` | `#2c3e50` (Mørkegrå) | Overskrifter, primær tekst |
| `--text-secondary` | `#6c757d` (Mellemgrå) | Brødtekst, labels, meta-data |
| `--bg-body` | `#f8f9fa` (Off-white) | Sidens baggrundsfarve |
| `--bg-card` | `#ffffff` (Hvid) | Kort, navbar, modaler |
### Typografi
* **Font:** 'Inter', system-ui, sans-serif.
* **Vægtning:**
* **Bold (700):** Overskrifter, KPI-tal.
* **Medium (500):** Labels, menupunkter, knaptekst.
* **Regular (400):** Brødtekst.
---
## 3. Komponentbibliotek
### Navigation (Topbar)
* **Struktur:** Logo venstre, Menu center, Profil/Notifikationer højre.
* **Dropdowns:** Bruges til underkategorier (f.eks. Kunder -> Oversigt, Opret, Rapporter).
* **Aktiv Tilstand:** Markeres med `--accent-light` baggrund og `--accent` tekstfarve.
### Knapper (Buttons)
* **Primær:** Fuld `--accent` baggrund. Bruges til hovedhandlinger (f.eks. "Opret Kunde", "Gem").
* **Sekundær/Filter:** Hvid baggrund med border. Bliver farvet ved aktiv/hover.
* **Ikon-knapper:** Runde knapper til hurtige handlinger (f.eks. notifikationer, rediger).
### Tabeller (Data Lists)
* **Design:** Clean look uden lodrette streger.
* **Features:**
* Avatar/Initialer for virksomheder.
* Status badges (pille-formede).
* Handlingsmenu (tre prikker) yderst til højre.
* Hover-effekt på rækker for læsbarhed.
### Formularer (Inputs)
* **Styling:** Store, luftige inputfelter med blød border.
* **Fokus:** Tydelig `--accent` border og skygge ved fokus.
* **Gruppering:** Logisk opdeling med overskrifter (f.eks. "Virksomhedsoplysninger", "Kontaktperson").
* **Kort-valg:** Radio buttons designet som klikbare kort (se Abonnement-valg i `form.html`).
### Status Indikatorer (Badges)
Bruges til at vise tilstande hurtigt i tabeller.
* 🟢 **Success:** Aktiv, Fuldført (Grøn baggrund/tekst).
* 🟡 **Warning:** Afventer, Pause (Gul/Orange baggrund/tekst).
* 🔴 **Danger:** Kritisk, Opsagt (Rød baggrund/tekst).
* ⚪ **Neutral:** Hardware typer (Grå baggrund/tekst).
---
## 4. Side-Skabeloner (Templates)
Systemet består af 5 kerneskabeloner der dækker de fleste behov:
### 1. Dashboard (`index.html`)
* **Formål:** Give hurtigt overblik over forretningens tilstand.
* **Indhold:**
* 4 KPI-kort i toppen (Kunder, Hardware, Support, Omsætning).
* Grafisk visning (placeholder) eller lister.
* "Seneste Aktiviteter" tabel.
* System Status panel (Server load, memory).
### 2. Listevisning (`customers.html`)
* **Formål:** Administration af store datamængder.
* **Indhold:**
* Søgefelt og "Opret" knap i header.
* Filter-bar (Alle, Aktive, Inaktive, VIP).
* Datatabel med rig information.
* Paginering i bunden.
### 3. Formular / Opret (`form.html`)
* **Formål:** Indtastning af data (CRUD operationer).
* **Indhold:**
* "Tilbage" knap for nem navigation.
* Opdelt formular i sektioner.
* Forskellige input typer: Text, Email, Tel, Select, Radio Cards, Textarea.
### 4. Indstillinger (`settings.html`)
* **Formål:** Brugerprofil og systemkonfiguration.
* **Indhold:**
* 2-kolonne layout.
* Venstre: Vertikal navigationsmenu.
* Højre: Indholdspaneler (Profilbillede, Stamdata, Toggles for notifikationer/Dark mode).
### 5. Login (`login.html`)
* **Formål:** Adgangskontrol.
* **Indhold:**
* Centreret kort på fuld skærm.
* Logo og velkomsttekst.
* Email/Password felter.
* "Husk mig" og "Glemt kode" funktioner.
---
## 5. Brugerrejse & Navigation
### Flow Eksempel: Opret ny kunde
1. **Login:** Bruger logger ind via `login.html`.
2. **Dashboard:** Lander på `index.html` og ser overblik.
3. **Navigation:** Klikker på "Kunder" i topmenuen -> Vælger "Oversigt" (eller direkte "Opret ny kunde").
4. **Liste:** Ser listen på `customers.html`, søger evt. efter eksisterende.
5. **Handling:** Klikker "Opret Kunde" knappen.
6. **Formular:** Udfylder data på `form.html`.
* Vælger branche via dropdown.
* Vælger abonnementspakke via visuelle kort.
7. **Afslutning:** Klikker "Opret Kunde" -> Sendes tilbage til `customers.html` (eller detaljevisning).
---
## 6. Teknisk Implementation
* **Framework:** Bootstrap 5.3.2 (CSS/JS).
* **Ikoner:** Bootstrap Icons (CDN).
* **Layout:** `container-fluid` for fuld bredde, `row` og `col-*` grid system.
* **Responsivitet:**
* Navbar kollapser til "burger menu" på mobil.
* Tabeller bliver scrollbare horisontalt (`table-responsive`).
* Grid system tilpasser sig fra 4 kolonner (desktop) til 1 kolonne (mobil).

39
main.py
View File

@ -12,12 +12,18 @@ from contextlib import asynccontextmanager
from app.core.config import settings from app.core.config import settings
from app.core.database import init_db from app.core.database import init_db
from app.routers import (
customers, # Import Feature Routers
hardware, from app.auth.backend import router as auth_api
billing, from app.auth.backend import views as auth_views
system, from app.customers.backend import router as customers_api
) from app.customers.backend import views as customers_views
from app.contacts.backend import router as contacts_api
from app.contacts.backend import views as contacts_views
from app.hardware.backend import router as hardware_api
from app.billing.backend import router as billing_api
from app.system.backend import router as system_api
from app.dashboard.backend import views as dashboard_views
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
@ -74,19 +80,22 @@ app.add_middleware(
) )
# Include routers # Include routers
app.include_router(customers.router, prefix="/api/v1", tags=["Customers"]) app.include_router(auth_api.router, prefix="/api/v1/auth", tags=["Authentication"])
app.include_router(hardware.router, prefix="/api/v1", tags=["Hardware"]) app.include_router(customers_api.router, prefix="/api/v1", tags=["Customers"])
app.include_router(billing.router, prefix="/api/v1", tags=["Billing"]) app.include_router(contacts_api.router, prefix="/api/v1", tags=["Contacts"])
app.include_router(system.router, prefix="/api/v1", tags=["System"]) app.include_router(hardware_api.router, prefix="/api/v1", tags=["Hardware"])
app.include_router(billing_api.router, prefix="/api/v1", tags=["Billing"])
app.include_router(system_api.router, prefix="/api/v1", tags=["System"])
# Frontend Routers
app.include_router(auth_views.router, tags=["Frontend"])
app.include_router(dashboard_views.router, tags=["Frontend"])
app.include_router(customers_views.router, tags=["Frontend"])
app.include_router(contacts_views.router, tags=["Frontend"])
# Serve static files (UI) # Serve static files (UI)
app.mount("/static", StaticFiles(directory="static", html=True), name="static") app.mount("/static", StaticFiles(directory="static", html=True), name="static")
@app.get("/")
async def root():
"""Redirect to dashboard"""
return RedirectResponse(url="/static/index.html")
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
"""Health check endpoint""" """Health check endpoint"""

View File

@ -0,0 +1,215 @@
-- Migration 002: Authentication & Authorization System
-- Based on OmniSync auth implementation
-- Users table
CREATE TABLE IF NOT EXISTS users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE,
is_superadmin BOOLEAN DEFAULT FALSE,
failed_login_attempts INTEGER DEFAULT 0,
locked_until TIMESTAMP,
last_login_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Groups table
CREATE TABLE IF NOT EXISTS groups (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Permissions table
CREATE TABLE IF NOT EXISTS permissions (
id SERIAL PRIMARY KEY,
code VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
category VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- User-Group relationship
CREATE TABLE IF NOT EXISTS user_groups (
user_id INTEGER REFERENCES users(user_id) ON DELETE CASCADE,
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, group_id)
);
-- Group-Permission relationship
CREATE TABLE IF NOT EXISTS group_permissions (
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (group_id, permission_id)
);
-- Sessions table (for JWT token revocation)
CREATE TABLE IF NOT EXISTS sessions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(user_id) ON DELETE CASCADE,
token_jti VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
revoked BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Audit log table
CREATE TABLE IF NOT EXISTS audit_log (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(user_id) ON DELETE SET NULL,
username VARCHAR(100),
action VARCHAR(100) NOT NULL,
resource_type VARCHAR(100),
resource_id INTEGER,
resource_name VARCHAR(255),
before_value TEXT,
after_value TEXT,
success BOOLEAN DEFAULT TRUE,
error_message TEXT,
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_sessions_jti ON sessions(token_jti);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action);
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log(created_at);
-- Insert default permissions
INSERT INTO permissions (code, description, category) VALUES
-- Customers
('customers.view', 'View customers', 'customers'),
('customers.edit', 'Edit customers', 'customers'),
('customers.create', 'Create customers', 'customers'),
('customers.delete', 'Delete customers', 'customers'),
-- Hardware
('hardware.view', 'View hardware', 'hardware'),
('hardware.edit', 'Edit hardware', 'hardware'),
('hardware.create', 'Create hardware', 'hardware'),
('hardware.delete', 'Delete hardware', 'hardware'),
('hardware.assign', 'Assign hardware to customers', 'hardware'),
-- Billing
('billing.view', 'View billing information', 'billing'),
('billing.edit', 'Edit billing information', 'billing'),
('billing.approve', 'Approve billing items', 'billing'),
-- System
('system.view', 'View system settings', 'system'),
('system.edit', 'Edit system settings', 'system'),
-- Reports
('reports.view', 'View reports', 'reports'),
('reports.export', 'Export reports', 'reports'),
-- Audit
('audit.view', 'View audit logs', 'audit'),
-- Admin
('users.manage', 'Manage users and groups', 'admin'),
('permissions.manage', 'Manage permissions', 'admin'),
('system.admin', 'Full system administration', 'admin')
ON CONFLICT (code) DO NOTHING;
-- Insert default groups
INSERT INTO groups (name, description) VALUES
('Administrators', 'Full system access'),
('Managers', 'Can manage customers and billing'),
('Technicians', 'Can manage hardware and assignments'),
('Viewers', 'Read-only access')
ON CONFLICT (name) DO NOTHING;
-- Assign permissions to Administrators group (all permissions)
INSERT INTO group_permissions (group_id, permission_id)
SELECT g.id, p.id
FROM groups g
CROSS JOIN permissions p
WHERE g.name = 'Administrators'
ON CONFLICT DO NOTHING;
-- Assign permissions to Managers group
INSERT INTO group_permissions (group_id, permission_id)
SELECT g.id, p.id
FROM groups g
CROSS JOIN permissions p
WHERE g.name = 'Managers' AND p.code IN (
'customers.view', 'customers.edit', 'customers.create',
'hardware.view',
'billing.view', 'billing.edit', 'billing.approve',
'reports.view', 'reports.export'
)
ON CONFLICT DO NOTHING;
-- Assign permissions to Technicians group
INSERT INTO group_permissions (group_id, permission_id)
SELECT g.id, p.id
FROM groups g
CROSS JOIN permissions p
WHERE g.name = 'Technicians' AND p.code IN (
'customers.view',
'hardware.view', 'hardware.edit', 'hardware.create', 'hardware.assign',
'reports.view'
)
ON CONFLICT DO NOTHING;
-- Assign permissions to Viewers group
INSERT INTO group_permissions (group_id, permission_id)
SELECT g.id, p.id
FROM groups g
CROSS JOIN permissions p
WHERE g.name = 'Viewers' AND p.code IN (
'customers.view',
'hardware.view',
'billing.view',
'reports.view'
)
ON CONFLICT DO NOTHING;
-- Create default admin user (password: admin123, hashed with SHA256)
-- SHA256 hash of 'admin123'
INSERT INTO users (username, email, password_hash, full_name, is_superadmin, is_active)
VALUES (
'admin',
'admin@bmcnetworks.dk',
'240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9',
'System Administrator',
TRUE,
TRUE
)
ON CONFLICT (username) DO NOTHING;
-- Assign admin user to Administrators group
INSERT INTO user_groups (user_id, group_id)
SELECT u.user_id, g.id
FROM users u
CROSS JOIN groups g
WHERE u.username = 'admin' AND g.name = 'Administrators'
ON CONFLICT DO NOTHING;
-- Update timestamp trigger function
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Add trigger to users table
DROP TRIGGER IF EXISTS update_users_updated_at ON users;
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View File

@ -0,0 +1,71 @@
-- Migration 003: Udvid customers tabel med felter fra OmniSync
-- Dato: 6. december 2025
-- Tilføj nye kolonner til customers tabel
ALTER TABLE customers
ADD COLUMN IF NOT EXISTS cvr_number VARCHAR(20) UNIQUE,
ADD COLUMN IF NOT EXISTS email_domain VARCHAR(255),
ADD COLUMN IF NOT EXISTS city VARCHAR(100),
ADD COLUMN IF NOT EXISTS postal_code VARCHAR(10),
ADD COLUMN IF NOT EXISTS country VARCHAR(2) DEFAULT 'DK',
ADD COLUMN IF NOT EXISTS website VARCHAR(255),
ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE,
-- vTiger integration
ADD COLUMN IF NOT EXISTS vtiger_id VARCHAR(50) UNIQUE,
ADD COLUMN IF NOT EXISTS vtiger_account_no VARCHAR(50),
ADD COLUMN IF NOT EXISTS last_synced_at TIMESTAMP,
-- E-conomic integration
ADD COLUMN IF NOT EXISTS economic_customer_number INTEGER,
ADD COLUMN IF NOT EXISTS payment_terms_number INTEGER,
ADD COLUMN IF NOT EXISTS payment_terms_type VARCHAR(50),
ADD COLUMN IF NOT EXISTS vat_zone_number INTEGER,
ADD COLUMN IF NOT EXISTS customer_group_number INTEGER,
ADD COLUMN IF NOT EXISTS barred BOOLEAN DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS currency_code VARCHAR(3) DEFAULT 'DKK',
ADD COLUMN IF NOT EXISTS ean VARCHAR(13),
ADD COLUMN IF NOT EXISTS public_entry_number VARCHAR(50),
-- Additional contact info
ADD COLUMN IF NOT EXISTS invoice_email VARCHAR(255),
ADD COLUMN IF NOT EXISTS mobile_phone VARCHAR(50),
-- Customer relationships
ADD COLUMN IF NOT EXISTS parent_customer_id INTEGER REFERENCES customers(id),
ADD COLUMN IF NOT EXISTS billing_customer_id INTEGER REFERENCES customers(id);
-- Opret nye indexes
CREATE INDEX IF NOT EXISTS idx_customers_cvr ON customers(cvr_number);
CREATE INDEX IF NOT EXISTS idx_customers_vtiger ON customers(vtiger_id);
CREATE INDEX IF NOT EXISTS idx_customers_economic ON customers(economic_customer_number);
CREATE INDEX IF NOT EXISTS idx_customers_active ON customers(is_active);
CREATE INDEX IF NOT EXISTS idx_customers_parent ON customers(parent_customer_id);
CREATE INDEX IF NOT EXISTS idx_customers_billing ON customers(billing_customer_id);
CREATE INDEX IF NOT EXISTS idx_customers_city ON customers(city);
CREATE INDEX IF NOT EXISTS idx_customers_name ON customers(name);
-- Opdater trigger function hvis den ikke findes
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Tilføj trigger til customers (hvis ikke allerede eksisterer)
DROP TRIGGER IF EXISTS update_customers_updated_at ON customers;
CREATE TRIGGER update_customers_updated_at
BEFORE UPDATE ON customers
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Tilføj kommentarer til dokumentation
COMMENT ON COLUMN customers.cvr_number IS 'Danish CVR (company registration) number';
COMMENT ON COLUMN customers.email_domain IS 'Primary email domain for customer';
COMMENT ON COLUMN customers.vtiger_id IS 'Reference to vTiger CRM account ID';
COMMENT ON COLUMN customers.economic_customer_number IS 'Reference to e-conomic customer number';
COMMENT ON COLUMN customers.parent_customer_id IS 'Parent company if part of a group';
COMMENT ON COLUMN customers.billing_customer_id IS 'Billing account if different from this customer';
COMMENT ON COLUMN customers.barred IS 'Whether customer is barred from ordering';

View File

@ -0,0 +1,78 @@
-- Migration 004: Opret contacts og customer relationships tabeller
-- Dato: 6. december 2025
-- Contacts tabel
CREATE TABLE IF NOT EXISTS contacts (
id SERIAL PRIMARY KEY,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(255),
phone VARCHAR(50),
mobile VARCHAR(50),
title VARCHAR(100),
department VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
vtiger_id VARCHAR(50) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Junction tabel for mange-til-mange relation mellem contacts og customers
CREATE TABLE IF NOT EXISTS contact_companies (
id SERIAL PRIMARY KEY,
contact_id INTEGER NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
is_primary BOOLEAN DEFAULT FALSE,
role VARCHAR(100),
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(contact_id, customer_id)
);
-- Customer relationships tabel (for parent/child og billing strukturer)
CREATE TABLE IF NOT EXISTS customer_relationships (
id SERIAL PRIMARY KEY,
parent_customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
child_customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
relationship_type VARCHAR(50) NOT NULL CHECK (relationship_type IN ('parent_child', 'billing', 'partnership', 'subsidiary')),
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(parent_customer_id, child_customer_id, relationship_type)
);
-- Indexes for contacts
CREATE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email);
CREATE INDEX IF NOT EXISTS idx_contacts_active ON contacts(is_active);
CREATE INDEX IF NOT EXISTS idx_contacts_vtiger ON contacts(vtiger_id);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(first_name, last_name);
-- Indexes for contact_companies
CREATE INDEX IF NOT EXISTS idx_contact_companies_contact ON contact_companies(contact_id);
CREATE INDEX IF NOT EXISTS idx_contact_companies_customer ON contact_companies(customer_id);
CREATE INDEX IF NOT EXISTS idx_contact_companies_primary ON contact_companies(customer_id, is_primary);
-- Indexes for customer_relationships
CREATE INDEX IF NOT EXISTS idx_customer_relationships_parent ON customer_relationships(parent_customer_id);
CREATE INDEX IF NOT EXISTS idx_customer_relationships_child ON customer_relationships(child_customer_id);
CREATE INDEX IF NOT EXISTS idx_customer_relationships_type ON customer_relationships(relationship_type);
-- Triggers for updated_at
DROP TRIGGER IF EXISTS update_contacts_updated_at ON contacts;
CREATE TRIGGER update_contacts_updated_at
BEFORE UPDATE ON contacts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
DROP TRIGGER IF EXISTS update_customer_relationships_updated_at ON customer_relationships;
CREATE TRIGGER update_customer_relationships_updated_at
BEFORE UPDATE ON customer_relationships
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Kommentarer
COMMENT ON TABLE contacts IS 'Contact persons associated with one or more customers';
COMMENT ON TABLE contact_companies IS 'Many-to-many junction table linking contacts to customers';
COMMENT ON TABLE customer_relationships IS 'Hierarchical and billing relationships between customers';
COMMENT ON COLUMN contact_companies.is_primary IS 'Whether this is the primary contact for this customer';
COMMENT ON COLUMN customer_relationships.relationship_type IS 'Type of relationship: parent_child, billing, partnership, subsidiary';

View File

@ -0,0 +1,60 @@
-- Migration: Add vendors table
-- Similar to customers but for suppliers/vendors
CREATE TABLE IF NOT EXISTS vendors (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
cvr_number VARCHAR(20) UNIQUE,
-- Contact information
email VARCHAR(255),
phone VARCHAR(50),
website VARCHAR(255),
-- Address
address TEXT,
postal_code VARCHAR(10),
city VARCHAR(100),
country VARCHAR(100) DEFAULT 'Danmark',
-- Integration IDs
economic_supplier_number INTEGER,
-- Vendor specific
domain VARCHAR(255),
email_pattern TEXT,
category VARCHAR(50) DEFAULT 'general',
priority INTEGER DEFAULT 50,
-- Metadata
notes TEXT,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by_user_id INTEGER,
updated_by_user_id INTEGER
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_vendors_name ON vendors(name);
CREATE INDEX IF NOT EXISTS idx_vendors_cvr ON vendors(cvr_number);
CREATE INDEX IF NOT EXISTS idx_vendors_domain ON vendors(domain);
CREATE INDEX IF NOT EXISTS idx_vendors_active ON vendors(is_active);
-- Updated timestamp trigger
CREATE OR REPLACE FUNCTION update_vendors_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER vendors_updated_at_trigger
BEFORE UPDATE ON vendors
FOR EACH ROW
EXECUTE FUNCTION update_vendors_updated_at();
COMMENT ON TABLE vendors IS 'Suppliers and vendors';
COMMENT ON COLUMN vendors.category IS 'Vendor category: hardware, software, telecom, services, etc.';
COMMENT ON COLUMN vendors.priority IS 'Priority level 1-100, higher = more important';

View File

@ -5,3 +5,6 @@ pydantic==2.10.3
pydantic-settings==2.6.1 pydantic-settings==2.6.1
python-dotenv==1.0.1 python-dotenv==1.0.1
python-multipart==0.0.17 python-multipart==0.0.17
jinja2==3.1.4
pyjwt==2.9.0
aiohttp==3.10.10

View File

@ -0,0 +1,245 @@
"""
Import customers and contacts from OmniSync database with better error handling
"""
import psycopg2
from psycopg2.extras import RealDictCursor
import sqlite3
import os
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
OMNISYNC_DB = '/omnisync_data/fakturering.db'
def get_postgres_connection():
"""Get PostgreSQL connection"""
database_url = os.getenv('DATABASE_URL', 'postgresql://bmc_hub:bmc_hub@postgres:5432/bmc_hub')
return psycopg2.connect(database_url, cursor_factory=RealDictCursor)
def get_sqlite_connection():
"""Get SQLite connection to OmniSync database"""
return sqlite3.connect(OMNISYNC_DB)
def import_customers():
"""Import customers from OmniSync"""
sqlite_conn = get_sqlite_connection()
sqlite_cursor = sqlite_conn.cursor()
imported = 0
skipped = 0
errors = []
try:
# Get active customers from OmniSync
sqlite_cursor.execute("""
SELECT
name, cvr_number, email, phone, address, city, postal_code,
country, website, vtiger_id, economic_customer_number,
email_domain, created_at
FROM customers
WHERE active = 1 AND deleted_at IS NULL
ORDER BY name
""")
customers = sqlite_cursor.fetchall()
logger.info(f"📥 Found {len(customers)} active customers in OmniSync")
for row in customers:
name, cvr, email, phone, address, city, postal_code, country, website, vtiger_id, economic_no, email_domain, created_at = row
# Skip if no name
if not name or name.strip() == '':
skipped += 1
continue
# Insert each customer individually
individual_conn = get_postgres_connection()
individual_cursor = individual_conn.cursor()
try:
individual_cursor.execute("""
INSERT INTO customers (
name, cvr_number, email, phone, address, postal_code, city,
country, website, vtiger_id, economic_customer_number,
email_domain, is_active, created_at
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (cvr_number) DO UPDATE SET
name = EXCLUDED.name,
email = EXCLUDED.email,
phone = EXCLUDED.phone,
address = EXCLUDED.address,
postal_code = EXCLUDED.postal_code,
city = EXCLUDED.city,
website = EXCLUDED.website,
vtiger_id = EXCLUDED.vtiger_id,
economic_customer_number = EXCLUDED.economic_customer_number
RETURNING id
""", (
name, cvr, email, phone, address, postal_code, city,
country or 'Danmark', website, vtiger_id, economic_no,
email_domain, True, created_at
))
result = individual_cursor.fetchone()
individual_conn.commit()
if result:
imported += 1
if imported % 50 == 0:
logger.info(f" Imported {imported} customers...")
except Exception as e:
error_msg = str(e)[:100]
if imported + skipped < 10: # Only log first 10 errors in detail
logger.warning(f" ⚠️ Could not import '{name}': {error_msg}")
errors.append((name, error_msg))
skipped += 1
finally:
individual_cursor.close()
individual_conn.close()
logger.info(f"✅ Customers: {imported} imported, {skipped} skipped")
if len(errors) > 10:
logger.info(f" (Suppressed {len(errors)-10} error messages)")
return imported
except Exception as e:
logger.error(f"❌ Customer import failed: {e}")
raise
finally:
sqlite_cursor.close()
sqlite_conn.close()
def import_contacts():
"""Import contacts from OmniSync"""
sqlite_conn = get_sqlite_connection()
sqlite_cursor = sqlite_conn.cursor()
imported = 0
skipped = 0
errors = []
try:
# Get contacts
sqlite_cursor.execute("""
SELECT
c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile,
c.title, c.department, c.active, c.vtiger_id, c.created_at
FROM contacts c
WHERE c.active = 1 AND c.deleted_at IS NULL
ORDER BY c.last_name, c.first_name
""")
contacts = sqlite_cursor.fetchall()
logger.info(f"📥 Found {len(contacts)} active contacts in OmniSync")
for row in contacts:
omnisync_id, first_name, last_name, email, phone, mobile, title, department, active, vtiger_id, created_at = row
# Skip if no name
if not first_name and not last_name:
skipped += 1
continue
# Individual connection per contact
postgres_conn = get_postgres_connection()
postgres_cursor = postgres_conn.cursor()
try:
# Insert contact
postgres_cursor.execute("""
INSERT INTO contacts (
first_name, last_name, email, phone, mobile,
title, department, is_active, vtiger_id, created_at
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (
first_name or '', last_name or '', email, phone, mobile,
title, department, bool(active), vtiger_id, created_at
))
result = postgres_cursor.fetchone()
contact_id = result['id']
# Get company relationships from OmniSync
sqlite_cursor.execute("""
SELECT customer_id, is_primary, role, notes
FROM contact_companies
WHERE contact_id = ?
""", (omnisync_id,))
companies = sqlite_cursor.fetchall()
# Link to companies in BMC Hub
for company_row in companies:
customer_id_omnisync, is_primary, role, notes = company_row
# Find matching customer in BMC Hub by vtiger_id or name
# First get the OmniSync customer
sqlite_cursor.execute("SELECT vtiger_id, name FROM customers WHERE id = ?", (customer_id_omnisync,))
customer_data = sqlite_cursor.fetchone()
if customer_data:
vtiger_id_customer, customer_name = customer_data
# Find in BMC Hub
if vtiger_id_customer:
postgres_cursor.execute("SELECT id FROM customers WHERE vtiger_id = %s", (vtiger_id_customer,))
else:
postgres_cursor.execute("SELECT id FROM customers WHERE name = %s LIMIT 1", (customer_name,))
bmc_customer = postgres_cursor.fetchone()
if bmc_customer:
postgres_cursor.execute("""
INSERT INTO contact_companies (contact_id, customer_id, is_primary, role, notes)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT DO NOTHING
""", (contact_id, bmc_customer['id'], bool(is_primary), role, notes))
postgres_conn.commit()
imported += 1
if imported % 50 == 0:
logger.info(f" Imported {imported} contacts...")
except Exception as e:
error_msg = str(e)[:100]
if imported + skipped < 10:
logger.warning(f" ⚠️ Could not import '{first_name} {last_name}': {error_msg}")
errors.append((f"{first_name} {last_name}", error_msg))
skipped += 1
finally:
postgres_cursor.close()
postgres_conn.close()
logger.info(f"✅ Contacts: {imported} imported, {skipped} skipped")
if len(errors) > 10:
logger.info(f" (Suppressed {len(errors)-10} error messages)")
return imported
except Exception as e:
logger.error(f"❌ Contact import failed: {e}")
raise
finally:
sqlite_cursor.close()
sqlite_conn.close()
if __name__ == "__main__":
logger.info("🚀 Starting OmniSync import...")
logger.info(f"📂 Source: {OMNISYNC_DB}")
customer_count = import_customers()
contact_count = import_contacts()
logger.info(f"\n🎉 Import completed!")
logger.info(f" Customers: {customer_count}")
logger.info(f" Contacts: {contact_count}")

View File

@ -0,0 +1,232 @@
"""
Data import script for BMC Hub
Imports customers and contacts from CSV files or creates sample data
"""
import psycopg2
from psycopg2.extras import RealDictCursor
import os
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def get_connection():
"""Get database connection"""
database_url = os.getenv('DATABASE_URL', 'postgresql://bmc_hub:bmc_hub@postgres:5432/bmc_hub')
return psycopg2.connect(database_url, cursor_factory=RealDictCursor)
def import_sample_data():
"""Import sample customers and contacts for testing"""
conn = get_connection()
cursor = conn.cursor()
try:
# Sample customers
customers = [
{
'name': 'TDC A/S',
'cvr_number': '14773908',
'address': 'Teglholmsgade 1',
'postal_code': '2450',
'city': 'København SV',
'email': 'info@tdc.dk',
'phone': '+45 70 70 40 30',
'website': 'https://tdc.dk',
'is_active': True
},
{
'name': 'Dansk Supermarked Group',
'cvr_number': '16314439',
'address': 'Roskildevej 65',
'postal_code': '2620',
'city': 'Albertslund',
'email': 'info@dsg.dk',
'phone': '+45 43 86 43 86',
'website': 'https://dansksupermarked.dk',
'is_active': True
},
{
'name': 'Nets Denmark A/S',
'cvr_number': '20016175',
'address': 'Lautrupbjerg 10',
'postal_code': '2750',
'city': 'Ballerup',
'email': 'info@nets.eu',
'phone': '+45 44 68 44 68',
'website': 'https://nets.eu',
'is_active': True
},
{
'name': 'Salling Group',
'cvr_number': '30521736',
'address': 'Skanderborgvej 121',
'postal_code': '8260',
'city': 'Viby J',
'email': 'info@sallinggroup.com',
'phone': '+45 87 93 35 00',
'website': 'https://sallinggroup.com',
'is_active': True
},
{
'name': 'ISS A/S',
'cvr_number': '28861341',
'address': 'Buddingevej 197',
'postal_code': '2860',
'city': 'Søborg',
'email': 'info@dk.issworld.com',
'phone': '+45 38 17 00 00',
'website': 'https://issworld.com',
'is_active': True
}
]
logger.info("Importing customers...")
customer_ids = {}
for customer in customers:
cursor.execute("""
INSERT INTO customers (
name, cvr_number, address, postal_code, city,
email, phone, website, is_active
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (
customer['name'], customer['cvr_number'], customer['address'],
customer['postal_code'], customer['city'], customer['email'],
customer['phone'], customer['website'], customer['is_active']
))
result = cursor.fetchone()
customer_id = result['id']
customer_ids[customer['name']] = customer_id
logger.info(f"✅ Imported customer: {customer['name']} (ID: {customer_id})")
# Sample contacts
contacts = [
{
'first_name': 'Lars',
'last_name': 'Jensen',
'email': 'lars.jensen@tdc.dk',
'phone': '+45 70 70 40 31',
'mobile': '+45 20 12 34 56',
'title': 'CTO',
'department': 'IT',
'companies': ['TDC A/S'],
'is_primary': True,
'role': 'Teknisk kontakt'
},
{
'first_name': 'Mette',
'last_name': 'Nielsen',
'email': 'mette.nielsen@dsg.dk',
'phone': '+45 43 86 43 87',
'mobile': '+45 30 98 76 54',
'title': 'IT Manager',
'department': 'IT Operations',
'companies': ['Dansk Supermarked Group'],
'is_primary': True,
'role': 'Primær kontakt'
},
{
'first_name': 'Peter',
'last_name': 'Hansen',
'email': 'peter.hansen@nets.eu',
'phone': '+45 44 68 44 69',
'mobile': '+45 40 11 22 33',
'title': 'Network Engineer',
'department': 'Infrastructure',
'companies': ['Nets Denmark A/S'],
'is_primary': True,
'role': 'Teknisk ansvarlig'
},
{
'first_name': 'Anne',
'last_name': 'Andersen',
'email': 'anne.andersen@sallinggroup.com',
'phone': '+45 87 93 35 01',
'mobile': '+45 50 44 55 66',
'title': 'IT Director',
'department': 'IT',
'companies': ['Salling Group'],
'is_primary': True,
'role': 'Beslutningsansvarlig'
},
{
'first_name': 'Thomas',
'last_name': 'Christensen',
'email': 'thomas.christensen@issworld.com',
'phone': '+45 38 17 00 01',
'mobile': '+45 60 77 88 99',
'title': 'Senior IT Consultant',
'department': 'IT Services',
'companies': ['ISS A/S', 'Nets Denmark A/S'], # Multi-company contact
'is_primary': False,
'role': 'Konsulent'
}
]
logger.info("Importing contacts...")
for contact in contacts:
# Insert contact
cursor.execute("""
INSERT INTO contacts (
first_name, last_name, email, phone, mobile,
title, department, is_active
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (
contact['first_name'], contact['last_name'], contact['email'],
contact['phone'], contact['mobile'], contact['title'],
contact['department'], True
))
result = cursor.fetchone()
contact_id = result['id']
# Link to companies
for company_name in contact['companies']:
if company_name in customer_ids:
cursor.execute("""
INSERT INTO contact_companies (
contact_id, customer_id, is_primary, role
) VALUES (%s, %s, %s, %s)
""", (
contact_id, customer_ids[company_name],
contact['is_primary'] if contact['companies'][0] == company_name else False,
contact['role']
))
logger.info(f"✅ Imported contact: {contact['first_name']} {contact['last_name']} (ID: {contact_id}, Companies: {len(contact['companies'])})")
conn.commit()
logger.info("🎉 Sample data import completed successfully!")
# Print summary
cursor.execute("SELECT COUNT(*) as count FROM customers")
customer_count = cursor.fetchone()['count']
cursor.execute("SELECT COUNT(*) as count FROM contacts")
contact_count = cursor.fetchone()['count']
cursor.execute("SELECT COUNT(*) as count FROM contact_companies")
link_count = cursor.fetchone()['count']
logger.info(f"\n📊 Summary:")
logger.info(f" Customers: {customer_count}")
logger.info(f" Contacts: {contact_count}")
logger.info(f" Company-Contact Links: {link_count}")
except Exception as e:
conn.rollback()
logger.error(f"❌ Import failed: {e}")
raise
finally:
cursor.close()
conn.close()
if __name__ == "__main__":
logger.info("🚀 Starting data import...")
import_sample_data()

View File

@ -0,0 +1,282 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Nordic Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
--border-radius: 12px;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.sidebar {
background: var(--bg-card);
height: 100vh;
position: fixed;
width: 260px;
border-right: 1px solid #eee;
padding: 2rem 1.5rem;
}
.nav-link {
color: var(--text-secondary);
padding: 0.8rem 1rem;
border-radius: var(--border-radius);
margin-bottom: 0.5rem;
transition: all 0.2s;
font-weight: 500;
}
.nav-link:hover, .nav-link.active {
background-color: var(--accent-light);
color: var(--accent);
}
.nav-link i {
margin-right: 10px;
}
.main-content {
margin-left: 260px;
padding: 2rem 3rem;
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
}
.table th {
font-weight: 600;
color: var(--text-secondary);
border-bottom-width: 1px;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
padding-top: 1rem;
padding-bottom: 1rem;
}
.table td {
padding-top: 1rem;
padding-bottom: 1rem;
vertical-align: middle;
}
.btn-primary {
background-color: var(--accent);
border-color: var(--accent);
padding: 0.6rem 1.5rem;
border-radius: 8px;
}
.header-search {
background: white;
border: 1px solid #eee;
padding: 0.6rem 1.2rem;
border-radius: 50px;
width: 300px;
}
.avatar {
width: 40px;
height: 40px;
background-color: var(--accent-light);
color: var(--accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 1rem;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="d-flex align-items-center mb-5">
<div class="bg-primary rounded-circle p-2 me-2 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px; background-color: var(--accent) !important;">
<i class="bi bi-hdd-network text-white"></i>
</div>
<h4 class="mb-0 fw-bold">BMC Hub</h4>
</div>
<nav class="nav flex-column">
<a class="nav-link" href="index.html"><i class="bi bi-grid"></i> Dashboard</a>
<a class="nav-link active" href="customers.html"><i class="bi bi-people"></i> Kunder</a>
<a class="nav-link" href="#"><i class="bi bi-router"></i> Hardware</a>
<a class="nav-link" href="#"><i class="bi bi-receipt"></i> Fakturering</a>
<a class="nav-link" href="#"><i class="bi bi-gear"></i> Indstillinger</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Kunder</h2>
<p class="text-muted mb-0">Administrer dine kunder og deres services</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="header-search" placeholder="Søg efter kunde...">
<button class="btn btn-primary"><i class="bi bi-plus-lg"></i> Opret Kunde</button>
</div>
</div>
<div class="card p-4">
<div class="d-flex gap-2 mb-4">
<button class="btn btn-light active fw-medium">Alle Kunder</button>
<button class="btn btn-light text-muted">Aktive</button>
<button class="btn btn-light text-muted">Inaktive</button>
<button class="btn btn-light text-muted">VIP</button>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Virksomhed</th>
<th>Kontaktperson</th>
<th>CVR Nummer</th>
<th>Status</th>
<th>Hardware</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar">A</div>
<div>
<div class="fw-bold">Advokatgruppen A/S</div>
<div class="small text-muted">København K</div>
</div>
</div>
</td>
<td>
<div>Jens Jensen</div>
<div class="small text-muted">jens@advokat.dk</div>
</td>
<td class="text-muted">12345678</td>
<td><span class="badge bg-success-subtle text-success rounded-pill px-3">Aktiv</span></td>
<td>
<div class="d-flex gap-1">
<span class="badge bg-light text-dark border">Firewall</span>
<span class="badge bg-light text-dark border">Switch</span>
</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar" style="background-color: #fff3cd; color: #856404;">B</div>
<div>
<div class="fw-bold">Byg & Bo ApS</div>
<div class="small text-muted">Aarhus C</div>
</div>
</div>
</td>
<td>
<div>Mette Hansen</div>
<div class="small text-muted">mh@bygbo.dk</div>
</td>
<td class="text-muted">87654321</td>
<td><span class="badge bg-success-subtle text-success rounded-pill px-3">Aktiv</span></td>
<td>
<div class="d-flex gap-1">
<span class="badge bg-light text-dark border">Router</span>
</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar" style="background-color: #f8d7da; color: #721c24;">C</div>
<div>
<div class="fw-bold">Cafe Møller</div>
<div class="small text-muted">Odense M</div>
</div>
</div>
</td>
<td>
<div>Peter Møller</div>
<div class="small text-muted">pm@cafe.dk</div>
</td>
<td class="text-muted">11223344</td>
<td><span class="badge bg-warning-subtle text-warning rounded-pill px-3">Afventer</span></td>
<td>
<span class="text-muted small">-</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar" style="background-color: #d1e7dd; color: #0f5132;">D</div>
<div>
<div class="fw-bold">Dansk Design Hus</div>
<div class="small text-muted">København Ø</div>
</div>
</div>
</td>
<td>
<div>Lars Larsen</div>
<div class="small text-muted">ll@design.dk</div>
</td>
<td class="text-muted">44332211</td>
<td><span class="badge bg-success-subtle text-success rounded-pill px-3">Aktiv</span></td>
<td>
<div class="d-flex gap-1">
<span class="badge bg-light text-dark border">Firewall</span>
<span class="badge bg-light text-dark border">AP x4</span>
</div>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted small">Viser 1-4 af 124 kunder</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item disabled"><a class="page-link" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,292 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Nordic Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #f8f9fa;
--bg-card: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #6c757d;
--accent: #0f4c75;
--accent-light: #eef2f5;
--border-radius: 12px;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.sidebar {
background: var(--bg-card);
height: 100vh;
position: fixed;
width: 260px;
border-right: 1px solid #eee;
padding: 2rem 1.5rem;
}
.nav-link {
color: var(--text-secondary);
padding: 0.8rem 1rem;
border-radius: var(--border-radius);
margin-bottom: 0.5rem;
transition: all 0.2s;
font-weight: 500;
}
.nav-link:hover, .nav-link.active {
background-color: var(--accent-light);
color: var(--accent);
}
.nav-link i {
margin-right: 10px;
}
.main-content {
margin-left: 260px;
padding: 2rem 3rem;
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: 0 2px 15px rgba(0,0,0,0.03);
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-2px);
}
.stat-card h3 {
font-size: 2rem;
font-weight: 700;
color: var(--accent);
margin-bottom: 0;
}
.stat-card p {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.table th {
font-weight: 600;
color: var(--text-secondary);
border-bottom-width: 1px;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background-color: var(--accent);
border-color: var(--accent);
padding: 0.6rem 1.5rem;
border-radius: 8px;
}
.header-search {
background: white;
border: 1px solid #eee;
padding: 0.6rem 1.2rem;
border-radius: 50px;
width: 300px;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="d-flex align-items-center mb-5">
<div class="bg-primary rounded-circle p-2 me-2 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px; background-color: var(--accent) !important;">
<i class="bi bi-hdd-network text-white"></i>
</div>
<h4 class="mb-0 fw-bold">BMC Hub</h4>
</div>
<nav class="nav flex-column">
<a class="nav-link active" href="index.html"><i class="bi bi-grid"></i> Dashboard</a>
<a class="nav-link" href="customers.html"><i class="bi bi-people"></i> Kunder</a>
<a class="nav-link" href="#"><i class="bi bi-router"></i> Hardware</a>
<a class="nav-link" href="#"><i class="bi bi-receipt"></i> Fakturering</a>
<a class="nav-link" href="#"><i class="bi bi-gear"></i> Indstillinger</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Oversigt</h2>
<p class="text-muted mb-0">Velkommen tilbage, Christian</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="header-search" placeholder="Søg...">
<button class="btn btn-primary"><i class="bi bi-plus-lg"></i> Ny Opgave</button>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-3">
<div class="card stat-card h-100 p-4">
<div class="d-flex justify-content-between align-items-start">
<div>
<p>Aktive Kunder</p>
<h3>124</h3>
</div>
<span class="badge bg-success-subtle text-success">+12%</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card h-100 p-4">
<div class="d-flex justify-content-between align-items-start">
<div>
<p>Hardware Enheder</p>
<h3>856</h3>
</div>
<span class="badge bg-primary-subtle text-primary">Online</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card h-100 p-4">
<div class="d-flex justify-content-between align-items-start">
<div>
<p>Support Sager</p>
<h3>12</h3>
</div>
<span class="badge bg-warning-subtle text-warning">3 Haster</span>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card h-100 p-4">
<div class="d-flex justify-content-between align-items-start">
<div>
<p>Omsætning (Mdr)</p>
<h3>450k</h3>
</div>
<span class="badge bg-success-subtle text-success">+5%</span>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-8">
<div class="card p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-0">Seneste Aktiviteter</h5>
<a href="#" class="text-decoration-none small">Se alle</a>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Kunde</th>
<th>Handling</th>
<th>Status</th>
<th>Dato</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-3" style="width: 32px; height: 32px;">A</div>
<span class="fw-medium">Advokatgruppen A/S</span>
</div>
</td>
<td>Ny firewall konfigureret</td>
<td><span class="badge bg-success-subtle text-success rounded-pill">Fuldført</span></td>
<td class="text-muted">I dag, 10:23</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-3" style="width: 32px; height: 32px;">B</div>
<span class="fw-medium">Byg & Bo ApS</span>
</div>
</td>
<td>Opdatering af licenser</td>
<td><span class="badge bg-warning-subtle text-warning rounded-pill">Afventer</span></td>
<td class="text-muted">I går, 14:45</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-3" style="width: 32px; height: 32px;">C</div>
<span class="fw-medium">Cafe Møller</span>
</div>
</td>
<td>Netværksnedbrud</td>
<td><span class="badge bg-danger-subtle text-danger rounded-pill">Kritisk</span></td>
<td class="text-muted">I går, 09:12</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-4 h-100">
<h5 class="fw-bold mb-4">System Status</h5>
<div class="d-flex align-items-center mb-4">
<div class="flex-grow-1">
<div class="d-flex justify-content-between mb-1">
<span class="small fw-medium">Server Load</span>
<span class="small text-muted">24%</span>
</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-success" style="width: 24%"></div>
</div>
</div>
</div>
<div class="d-flex align-items-center mb-4">
<div class="flex-grow-1">
<div class="d-flex justify-content-between mb-1">
<span class="small fw-medium">Database</span>
<span class="small text-muted">56%</span>
</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-primary" style="width: 56%"></div>
</div>
</div>
</div>
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<div class="d-flex justify-content-between mb-1">
<span class="small fw-medium">Storage</span>
<span class="small text-muted">89%</span>
</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-warning" style="width: 89%"></div>
</div>
</div>
</div>
<div class="mt-auto pt-4">
<div class="alert alert-light border mb-0">
<div class="d-flex">
<i class="bi bi-info-circle text-primary me-2"></i>
<small class="text-muted">System backup kører i nat kl. 03:00</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Dark Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #0f111a;
--bg-card: #1a1d2d;
--bg-sidebar: #141622;
--accent: #6c5ce7;
--accent-hover: #5b4cc4;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
}
body {
background-color: var(--bg-body);
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.sidebar {
background: var(--bg-sidebar);
height: 100vh;
position: fixed;
width: 250px;
border-right: 1px solid rgba(255,255,255,0.05);
padding: 1.5rem;
}
.nav-link {
color: var(--text-secondary);
padding: 0.8rem 1rem;
border-radius: 6px;
margin-bottom: 0.2rem;
transition: all 0.2s;
}
.nav-link:hover, .nav-link.active {
background-color: rgba(108, 92, 231, 0.15);
color: var(--accent);
}
.main-content {
margin-left: 250px;
padding: 2rem;
}
.card {
background-color: var(--bg-card);
border: 1px solid rgba(255,255,255,0.05);
border-radius: 10px;
}
.btn-primary {
background-color: var(--accent);
border: none;
padding: 0.5rem 1.5rem;
}
.btn-primary:hover {
background-color: var(--accent-hover);
}
.search-input {
background-color: var(--bg-card);
border: 1px solid rgba(255,255,255,0.1);
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
width: 300px;
}
.search-input:focus {
background-color: var(--bg-card);
color: white;
border-color: var(--accent);
outline: none;
}
.table {
--bs-table-bg: transparent;
--bs-table-color: var(--text-secondary);
--bs-table-border-color: rgba(255,255,255,0.05);
}
.table td {
padding: 1rem 0.5rem;
vertical-align: middle;
}
.table tr:hover td {
color: white;
}
.customer-avatar {
width: 40px;
height: 40px;
background: rgba(255,255,255,0.05);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-weight: 600;
}
.filter-btn {
background: transparent;
border: 1px solid rgba(255,255,255,0.1);
color: var(--text-secondary);
padding: 0.4rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
transition: all 0.2s;
}
.filter-btn:hover, .filter-btn.active {
background: rgba(108, 92, 231, 0.15);
color: var(--accent);
border-color: var(--accent);
}
</style>
</head>
<body>
<div class="sidebar">
<div class="d-flex align-items-center mb-5 px-2">
<div class="me-2 text-primary">
<i class="bi bi-hexagon-fill fs-4" style="color: var(--accent);"></i>
</div>
<h5 class="mb-0 fw-bold text-white tracking-wide">BMC HUB</h5>
</div>
<nav class="nav flex-column">
<small class="text-uppercase text-muted mb-2 px-3" style="font-size: 0.7rem; letter-spacing: 1px;">Menu</small>
<a class="nav-link" href="index.html"><i class="bi bi-speedometer2 me-2"></i> Dashboard</a>
<a class="nav-link active" href="customers.html"><i class="bi bi-people me-2"></i> Kunder</a>
<a class="nav-link" href="#"><i class="bi bi-box-seam me-2"></i> Hardware</a>
<a class="nav-link" href="#"><i class="bi bi-credit-card me-2"></i> Fakturering</a>
<small class="text-uppercase text-muted mb-2 mt-4 px-3" style="font-size: 0.7rem; letter-spacing: 1px;">System</small>
<a class="nav-link" href="#"><i class="bi bi-shield-check me-2"></i> Sikkerhed</a>
<a class="nav-link" href="#"><i class="bi bi-gear me-2"></i> Indstillinger</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h3 class="text-white mb-1">Kundeoversigt</h3>
<p class="text-secondary mb-0">Administrer kunder og abonnementer</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="search-input" placeholder="Søg efter kunde...">
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Opret Kunde</button>
</div>
</div>
<div class="card p-4">
<div class="d-flex gap-2 mb-4">
<button class="filter-btn active">Alle Kunder</button>
<button class="filter-btn">Aktive</button>
<button class="filter-btn">Inaktive</button>
<button class="filter-btn">VIP</button>
</div>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="text-secondary fw-normal ps-0">Virksomhed</th>
<th class="text-secondary fw-normal">Kontakt</th>
<th class="text-secondary fw-normal">CVR</th>
<th class="text-secondary fw-normal">Status</th>
<th class="text-secondary fw-normal">Udstyr</th>
<th class="text-secondary fw-normal text-end pe-0">Handling</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-0">
<div class="d-flex align-items-center">
<div class="customer-avatar me-3">A</div>
<div>
<div class="text-white fw-medium">Advokatgruppen A/S</div>
<div class="small text-secondary">København K</div>
</div>
</div>
</td>
<td>
<div class="text-white">Jens Jensen</div>
<div class="small text-secondary">jens@advokat.dk</div>
</td>
<td class="text-secondary">12345678</td>
<td><span class="badge bg-success-subtle text-success border border-success-subtle">Aktiv</span></td>
<td>
<span class="badge bg-dark border border-secondary text-secondary">Firewall</span>
<span class="badge bg-dark border border-secondary text-secondary">Switch</span>
</td>
<td class="text-end pe-0">
<button class="btn btn-sm btn-outline-secondary border-0"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td class="ps-0">
<div class="d-flex align-items-center">
<div class="customer-avatar me-3" style="color: #ffeaa7;">B</div>
<div>
<div class="text-white fw-medium">Byg & Bo ApS</div>
<div class="small text-secondary">Aarhus C</div>
</div>
</div>
</td>
<td>
<div class="text-white">Mette Hansen</div>
<div class="small text-secondary">mh@bygbo.dk</div>
</td>
<td class="text-secondary">87654321</td>
<td><span class="badge bg-success-subtle text-success border border-success-subtle">Aktiv</span></td>
<td>
<span class="badge bg-dark border border-secondary text-secondary">Router</span>
</td>
<td class="text-end pe-0">
<button class="btn btn-sm btn-outline-secondary border-0"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td class="ps-0">
<div class="d-flex align-items-center">
<div class="customer-avatar me-3" style="color: #ff7675;">C</div>
<div>
<div class="text-white fw-medium">Cafe Møller</div>
<div class="small text-secondary">Odense M</div>
</div>
</div>
</td>
<td>
<div class="text-white">Peter Møller</div>
<div class="small text-secondary">pm@cafe.dk</div>
</td>
<td class="text-secondary">11223344</td>
<td><span class="badge bg-warning-subtle text-warning border border-warning-subtle">Afventer</span></td>
<td>
<span class="text-secondary small">-</span>
</td>
<td class="text-end pe-0">
<button class="btn btn-sm btn-outline-secondary border-0"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td class="ps-0">
<div class="d-flex align-items-center">
<div class="customer-avatar me-3" style="color: #55efc4;">D</div>
<div>
<div class="text-white fw-medium">Dansk Design Hus</div>
<div class="small text-secondary">København Ø</div>
</div>
</div>
</td>
<td>
<div class="text-white">Lars Larsen</div>
<div class="small text-secondary">ll@design.dk</div>
</td>
<td class="text-secondary">44332211</td>
<td><span class="badge bg-success-subtle text-success border border-success-subtle">Aktiv</span></td>
<td>
<span class="badge bg-dark border border-secondary text-secondary">Firewall</span>
<span class="badge bg-dark border border-secondary text-secondary">+4</span>
</td>
<td class="text-end pe-0">
<button class="btn btn-sm btn-outline-secondary border-0"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-4 border-top border-secondary border-opacity-10 pt-4">
<div class="text-secondary small">Viser 1-4 af 124 kunder</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item disabled"><a class="page-link bg-transparent border-secondary border-opacity-25 text-secondary" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link border-accent bg-accent" style="background-color: var(--accent); border-color: var(--accent);" href="#">1</a></li>
<li class="page-item"><a class="page-link bg-transparent border-secondary border-opacity-25 text-secondary" href="#">2</a></li>
<li class="page-item"><a class="page-link bg-transparent border-secondary border-opacity-25 text-secondary" href="#">3</a></li>
<li class="page-item"><a class="page-link bg-transparent border-secondary border-opacity-25 text-secondary" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,267 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Dark Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #0f111a;
--bg-card: #1a1d2d;
--bg-sidebar: #141622;
--accent: #6c5ce7;
--accent-hover: #5b4cc4;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
}
body {
background-color: var(--bg-body);
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.sidebar {
background: var(--bg-sidebar);
height: 100vh;
position: fixed;
width: 250px;
border-right: 1px solid rgba(255,255,255,0.05);
padding: 1.5rem;
}
.nav-link {
color: var(--text-secondary);
padding: 0.8rem 1rem;
border-radius: 6px;
margin-bottom: 0.2rem;
transition: all 0.2s;
}
.nav-link:hover, .nav-link.active {
background-color: rgba(108, 92, 231, 0.15);
color: var(--accent);
}
.main-content {
margin-left: 250px;
padding: 2rem;
}
.card {
background-color: var(--bg-card);
border: 1px solid rgba(255,255,255,0.05);
border-radius: 10px;
}
.stat-value {
font-size: 2.5rem;
font-weight: 300;
color: white;
line-height: 1.2;
}
.stat-label {
color: var(--text-secondary);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
font-weight: 600;
}
.btn-primary {
background-color: var(--accent);
border: none;
padding: 0.5rem 1.5rem;
}
.btn-primary:hover {
background-color: var(--accent-hover);
}
.search-input {
background-color: var(--bg-card);
border: 1px solid rgba(255,255,255,0.1);
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
}
.search-input:focus {
background-color: var(--bg-card);
color: white;
border-color: var(--accent);
outline: none;
}
.table {
--bs-table-bg: transparent;
--bs-table-color: var(--text-secondary);
--bs-table-border-color: rgba(255,255,255,0.05);
}
.table td {
padding: 1rem 0.5rem;
}
.table tr:hover td {
color: white;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="d-flex align-items-center mb-5 px-2">
<div class="me-2 text-primary">
<i class="bi bi-hexagon-fill fs-4" style="color: var(--accent);"></i>
</div>
<h5 class="mb-0 fw-bold text-white tracking-wide">BMC HUB</h5>
</div>
<nav class="nav flex-column">
<small class="text-uppercase text-muted mb-2 px-3" style="font-size: 0.7rem; letter-spacing: 1px;">Menu</small>
<a class="nav-link active" href="index.html"><i class="bi bi-speedometer2 me-2"></i> Dashboard</a>
<a class="nav-link" href="customers.html"><i class="bi bi-people me-2"></i> Kunder</a>
<a class="nav-link" href="#"><i class="bi bi-box-seam me-2"></i> Hardware</a>
<a class="nav-link" href="#"><i class="bi bi-credit-card me-2"></i> Fakturering</a>
<small class="text-uppercase text-muted mb-2 mt-4 px-3" style="font-size: 0.7rem; letter-spacing: 1px;">System</small>
<a class="nav-link" href="#"><i class="bi bi-shield-check me-2"></i> Sikkerhed</a>
<a class="nav-link" href="#"><i class="bi bi-gear me-2"></i> Indstillinger</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h3 class="text-white mb-1">Dashboard</h3>
<p class="text-secondary mb-0">System overblik og status</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="search-input" placeholder="Søg i systemet...">
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Ny Opgave</button>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="stat-label mb-2">Aktive Kunder</div>
<div class="stat-value mb-2">124</div>
<div class="text-success small"><i class="bi bi-arrow-up-short"></i> 12% denne måned</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="stat-label mb-2">Hardware</div>
<div class="stat-value mb-2">856</div>
<div class="text-secondary small">Enheder online</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="stat-label mb-2">Support</div>
<div class="stat-value mb-2" style="color: #ff7675;">12</div>
<div class="text-secondary small">Åbne sager</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="stat-label mb-2">Omsætning</div>
<div class="stat-value mb-2">450k</div>
<div class="text-success small">DKK denne måned</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-8">
<div class="card p-4">
<h5 class="text-white mb-4">Seneste Hændelser</h5>
<table class="table align-middle">
<thead>
<tr>
<th class="text-secondary fw-normal ps-0">Hændelse</th>
<th class="text-secondary fw-normal">Kunde</th>
<th class="text-secondary fw-normal">Tid</th>
<th class="text-secondary fw-normal text-end pe-0">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-0 text-white">Firewall konfiguration ændret</td>
<td>Advokatgruppen A/S</td>
<td>10:23</td>
<td class="text-end pe-0"><span class="badge bg-success-subtle text-success border border-success-subtle">Fuldført</span></td>
</tr>
<tr>
<td class="ps-0 text-white">Licens fornyelse</td>
<td>Byg & Bo ApS</td>
<td>I går</td>
<td class="text-end pe-0"><span class="badge bg-warning-subtle text-warning border border-warning-subtle">Afventer</span></td>
</tr>
<tr>
<td class="ps-0 text-white">VPN forbindelse tabt</td>
<td>Cafe Møller</td>
<td>I går</td>
<td class="text-end pe-0"><span class="badge bg-danger-subtle text-danger border border-danger-subtle">Fejl</span></td>
</tr>
<tr>
<td class="ps-0 text-white">Ny bruger oprettet</td>
<td>Dansk Design Hus</td>
<td>2 dage siden</td>
<td class="text-end pe-0"><span class="badge bg-success-subtle text-success border border-success-subtle">Fuldført</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-4">
<div class="card p-4 h-100">
<h5 class="text-white mb-4">Server Status</h5>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">CPU Usage</span>
<span class="text-white">24%</span>
</div>
<div class="progress bg-dark" style="height: 4px;">
<div class="progress-bar" style="width: 24%; background-color: var(--accent);"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Memory</span>
<span class="text-white">56%</span>
</div>
<div class="progress bg-dark" style="height: 4px;">
<div class="progress-bar bg-info" style="width: 56%"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="text-secondary">Storage</span>
<span class="text-white">89%</span>
</div>
<div class="progress bg-dark" style="height: 4px;">
<div class="progress-bar bg-warning" style="width: 89%"></div>
</div>
</div>
<div class="mt-auto p-3 rounded" style="background: rgba(108, 92, 231, 0.1); border: 1px solid rgba(108, 92, 231, 0.2);">
<div class="d-flex">
<i class="bi bi-lightning-charge-fill me-2" style="color: var(--accent);"></i>
<small class="text-white">Systemet kører optimalt. Ingen kritiske fejl fundet.</small>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,285 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Swiss Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap');
:root {
--bg-body: #ffffff;
--text-primary: #000000;
--text-secondary: #666666;
--accent: #ff3b30; /* Swiss Red */
--grid-line: #e5e5e5;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
}
.sidebar {
height: 100vh;
position: fixed;
width: 280px;
border-right: 2px solid var(--text-primary);
padding: 0;
background: white;
z-index: 100;
}
.brand-section {
padding: 2rem;
border-bottom: 2px solid var(--text-primary);
}
.nav-link {
color: var(--text-primary);
padding: 1.2rem 2rem;
border-bottom: 1px solid var(--grid-line);
font-weight: 600;
font-size: 1.1rem;
border-radius: 0;
transition: all 0.2s;
}
.nav-link:hover, .nav-link.active {
background-color: var(--text-primary);
color: white;
}
.main-content {
margin-left: 280px;
padding: 0;
}
.top-bar {
border-bottom: 2px solid var(--text-primary);
padding: 2rem 3rem;
background: white;
}
.content-grid {
padding: 3rem;
}
.btn-primary {
background-color: var(--text-primary);
border: none;
border-radius: 0;
padding: 1rem 2rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-primary:hover {
background-color: var(--accent);
}
.table {
border: 2px solid var(--text-primary);
margin-bottom: 0;
}
.table th {
background: var(--text-primary);
color: white;
font-weight: 600;
text-transform: uppercase;
padding: 1rem;
border: none;
}
.table td {
padding: 1.2rem 1rem;
border-bottom: 1px solid var(--grid-line);
font-weight: 500;
vertical-align: middle;
}
.status-badge {
border: 1px solid var(--text-primary);
padding: 0.2rem 0.8rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
background: white;
}
.status-badge.active {
background: var(--text-primary);
color: white;
}
.filter-bar {
border: 2px solid var(--text-primary);
border-bottom: none;
padding: 1rem;
background: #f8f9fa;
}
.filter-btn {
background: white;
border: 1px solid var(--text-primary);
padding: 0.5rem 1.5rem;
font-weight: 600;
text-transform: uppercase;
font-size: 0.8rem;
margin-right: 0.5rem;
}
.filter-btn.active {
background: var(--text-primary);
color: white;
}
h1, h2, h3, h4, h5 {
font-weight: 800;
letter-spacing: -1px;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="brand-section">
<h2 class="mb-0">BMC<span style="color: var(--accent)">.</span>HUB</h2>
</div>
<nav class="nav flex-column">
<a class="nav-link" href="index.html">Dashboard</a>
<a class="nav-link active" href="customers.html">Kunder</a>
<a class="nav-link" href="#">Hardware</a>
<a class="nav-link" href="#">Fakturering</a>
<a class="nav-link" href="#">Indstillinger</a>
</nav>
</div>
<div class="main-content">
<div class="top-bar d-flex justify-content-between align-items-center">
<div>
<h1 class="mb-0">KUNDER</h1>
</div>
<div class="d-flex gap-3">
<input type="text" class="form-control rounded-0 border-dark border-2" placeholder="SØG KUNDE..." style="width: 300px;">
<button class="btn btn-primary">OPRET KUNDE</button>
</div>
</div>
<div class="content-grid">
<div class="filter-bar d-flex align-items-center">
<span class="fw-bold me-3">FILTER:</span>
<button class="filter-btn active">ALLE</button>
<button class="filter-btn">AKTIVE</button>
<button class="filter-btn">INAKTIVE</button>
<button class="filter-btn">VIP</button>
</div>
<table class="table">
<thead>
<tr>
<th>Virksomhed</th>
<th>Kontaktperson</th>
<th>CVR</th>
<th>Status</th>
<th>Hardware</th>
<th class="text-end">Handling</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="fw-bold">Advokatgruppen A/S</div>
<div class="small text-secondary">København K</div>
</td>
<td>
<div>Jens Jensen</div>
<div class="small text-secondary">jens@advokat.dk</div>
</td>
<td>12345678</td>
<td><span class="status-badge active">Aktiv</span></td>
<td>
<span class="fw-bold small">FIREWALL, SWITCH</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-dark rounded-0 fw-bold">REDIGER</button>
</td>
</tr>
<tr>
<td>
<div class="fw-bold">Byg & Bo ApS</div>
<div class="small text-secondary">Aarhus C</div>
</td>
<td>
<div>Mette Hansen</div>
<div class="small text-secondary">mh@bygbo.dk</div>
</td>
<td>87654321</td>
<td><span class="status-badge active">Aktiv</span></td>
<td>
<span class="fw-bold small">ROUTER</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-dark rounded-0 fw-bold">REDIGER</button>
</td>
</tr>
<tr>
<td>
<div class="fw-bold">Cafe Møller</div>
<div class="small text-secondary">Odense M</div>
</td>
<td>
<div>Peter Møller</div>
<div class="small text-secondary">pm@cafe.dk</div>
</td>
<td>11223344</td>
<td><span class="status-badge">Afventer</span></td>
<td>
<span class="text-secondary">-</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-dark rounded-0 fw-bold">REDIGER</button>
</td>
</tr>
<tr>
<td>
<div class="fw-bold">Dansk Design Hus</div>
<div class="small text-secondary">København Ø</div>
</td>
<td>
<div>Lars Larsen</div>
<div class="small text-secondary">ll@design.dk</div>
</td>
<td>44332211</td>
<td><span class="status-badge active">Aktiv</span></td>
<td>
<span class="fw-bold small">FIREWALL, AP x4</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-dark rounded-0 fw-bold">REDIGER</button>
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="fw-bold small">VISER 1-4 AF 124 KUNDER</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-dark rounded-0 btn-sm fw-bold" disabled>FORRIGE</button>
<button class="btn btn-dark rounded-0 btn-sm fw-bold">1</button>
<button class="btn btn-outline-dark rounded-0 btn-sm fw-bold">2</button>
<button class="btn btn-outline-dark rounded-0 btn-sm fw-bold">3</button>
<button class="btn btn-outline-dark rounded-0 btn-sm fw-bold">NÆSTE</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Swiss Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap');
:root {
--bg-body: #ffffff;
--text-primary: #000000;
--text-secondary: #666666;
--accent: #ff3b30; /* Swiss Red */
--grid-line: #e5e5e5;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
}
.sidebar {
height: 100vh;
position: fixed;
width: 280px;
border-right: 2px solid var(--text-primary);
padding: 0;
background: white;
z-index: 100;
}
.brand-section {
padding: 2rem;
border-bottom: 2px solid var(--text-primary);
}
.nav-link {
color: var(--text-primary);
padding: 1.2rem 2rem;
border-bottom: 1px solid var(--grid-line);
font-weight: 600;
font-size: 1.1rem;
border-radius: 0;
transition: all 0.2s;
}
.nav-link:hover, .nav-link.active {
background-color: var(--text-primary);
color: white;
}
.main-content {
margin-left: 280px;
padding: 0;
}
.top-bar {
border-bottom: 2px solid var(--text-primary);
padding: 2rem 3rem;
background: white;
}
.content-grid {
padding: 3rem;
}
.stat-card {
border: 2px solid var(--text-primary);
padding: 2rem;
height: 100%;
background: white;
transition: transform 0.2s;
}
.stat-card:hover {
box-shadow: 8px 8px 0px var(--text-primary);
transform: translate(-4px, -4px);
}
.stat-number {
font-size: 3.5rem;
font-weight: 800;
line-height: 1;
margin-bottom: 0.5rem;
letter-spacing: -2px;
}
.stat-label {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.8rem;
}
.btn-primary {
background-color: var(--text-primary);
border: none;
border-radius: 0;
padding: 1rem 2rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-primary:hover {
background-color: var(--accent);
}
.table {
border: 2px solid var(--text-primary);
}
.table th {
background: var(--text-primary);
color: white;
font-weight: 600;
text-transform: uppercase;
padding: 1rem;
border: none;
}
.table td {
padding: 1.2rem 1rem;
border-bottom: 1px solid var(--grid-line);
font-weight: 500;
}
.status-dot {
width: 12px;
height: 12px;
background: var(--text-primary);
display: inline-block;
margin-right: 8px;
}
.status-dot.success { background: #2ecc71; }
.status-dot.warning { background: #f1c40f; }
.status-dot.danger { background: #e74c3c; }
h1, h2, h3, h4, h5 {
font-weight: 800;
letter-spacing: -1px;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="brand-section">
<h2 class="mb-0">BMC<span style="color: var(--accent)">.</span>HUB</h2>
</div>
<nav class="nav flex-column">
<a class="nav-link active" href="index.html">Dashboard</a>
<a class="nav-link" href="customers.html">Kunder</a>
<a class="nav-link" href="#">Hardware</a>
<a class="nav-link" href="#">Fakturering</a>
<a class="nav-link" href="#">Indstillinger</a>
</nav>
<div class="p-4 mt-auto">
<div class="p-3 bg-light border border-dark">
<small class="fw-bold d-block mb-1">SYSTEM STATUS</small>
<div class="d-flex align-items-center">
<div class="status-dot success"></div>
<span class="fw-bold">ONLINE</span>
</div>
</div>
</div>
</div>
<div class="main-content">
<div class="top-bar d-flex justify-content-between align-items-center">
<div>
<h1 class="mb-0">OVERSIGT</h1>
</div>
<div class="d-flex gap-3">
<button class="btn btn-outline-dark rounded-0 fw-bold px-4">SØG</button>
<button class="btn btn-primary">NY OPGAVE</button>
</div>
</div>
<div class="content-grid">
<div class="row g-4 mb-5">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">124</div>
<div class="stat-label">Aktive Kunder</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">856</div>
<div class="stat-label">Hardware Enheder</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number" style="color: var(--accent)">12</div>
<div class="stat-label">Support Sager</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-number">450k</div>
<div class="stat-label">Månedlig Omsætning</div>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-md-8">
<h4 class="mb-4">SENESTE AKTIVITETER</h4>
<table class="table">
<thead>
<tr>
<th>Kunde</th>
<th>Handling</th>
<th>Status</th>
<th>Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td>Advokatgruppen A/S</td>
<td>Firewall konfiguration</td>
<td><div class="status-dot success"></div> Fuldført</td>
<td>10:23</td>
</tr>
<tr>
<td>Byg & Bo ApS</td>
<td>Licens fornyelse</td>
<td><div class="status-dot warning"></div> Afventer</td>
<td>I går</td>
</tr>
<tr>
<td>Cafe Møller</td>
<td>Netværksnedbrud</td>
<td><div class="status-dot danger"></div> Kritisk</td>
<td>I går</td>
</tr>
<tr>
<td>Dansk Design Hus</td>
<td>Ny bruger oprettet</td>
<td><div class="status-dot success"></div> Fuldført</td>
<td>2 dage siden</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-4">
<h4 class="mb-4">HURTIG ADGANG</h4>
<div class="d-grid gap-3">
<button class="btn btn-outline-dark rounded-0 text-start p-3 fw-bold">
<i class="bi bi-plus-lg me-2"></i> OPRET NY KUNDE
</button>
<button class="btn btn-outline-dark rounded-0 text-start p-3 fw-bold">
<i class="bi bi-router me-2"></i> TILFØJ HARDWARE
</button>
<button class="btn btn-outline-dark rounded-0 text-start p-3 fw-bold">
<i class="bi bi-file-text me-2"></i> OPRET FAKTURA
</button>
<div class="p-4 bg-light border border-dark mt-3">
<h5 class="mb-3">NOTER</h5>
<p class="mb-0 small fw-medium text-secondary">Husk at opdatere firewall regler for kunde #124 inden fredag.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Soft Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap');
:root {
--bg-body: #f0f2f5;
--bg-card: #ffffff;
--text-primary: #4a5568;
--text-secondary: #a0aec0;
--accent: #667eea;
--accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--shadow-soft: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.025);
--border-radius: 20px;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Nunito', sans-serif;
}
.sidebar {
background: var(--bg-card);
height: 95vh;
position: fixed;
width: 260px;
margin: 2.5vh;
border-radius: var(--border-radius);
padding: 2rem 1.5rem;
box-shadow: var(--shadow-soft);
}
.nav-link {
color: var(--text-primary);
padding: 1rem 1.5rem;
border-radius: 15px;
margin-bottom: 0.8rem;
transition: all 0.3s ease;
font-weight: 600;
}
.nav-link:hover {
background-color: #f7fafc;
color: var(--accent);
transform: translateX(5px);
}
.nav-link.active {
background: var(--accent-gradient);
color: white;
box-shadow: 0 4px 12px rgba(118, 75, 162, 0.3);
}
.main-content {
margin-left: 300px;
padding: 2.5rem;
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: var(--shadow-soft);
overflow: hidden;
}
.btn-primary {
background: var(--accent-gradient);
border: none;
padding: 0.8rem 2rem;
border-radius: 15px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(118, 75, 162, 0.3);
transition: all 0.3s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(118, 75, 162, 0.4);
}
.search-bar {
background: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 15px;
box-shadow: var(--shadow-soft);
width: 300px;
}
.table th {
font-weight: 700;
color: var(--text-secondary);
border: none;
text-transform: uppercase;
font-size: 0.8rem;
padding: 1.5rem 1rem;
}
.table td {
padding: 1.5rem 1rem;
border-bottom: 1px solid #f0f2f5;
vertical-align: middle;
}
.badge-soft {
padding: 0.5rem 1rem;
border-radius: 10px;
font-weight: 600;
}
.filter-pill {
background: white;
border: none;
padding: 0.5rem 1.5rem;
border-radius: 50px;
color: var(--text-primary);
font-weight: 600;
box-shadow: var(--shadow-soft);
transition: all 0.2s;
}
.filter-pill:hover, .filter-pill.active {
background: var(--accent);
color: white;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="d-flex align-items-center mb-5 px-3">
<div class="bg-white p-2 rounded-3 shadow-sm me-3">
<i class="bi bi-cloud-check-fill fs-4" style="color: #667eea;"></i>
</div>
<h4 class="mb-0 fw-bold">BMC Hub</h4>
</div>
<nav class="nav flex-column">
<a class="nav-link" href="index.html"><i class="bi bi-grid-fill me-3"></i> Dashboard</a>
<a class="nav-link active" href="customers.html"><i class="bi bi-people-fill me-3"></i> Kunder</a>
<a class="nav-link" href="#"><i class="bi bi-router-fill me-3"></i> Hardware</a>
<a class="nav-link" href="#"><i class="bi bi-receipt me-3"></i> Fakturering</a>
<a class="nav-link" href="#"><i class="bi bi-gear-fill me-3"></i> Indstillinger</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Kunder</h2>
<p class="text-secondary mb-0">Administrer dine kunder og deres services</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="search-bar" placeholder="Søg efter kunde...">
<button class="btn btn-primary"><i class="bi bi-plus-lg"></i> Opret Kunde</button>
</div>
</div>
<div class="d-flex gap-3 mb-4">
<button class="filter-pill active">Alle Kunder</button>
<button class="filter-pill">Aktive</button>
<button class="filter-pill">Inaktive</button>
<button class="filter-pill">VIP</button>
</div>
<div class="card p-4">
<table class="table">
<thead>
<tr>
<th>Virksomhed</th>
<th>Kontaktperson</th>
<th>CVR Nummer</th>
<th>Status</th>
<th>Hardware</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-3 bg-light p-2 me-3 text-primary">
<i class="bi bi-building"></i>
</div>
<div>
<div class="fw-bold">Advokatgruppen A/S</div>
<div class="small text-secondary">København K</div>
</div>
</div>
</td>
<td>
<div class="fw-bold">Jens Jensen</div>
<div class="small text-secondary">jens@advokat.dk</div>
</td>
<td class="text-secondary">12345678</td>
<td><span class="badge bg-success-subtle text-success badge-soft">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border">Firewall</span>
<span class="badge bg-light text-dark border">Switch</span>
</td>
<td class="text-end">
<button class="btn btn-light rounded-circle"><i class="bi bi-three-dots"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-3 bg-light p-2 me-3 text-warning">
<i class="bi bi-building"></i>
</div>
<div>
<div class="fw-bold">Byg & Bo ApS</div>
<div class="small text-secondary">Aarhus C</div>
</div>
</div>
</td>
<td>
<div class="fw-bold">Mette Hansen</div>
<div class="small text-secondary">mh@bygbo.dk</div>
</td>
<td class="text-secondary">87654321</td>
<td><span class="badge bg-success-subtle text-success badge-soft">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border">Router</span>
</td>
<td class="text-end">
<button class="btn btn-light rounded-circle"><i class="bi bi-three-dots"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-3 bg-light p-2 me-3 text-danger">
<i class="bi bi-building"></i>
</div>
<div>
<div class="fw-bold">Cafe Møller</div>
<div class="small text-secondary">Odense M</div>
</div>
</div>
</td>
<td>
<div class="fw-bold">Peter Møller</div>
<div class="small text-secondary">pm@cafe.dk</div>
</td>
<td class="text-secondary">11223344</td>
<td><span class="badge bg-warning-subtle text-warning badge-soft">Afventer</span></td>
<td>
<span class="text-secondary">-</span>
</td>
<td class="text-end">
<button class="btn btn-light rounded-circle"><i class="bi bi-three-dots"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-3 bg-light p-2 me-3 text-success">
<i class="bi bi-building"></i>
</div>
<div>
<div class="fw-bold">Dansk Design Hus</div>
<div class="small text-secondary">København Ø</div>
</div>
</div>
</td>
<td>
<div class="fw-bold">Lars Larsen</div>
<div class="small text-secondary">ll@design.dk</div>
</td>
<td class="text-secondary">44332211</td>
<td><span class="badge bg-success-subtle text-success badge-soft">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border">Firewall</span>
<span class="badge bg-light text-dark border">AP x4</span>
</td>
<td class="text-end">
<button class="btn btn-light rounded-circle"><i class="bi bi-three-dots"></i></button>
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center mt-4">
<nav>
<ul class="pagination">
<li class="page-item disabled"><a class="page-link border-0 rounded-start-pill" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link border-0 bg-primary" href="#">1</a></li>
<li class="page-item"><a class="page-link border-0" href="#">2</a></li>
<li class="page-item"><a class="page-link border-0" href="#">3</a></li>
<li class="page-item"><a class="page-link border-0 rounded-end-pill" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,321 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Soft Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap');
:root {
--bg-body: #f0f2f5;
--bg-card: #ffffff;
--text-primary: #4a5568;
--text-secondary: #a0aec0;
--accent: #667eea;
--accent-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--shadow-soft: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.025);
--border-radius: 20px;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: 'Nunito', sans-serif;
}
.sidebar {
background: var(--bg-card);
height: 95vh;
position: fixed;
width: 260px;
margin: 2.5vh;
border-radius: var(--border-radius);
padding: 2rem 1.5rem;
box-shadow: var(--shadow-soft);
}
.nav-link {
color: var(--text-primary);
padding: 1rem 1.5rem;
border-radius: 15px;
margin-bottom: 0.8rem;
transition: all 0.3s ease;
font-weight: 600;
}
.nav-link:hover {
background-color: #f7fafc;
color: var(--accent);
transform: translateX(5px);
}
.nav-link.active {
background: var(--accent-gradient);
color: white;
box-shadow: 0 4px 12px rgba(118, 75, 162, 0.3);
}
.main-content {
margin-left: 300px;
padding: 2.5rem;
}
.card {
border: none;
border-radius: var(--border-radius);
box-shadow: var(--shadow-soft);
transition: transform 0.3s ease;
overflow: hidden;
}
.card:hover {
transform: translateY(-5px);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.btn-primary {
background: var(--accent-gradient);
border: none;
padding: 0.8rem 2rem;
border-radius: 15px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(118, 75, 162, 0.3);
transition: all 0.3s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(118, 75, 162, 0.4);
}
.search-bar {
background: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 15px;
box-shadow: var(--shadow-soft);
width: 300px;
}
.table th {
font-weight: 700;
color: var(--text-secondary);
border: none;
text-transform: uppercase;
font-size: 0.8rem;
padding: 1.5rem 1rem;
}
.table td {
padding: 1.5rem 1rem;
border-bottom: 1px solid #f0f2f5;
vertical-align: middle;
}
.table tr:last-child td {
border-bottom: none;
}
.badge-soft {
padding: 0.5rem 1rem;
border-radius: 10px;
font-weight: 600;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="d-flex align-items-center mb-5 px-3">
<div class="bg-white p-2 rounded-3 shadow-sm me-3">
<i class="bi bi-cloud-check-fill fs-4" style="color: #667eea;"></i>
</div>
<h4 class="mb-0 fw-bold">BMC Hub</h4>
</div>
<nav class="nav flex-column">
<a class="nav-link active" href="index.html"><i class="bi bi-grid-fill me-3"></i> Dashboard</a>
<a class="nav-link" href="customers.html"><i class="bi bi-people-fill me-3"></i> Kunder</a>
<a class="nav-link" href="#"><i class="bi bi-router-fill me-3"></i> Hardware</a>
<a class="nav-link" href="#"><i class="bi bi-receipt me-3"></i> Fakturering</a>
<a class="nav-link" href="#"><i class="bi bi-gear-fill me-3"></i> Indstillinger</a>
</nav>
<div class="mt-auto px-3">
<div class="card bg-light p-3 border-0">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-white p-1 me-2">
<img src="https://ui-avatars.com/api/?name=Christian+Thomas&background=667eea&color=fff" class="rounded-circle" width="32">
</div>
<div>
<small class="d-block fw-bold">Christian</small>
<small class="text-muted" style="font-size: 0.7rem;">Admin</small>
</div>
</div>
</div>
</div>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Godmorgen, Christian! 👋</h2>
<p class="text-secondary mb-0">Her er dit overblik for i dag</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="search-bar" placeholder="Søg...">
<button class="btn btn-primary"><i class="bi bi-plus-lg"></i> Ny Opgave</button>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="stat-icon bg-primary-subtle text-primary">
<i class="bi bi-people"></i>
</div>
<h3 class="fw-bold mb-1">124</h3>
<p class="text-secondary mb-0">Aktive Kunder</p>
</div>
</div>
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="stat-icon bg-success-subtle text-success">
<i class="bi bi-hdd-network"></i>
</div>
<h3 class="fw-bold mb-1">856</h3>
<p class="text-secondary mb-0">Hardware Enheder</p>
</div>
</div>
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="stat-icon bg-warning-subtle text-warning">
<i class="bi bi-ticket-perforated"></i>
</div>
<h3 class="fw-bold mb-1">12</h3>
<p class="text-secondary mb-0">Support Sager</p>
</div>
</div>
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="stat-icon bg-info-subtle text-info">
<i class="bi bi-graph-up"></i>
</div>
<h3 class="fw-bold mb-1">450k</h3>
<p class="text-secondary mb-0">Månedlig Omsætning</p>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-8">
<div class="card p-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="fw-bold">Seneste Aktiviteter</h5>
<button class="btn btn-light btn-sm rounded-pill px-3">Se alle</button>
</div>
<table class="table">
<thead>
<tr>
<th>Kunde</th>
<th>Handling</th>
<th>Status</th>
<th>Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-3 bg-light p-2 me-3">
<i class="bi bi-building"></i>
</div>
<span class="fw-bold">Advokatgruppen A/S</span>
</div>
</td>
<td>Firewall konfiguration</td>
<td><span class="badge bg-success-subtle text-success badge-soft">Fuldført</span></td>
<td class="text-secondary">10:23</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-3 bg-light p-2 me-3">
<i class="bi bi-building"></i>
</div>
<span class="fw-bold">Byg & Bo ApS</span>
</div>
</td>
<td>Licens fornyelse</td>
<td><span class="badge bg-warning-subtle text-warning badge-soft">Afventer</span></td>
<td class="text-secondary">I går</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-3 bg-light p-2 me-3">
<i class="bi bi-building"></i>
</div>
<span class="fw-bold">Cafe Møller</span>
</div>
</td>
<td>Netværksnedbrud</td>
<td><span class="badge bg-danger-subtle text-danger badge-soft">Kritisk</span></td>
<td class="text-secondary">I går</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-4">
<div class="card p-4 h-100 bg-primary text-white" style="background: var(--accent-gradient);">
<h5 class="fw-bold mb-4">System Status</h5>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="opacity-75">CPU Usage</span>
<span class="fw-bold">24%</span>
</div>
<div class="progress bg-white bg-opacity-25" style="height: 6px;">
<div class="progress-bar bg-white" style="width: 24%"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="opacity-75">Memory</span>
<span class="fw-bold">56%</span>
</div>
<div class="progress bg-white bg-opacity-25" style="height: 6px;">
<div class="progress-bar bg-white" style="width: 56%"></div>
</div>
</div>
<div class="mt-auto">
<div class="bg-white bg-opacity-10 p-3 rounded-3 backdrop-blur">
<div class="d-flex">
<i class="bi bi-check-circle-fill me-2"></i>
<small>Alle systemer kører normalt. Ingen nedetid registreret de sidste 24 timer.</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Compact Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #e9ecef;
--bg-sidebar: #212529;
--accent: #0d6efd;
}
body {
background-color: var(--bg-body);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 0.85rem;
}
.sidebar {
background: var(--bg-sidebar);
height: 100vh;
position: fixed;
width: 220px;
padding-top: 1rem;
}
.nav-link {
color: #adb5bd;
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
.nav-link:hover, .nav-link.active {
color: white;
background-color: rgba(255,255,255,0.1);
}
.main-content {
margin-left: 220px;
padding: 1rem;
}
.card {
border: 1px solid #dee2e6;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
border-radius: 4px;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 0.5rem 1rem;
font-weight: 600;
}
.table th {
background-color: #f8f9fa;
font-size: 0.8rem;
text-transform: uppercase;
color: #6c757d;
}
.table td {
padding: 0.4rem 0.75rem;
vertical-align: middle;
}
.btn-sm {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="px-3 mb-3">
<h6 class="text-white fw-bold mb-0">BMC HUB <span class="badge bg-primary ms-1">v1.0</span></h6>
</div>
<nav class="nav flex-column">
<a class="nav-link" href="index.html"><i class="bi bi-speedometer2 me-2"></i> Dashboard</a>
<a class="nav-link active" href="customers.html"><i class="bi bi-people me-2"></i> Kunder</a>
<a class="nav-link" href="#"><i class="bi bi-hdd-network me-2"></i> Hardware</a>
<a class="nav-link" href="#"><i class="bi bi-receipt me-2"></i> Fakturering</a>
<div class="border-top border-secondary my-2 mx-3"></div>
<a class="nav-link" href="#"><i class="bi bi-gear me-2"></i> Indstillinger</a>
<a class="nav-link" href="#"><i class="bi bi-journal-text me-2"></i> Logs</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-3 bg-white p-2 border rounded">
<div class="d-flex align-items-center gap-3">
<h5 class="mb-0 fw-bold">Kundeoversigt</h5>
</div>
<div class="d-flex gap-2">
<input type="text" class="form-control form-control-sm" placeholder="Søg kunde..." style="width: 200px;">
<button class="btn btn-sm btn-primary"><i class="bi bi-plus"></i> Opret Kunde</button>
</div>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center py-1">
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary active">Alle</button>
<button class="btn btn-outline-secondary">Aktive</button>
<button class="btn btn-outline-secondary">Inaktive</button>
<button class="btn btn-outline-secondary">VIP</button>
</div>
<div class="text-muted small">Total: 124 kunder</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0 table-bordered">
<thead>
<tr>
<th style="width: 40px;">ID</th>
<th>Virksomhed</th>
<th>Kontaktperson</th>
<th>CVR</th>
<th>Status</th>
<th>Hardware</th>
<th class="text-end">Handling</th>
</tr>
</thead>
<tbody>
<tr>
<td>101</td>
<td>
<div class="fw-bold">Advokatgruppen A/S</div>
<div class="small text-muted">København K</div>
</td>
<td>
<div>Jens Jensen</div>
<div class="small text-muted">jens@advokat.dk</div>
</td>
<td>12345678</td>
<td><span class="badge bg-success">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border">Firewall</span>
<span class="badge bg-light text-dark border">Switch</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary">Rediger</button>
</td>
</tr>
<tr>
<td>102</td>
<td>
<div class="fw-bold">Byg & Bo ApS</div>
<div class="small text-muted">Aarhus C</div>
</td>
<td>
<div>Mette Hansen</div>
<div class="small text-muted">mh@bygbo.dk</div>
</td>
<td>87654321</td>
<td><span class="badge bg-success">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border">Router</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary">Rediger</button>
</td>
</tr>
<tr>
<td>103</td>
<td>
<div class="fw-bold">Cafe Møller</div>
<div class="small text-muted">Odense M</div>
</td>
<td>
<div>Peter Møller</div>
<div class="small text-muted">pm@cafe.dk</div>
</td>
<td>11223344</td>
<td><span class="badge bg-warning text-dark">Afventer</span></td>
<td>
<span class="text-muted">-</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary">Rediger</button>
</td>
</tr>
<tr>
<td>104</td>
<td>
<div class="fw-bold">Dansk Design Hus</div>
<div class="small text-muted">København Ø</div>
</td>
<td>
<div>Lars Larsen</div>
<div class="small text-muted">ll@design.dk</div>
</td>
<td>44332211</td>
<td><span class="badge bg-success">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border">Firewall</span>
<span class="badge bg-light text-dark border">AP x4</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary">Rediger</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer py-1">
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm justify-content-end mb-0">
<li class="page-item disabled"><a class="page-link" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,269 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Compact Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-body: #e9ecef;
--bg-sidebar: #212529;
--accent: #0d6efd;
}
body {
background-color: var(--bg-body);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 0.85rem;
}
.sidebar {
background: var(--bg-sidebar);
height: 100vh;
position: fixed;
width: 220px;
padding-top: 1rem;
}
.nav-link {
color: #adb5bd;
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
.nav-link:hover, .nav-link.active {
color: white;
background-color: rgba(255,255,255,0.1);
}
.main-content {
margin-left: 220px;
padding: 1rem;
}
.card {
border: 1px solid #dee2e6;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
border-radius: 4px;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 0.5rem 1rem;
font-weight: 600;
}
.table th {
background-color: #f8f9fa;
font-size: 0.8rem;
text-transform: uppercase;
color: #6c757d;
}
.table td {
padding: 0.4rem 0.75rem;
vertical-align: middle;
}
.btn-sm {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="px-3 mb-3">
<h6 class="text-white fw-bold mb-0">BMC HUB <span class="badge bg-primary ms-1">v1.0</span></h6>
</div>
<nav class="nav flex-column">
<a class="nav-link active" href="index.html"><i class="bi bi-speedometer2 me-2"></i> Dashboard</a>
<a class="nav-link" href="customers.html"><i class="bi bi-people me-2"></i> Kunder</a>
<a class="nav-link" href="#"><i class="bi bi-hdd-network me-2"></i> Hardware</a>
<a class="nav-link" href="#"><i class="bi bi-receipt me-2"></i> Fakturering</a>
<div class="border-top border-secondary my-2 mx-3"></div>
<a class="nav-link" href="#"><i class="bi bi-gear me-2"></i> Indstillinger</a>
<a class="nav-link" href="#"><i class="bi bi-journal-text me-2"></i> Logs</a>
</nav>
</div>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-3 bg-white p-2 border rounded">
<div class="d-flex align-items-center gap-3">
<h5 class="mb-0 fw-bold">Dashboard</h5>
<span class="text-muted border-start ps-3">System Status: <span class="text-success fw-bold">Online</span></span>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-clockwise"></i> Opdater</button>
<button class="btn btn-sm btn-primary"><i class="bi bi-plus"></i> Ny Opgave</button>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-md-3">
<div class="card">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small text-uppercase">Kunder</div>
<div class="fs-4 fw-bold">124</div>
</div>
<i class="bi bi-people fs-3 text-primary"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small text-uppercase">Hardware</div>
<div class="fs-4 fw-bold">856</div>
</div>
<i class="bi bi-hdd fs-3 text-success"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small text-uppercase">Support</div>
<div class="fs-4 fw-bold text-danger">12</div>
</div>
<i class="bi bi-exclamation-circle fs-3 text-danger"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="text-muted small text-uppercase">Omsætning</div>
<div class="fs-4 fw-bold">450k</div>
</div>
<i class="bi bi-currency-dollar fs-3 text-info"></i>
</div>
</div>
</div>
</div>
</div>
<div class="row g-2">
<div class="col-md-8">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center py-1">
<span>Seneste Aktiviteter</span>
<input type="text" class="form-control form-control-sm w-auto" placeholder="Filter..." style="height: 24px;">
</div>
<div class="table-responsive">
<table class="table table-striped table-hover mb-0 table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Kunde</th>
<th>Handling</th>
<th>Status</th>
<th>Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td>#1023</td>
<td>Advokatgruppen A/S</td>
<td>Firewall konfiguration</td>
<td><span class="badge bg-success">Fuldført</span></td>
<td>10:23</td>
</tr>
<tr>
<td>#1022</td>
<td>Byg & Bo ApS</td>
<td>Licens fornyelse</td>
<td><span class="badge bg-warning text-dark">Afventer</span></td>
<td>I går</td>
</tr>
<tr>
<td>#1021</td>
<td>Cafe Møller</td>
<td>Netværksnedbrud</td>
<td><span class="badge bg-danger">Kritisk</span></td>
<td>I går</td>
</tr>
<tr>
<td>#1020</td>
<td>Dansk Design Hus</td>
<td>Ny bruger oprettet</td>
<td><span class="badge bg-success">Fuldført</span></td>
<td>2 dage siden</td>
</tr>
<tr>
<td>#1019</td>
<td>Elektriker Madsen</td>
<td>VPN opsætning</td>
<td><span class="badge bg-success">Fuldført</span></td>
<td>2 dage siden</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-2">
<div class="card-header py-1">System Ressourcer</div>
<div class="card-body p-2">
<div class="mb-2">
<div class="d-flex justify-content-between small mb-1">
<span>CPU</span>
<span>24%</span>
</div>
<div class="progress" style="height: 4px;">
<div class="progress-bar" style="width: 24%"></div>
</div>
</div>
<div class="mb-2">
<div class="d-flex justify-content-between small mb-1">
<span>RAM</span>
<span>56%</span>
</div>
<div class="progress" style="height: 4px;">
<div class="progress-bar bg-info" style="width: 56%"></div>
</div>
</div>
<div>
<div class="d-flex justify-content-between small mb-1">
<span>Disk</span>
<span>89%</span>
</div>
<div class="progress" style="height: 4px;">
<div class="progress-bar bg-warning" style="width: 89%"></div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header py-1">Hurtig Handlinger</div>
<div class="list-group list-group-flush">
<a href="#" class="list-group-item list-group-item-action py-2 small"><i class="bi bi-plus-circle me-2"></i> Opret ny kunde</a>
<a href="#" class="list-group-item list-group-item-action py-2 small"><i class="bi bi-hdd-network me-2"></i> Tilføj hardware</a>
<a href="#" class="list-group-item list-group-item-action py-2 small"><i class="bi bi-file-earmark-text me-2"></i> Se system logs</a>
<a href="#" class="list-group-item list-group-item-action py-2 small"><i class="bi bi-shield-lock me-2"></i> Sikkerhedsscan</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,292 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Glass Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--glass-bg: rgba(255, 255, 255, 0.1);
--glass-border: rgba(255, 255, 255, 0.2);
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
--text-white: rgba(255, 255, 255, 0.9);
--text-muted: rgba(255, 255, 255, 0.6);
}
body {
background: var(--bg-gradient);
min-height: 100vh;
font-family: 'Segoe UI', sans-serif;
color: var(--text-white);
overflow-x: hidden;
}
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: 24px;
box-shadow: var(--glass-shadow);
}
.sidebar {
position: fixed;
left: 20px;
top: 20px;
bottom: 20px;
width: 100px;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 0;
z-index: 100;
}
.nav-item {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16px;
margin-bottom: 1.5rem;
color: var(--text-muted);
transition: all 0.3s;
font-size: 1.5rem;
text-decoration: none;
}
.nav-item:hover, .nav-item.active {
background: rgba(255, 255, 255, 0.2);
color: white;
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.main-content {
margin-left: 140px;
padding: 20px 40px 20px 0;
}
.table {
--bs-table-bg: transparent;
--bs-table-color: var(--text-white);
--bs-table-border-color: var(--glass-border);
}
.table td, .table th {
padding: 1.2rem 1rem;
vertical-align: middle;
}
.search-glass {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--glass-border);
color: white;
border-radius: 30px;
padding: 0.7rem 1.5rem;
width: 300px;
}
.search-glass::placeholder { color: var(--text-muted); }
.search-glass:focus { outline: none; background: rgba(0, 0, 0, 0.3); color: white; }
.btn-glass {
background: rgba(255, 255, 255, 0.2);
border: 1px solid var(--glass-border);
color: white;
border-radius: 30px;
padding: 0.7rem 2rem;
font-weight: 600;
transition: all 0.3s;
}
.btn-glass:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.filter-pill {
background: transparent;
border: 1px solid var(--glass-border);
color: var(--text-muted);
padding: 0.5rem 1.5rem;
border-radius: 50px;
transition: all 0.3s;
}
.filter-pill:hover, .filter-pill.active {
background: rgba(255, 255, 255, 0.2);
color: white;
border-color: white;
}
/* Floating shapes for background */
.shape {
position: fixed;
border-radius: 50%;
filter: blur(80px);
z-index: -1;
}
.shape-1 { top: -10%; left: -10%; width: 500px; height: 500px; background: #ff00cc; opacity: 0.4; }
.shape-2 { bottom: -10%; right: -10%; width: 600px; height: 600px; background: #3333ff; opacity: 0.4; }
</style>
</head>
<body>
<div class="shape shape-1"></div>
<div class="shape shape-2"></div>
<nav class="sidebar glass-panel">
<div class="mb-5">
<i class="bi bi-hexagon-fill fs-2 text-white"></i>
</div>
<a href="index.html" class="nav-item" title="Dashboard">
<i class="bi bi-grid"></i>
</a>
<a href="customers.html" class="nav-item active" title="Kunder">
<i class="bi bi-people"></i>
</a>
<a href="#" class="nav-item" title="Hardware">
<i class="bi bi-hdd-network"></i>
</a>
<a href="#" class="nav-item" title="Fakturering">
<i class="bi bi-receipt"></i>
</a>
<div class="mt-auto">
<a href="#" class="nav-item" title="Indstillinger">
<i class="bi bi-gear"></i>
</a>
<a href="#" class="nav-item mb-0">
<img src="https://ui-avatars.com/api/?name=CT&background=random" class="rounded-circle" width="40">
</a>
</div>
</nav>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Kunder</h2>
<p style="color: var(--text-muted)">Administrer dine kunder</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="search-glass" placeholder="Søg kunde...">
<button class="btn-glass"><i class="bi bi-plus-lg me-2"></i>Opret</button>
</div>
</div>
<div class="glass-panel p-4">
<div class="d-flex gap-2 mb-4">
<button class="filter-pill active">Alle</button>
<button class="filter-pill">Aktive</button>
<button class="filter-pill">Inaktive</button>
<button class="filter-pill">VIP</button>
</div>
<table class="table">
<thead>
<tr>
<th style="color: var(--text-muted); font-weight: 400;">Virksomhed</th>
<th style="color: var(--text-muted); font-weight: 400;">Kontakt</th>
<th style="color: var(--text-muted); font-weight: 400;">CVR</th>
<th style="color: var(--text-muted); font-weight: 400;">Status</th>
<th style="color: var(--text-muted); font-weight: 400;">Hardware</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-white bg-opacity-25 d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">A</div>
<div>
<div class="fw-bold">Advokatgruppen A/S</div>
<div class="small" style="color: var(--text-muted)">København K</div>
</div>
</div>
</td>
<td>
<div>Jens Jensen</div>
<div class="small" style="color: var(--text-muted)">jens@advokat.dk</div>
</td>
<td style="color: var(--text-muted)">12345678</td>
<td><span class="badge bg-success bg-opacity-25 text-white border border-success border-opacity-25">Aktiv</span></td>
<td>
<span class="badge bg-white bg-opacity-10 fw-normal">Firewall</span>
<span class="badge bg-white bg-opacity-10 fw-normal">Switch</span>
</td>
<td class="text-end">
<button class="btn btn-sm text-white"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-white bg-opacity-25 d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">B</div>
<div>
<div class="fw-bold">Byg & Bo ApS</div>
<div class="small" style="color: var(--text-muted)">Aarhus C</div>
</div>
</div>
</td>
<td>
<div>Mette Hansen</div>
<div class="small" style="color: var(--text-muted)">mh@bygbo.dk</div>
</td>
<td style="color: var(--text-muted)">87654321</td>
<td><span class="badge bg-success bg-opacity-25 text-white border border-success border-opacity-25">Aktiv</span></td>
<td>
<span class="badge bg-white bg-opacity-10 fw-normal">Router</span>
</td>
<td class="text-end">
<button class="btn btn-sm text-white"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-white bg-opacity-25 d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">C</div>
<div>
<div class="fw-bold">Cafe Møller</div>
<div class="small" style="color: var(--text-muted)">Odense M</div>
</div>
</div>
</td>
<td>
<div>Peter Møller</div>
<div class="small" style="color: var(--text-muted)">pm@cafe.dk</div>
</td>
<td style="color: var(--text-muted)">11223344</td>
<td><span class="badge bg-warning bg-opacity-25 text-white border border-warning border-opacity-25">Afventer</span></td>
<td>
<span style="color: var(--text-muted)">-</span>
</td>
<td class="text-end">
<button class="btn btn-sm text-white"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center mt-4">
<nav>
<ul class="pagination">
<li class="page-item disabled"><a class="page-link bg-transparent border-0 text-white" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link bg-white bg-opacity-25 border-0 text-white rounded-circle mx-1" href="#">1</a></li>
<li class="page-item"><a class="page-link bg-transparent border-0 text-white" href="#">2</a></li>
<li class="page-item"><a class="page-link bg-transparent border-0 text-white" href="#">3</a></li>
<li class="page-item"><a class="page-link bg-transparent border-0 text-white" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,295 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Glass Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--glass-bg: rgba(255, 255, 255, 0.1);
--glass-border: rgba(255, 255, 255, 0.2);
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
--text-white: rgba(255, 255, 255, 0.9);
--text-muted: rgba(255, 255, 255, 0.6);
}
body {
background: var(--bg-gradient);
min-height: 100vh;
font-family: 'Segoe UI', sans-serif;
color: var(--text-white);
overflow-x: hidden;
}
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: 24px;
box-shadow: var(--glass-shadow);
}
.sidebar {
position: fixed;
left: 20px;
top: 20px;
bottom: 20px;
width: 100px;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 0;
z-index: 100;
}
.nav-item {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16px;
margin-bottom: 1.5rem;
color: var(--text-muted);
transition: all 0.3s;
font-size: 1.5rem;
text-decoration: none;
}
.nav-item:hover, .nav-item.active {
background: rgba(255, 255, 255, 0.2);
color: white;
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.main-content {
margin-left: 140px;
padding: 20px 40px 20px 0;
}
.stat-card {
padding: 1.5rem;
height: 100%;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
background: rgba(255, 255, 255, 0.15);
}
.table {
--bs-table-bg: transparent;
--bs-table-color: var(--text-white);
--bs-table-border-color: var(--glass-border);
}
.table td, .table th {
padding: 1rem;
vertical-align: middle;
}
.search-glass {
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--glass-border);
color: white;
border-radius: 30px;
padding: 0.7rem 1.5rem;
width: 300px;
}
.search-glass::placeholder { color: var(--text-muted); }
.search-glass:focus { outline: none; background: rgba(0, 0, 0, 0.3); color: white; }
.btn-glass {
background: rgba(255, 255, 255, 0.2);
border: 1px solid var(--glass-border);
color: white;
border-radius: 30px;
padding: 0.7rem 2rem;
font-weight: 600;
transition: all 0.3s;
}
.btn-glass:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
/* Floating shapes for background */
.shape {
position: fixed;
border-radius: 50%;
filter: blur(80px);
z-index: -1;
}
.shape-1 { top: -10%; left: -10%; width: 500px; height: 500px; background: #ff00cc; opacity: 0.4; }
.shape-2 { bottom: -10%; right: -10%; width: 600px; height: 600px; background: #3333ff; opacity: 0.4; }
</style>
</head>
<body>
<div class="shape shape-1"></div>
<div class="shape shape-2"></div>
<nav class="sidebar glass-panel">
<div class="mb-5">
<i class="bi bi-hexagon-fill fs-2 text-white"></i>
</div>
<a href="index.html" class="nav-item active" title="Dashboard">
<i class="bi bi-grid"></i>
</a>
<a href="customers.html" class="nav-item" title="Kunder">
<i class="bi bi-people"></i>
</a>
<a href="#" class="nav-item" title="Hardware">
<i class="bi bi-hdd-network"></i>
</a>
<a href="#" class="nav-item" title="Fakturering">
<i class="bi bi-receipt"></i>
</a>
<div class="mt-auto">
<a href="#" class="nav-item" title="Indstillinger">
<i class="bi bi-gear"></i>
</a>
<a href="#" class="nav-item mb-0">
<img src="https://ui-avatars.com/api/?name=CT&background=random" class="rounded-circle" width="40">
</a>
</div>
</nav>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h2 class="fw-bold mb-1">Dashboard</h2>
<p style="color: var(--text-muted)">Velkommen tilbage, Christian</p>
</div>
<div class="d-flex gap-3">
<input type="text" class="search-glass" placeholder="Søg...">
<button class="btn-glass"><i class="bi bi-plus-lg me-2"></i>Ny Opgave</button>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-3">
<div class="glass-panel stat-card">
<div class="d-flex justify-content-between mb-3">
<span style="color: var(--text-muted)">Aktive Kunder</span>
<i class="bi bi-people text-white"></i>
</div>
<h2 class="fw-bold mb-0">124</h2>
<small class="text-success">+12% denne måned</small>
</div>
</div>
<div class="col-md-3">
<div class="glass-panel stat-card">
<div class="d-flex justify-content-between mb-3">
<span style="color: var(--text-muted)">Hardware</span>
<i class="bi bi-hdd text-white"></i>
</div>
<h2 class="fw-bold mb-0">856</h2>
<small style="color: var(--text-muted)">Enheder online</small>
</div>
</div>
<div class="col-md-3">
<div class="glass-panel stat-card">
<div class="d-flex justify-content-between mb-3">
<span style="color: var(--text-muted)">Support</span>
<i class="bi bi-ticket text-white"></i>
</div>
<h2 class="fw-bold mb-0">12</h2>
<small class="text-warning">3 kræver handling</small>
</div>
</div>
<div class="col-md-3">
<div class="glass-panel stat-card">
<div class="d-flex justify-content-between mb-3">
<span style="color: var(--text-muted)">Omsætning</span>
<i class="bi bi-currency-dollar text-white"></i>
</div>
<h2 class="fw-bold mb-0">450k</h2>
<small class="text-success">Over budget</small>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-md-8">
<div class="glass-panel p-4">
<h4 class="fw-bold mb-4">Seneste Aktiviteter</h4>
<table class="table">
<thead>
<tr>
<th style="color: var(--text-muted); font-weight: 400;">Kunde</th>
<th style="color: var(--text-muted); font-weight: 400;">Handling</th>
<th style="color: var(--text-muted); font-weight: 400;">Status</th>
<th style="color: var(--text-muted); font-weight: 400;">Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td class="fw-bold">Advokatgruppen A/S</td>
<td>Firewall konfiguration</td>
<td><span class="badge bg-success bg-opacity-25 text-white border border-success border-opacity-25">Fuldført</span></td>
<td style="color: var(--text-muted)">10:23</td>
</tr>
<tr>
<td class="fw-bold">Byg & Bo ApS</td>
<td>Licens fornyelse</td>
<td><span class="badge bg-warning bg-opacity-25 text-white border border-warning border-opacity-25">Afventer</span></td>
<td style="color: var(--text-muted)">I går</td>
</tr>
<tr>
<td class="fw-bold">Cafe Møller</td>
<td>Netværksnedbrud</td>
<td><span class="badge bg-danger bg-opacity-25 text-white border border-danger border-opacity-25">Kritisk</span></td>
<td style="color: var(--text-muted)">I går</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-4">
<div class="glass-panel p-4 h-100">
<h4 class="fw-bold mb-4">System Status</h4>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span>CPU Load</span>
<span class="fw-bold">24%</span>
</div>
<div class="progress" style="height: 6px; background: rgba(255,255,255,0.1);">
<div class="progress-bar bg-white" style="width: 24%"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span>Memory</span>
<span class="fw-bold">56%</span>
</div>
<div class="progress" style="height: 6px; background: rgba(255,255,255,0.1);">
<div class="progress-bar bg-white" style="width: 56%"></div>
</div>
</div>
<div class="mt-auto p-3 rounded" style="background: rgba(0,0,0,0.2);">
<div class="d-flex">
<i class="bi bi-check-circle-fill text-success me-2"></i>
<small>Alle systemer kører optimalt.</small>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,346 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Material Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--md-sys-color-primary: #6750A4;
--md-sys-color-on-primary: #FFFFFF;
--md-sys-color-primary-container: #EADDFF;
--md-sys-color-on-primary-container: #21005D;
--md-sys-color-surface: #FEF7FF;
--md-sys-color-surface-container: #F3EDF7;
--md-sys-color-on-surface: #1D1B20;
--md-sys-color-outline: #79747E;
--md-sys-shape-corner-large: 16px;
--md-sys-shape-corner-medium: 12px;
--md-sys-shape-corner-full: 100px;
}
body {
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
font-family: 'Roboto', sans-serif;
display: flex;
}
/* Navigation Rail */
.nav-rail {
width: 80px;
height: 100vh;
position: fixed;
background: var(--md-sys-color-surface);
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 0;
border-right: 1px solid #E7E0EC;
z-index: 100;
}
.nav-fab {
width: 56px;
height: 56px;
background: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: all 0.2s;
}
.nav-fab:hover {
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
transform: scale(1.05);
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 24px;
text-decoration: none;
color: var(--md-sys-color-on-surface);
width: 100%;
cursor: pointer;
}
.nav-icon-container {
width: 56px;
height: 32px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
transition: background-color 0.2s;
}
.nav-item.active .nav-icon-container {
background-color: var(--md-sys-color-primary-container);
}
.nav-item.active i {
color: var(--md-sys-color-on-primary-container);
}
.nav-label {
font-size: 12px;
font-weight: 500;
letter-spacing: 0.5px;
}
.main-content {
margin-left: 80px;
padding: 24px;
width: 100%;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.search-bar {
background: var(--md-sys-color-surface-container);
border-radius: 28px;
padding: 0 16px;
height: 56px;
display: flex;
align-items: center;
width: 400px;
}
.search-bar input {
border: none;
background: transparent;
margin-left: 12px;
width: 100%;
outline: none;
font-size: 16px;
}
.card-md {
background: var(--md-sys-color-surface-container);
border-radius: var(--md-sys-shape-corner-large);
padding: 24px;
border: none;
}
.btn-filled {
background: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-radius: 20px;
padding: 10px 24px;
border: none;
font-weight: 500;
letter-spacing: 0.1px;
transition: box-shadow 0.2s;
}
.btn-filled:hover {
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.table th {
font-weight: 500;
color: var(--md-sys-color-outline);
border-bottom: 1px solid #E7E0EC;
}
.table td {
padding: 16px 8px;
vertical-align: middle;
}
.chip {
display: inline-flex;
align-items: center;
padding: 6px 16px;
border-radius: 8px;
border: 1px solid #79747E;
margin-right: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
background: transparent;
}
.chip.active {
background: var(--md-sys-color-primary-container);
border-color: transparent;
color: var(--md-sys-color-on-primary-container);
}
.chip:hover:not(.active) {
background: rgba(0,0,0,0.05);
}
</style>
</head>
<body>
<nav class="nav-rail">
<div class="nav-fab">
<i class="bi bi-pencil-fill fs-5"></i>
</div>
<a href="index.html" class="nav-item">
<div class="nav-icon-container">
<i class="bi bi-grid"></i>
</div>
<span class="nav-label">Home</span>
</a>
<a href="customers.html" class="nav-item active">
<div class="nav-icon-container">
<i class="bi bi-people-fill"></i>
</div>
<span class="nav-label">Kunder</span>
</a>
<a href="#" class="nav-item">
<div class="nav-icon-container">
<i class="bi bi-hdd"></i>
</div>
<span class="nav-label">Udstyr</span>
</a>
<a href="#" class="nav-item">
<div class="nav-icon-container">
<i class="bi bi-gear"></i>
</div>
<span class="nav-label">Opsætning</span>
</a>
</nav>
<div class="main-content">
<div class="top-bar">
<h2 class="m-0 fw-normal">Kunder</h2>
<div class="search-bar">
<i class="bi bi-search text-muted"></i>
<input type="text" placeholder="Søg efter kunde...">
</div>
<div class="d-flex align-items-center gap-3">
<button class="btn-filled"><i class="bi bi-plus-lg me-2"></i>Opret Kunde</button>
</div>
</div>
<div class="mb-4">
<button class="chip active">Alle Kunder</button>
<button class="chip">Aktive</button>
<button class="chip">Inaktive</button>
<button class="chip">VIP</button>
</div>
<div class="card-md">
<table class="table table-hover">
<thead>
<tr>
<th>Virksomhed</th>
<th>Kontaktperson</th>
<th>CVR</th>
<th>Status</th>
<th>Hardware</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">A</div>
<div>
<div class="fw-medium">Advokatgruppen A/S</div>
<div class="small text-muted">København K</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Jens Jensen</div>
<div class="small text-muted">jens@advokat.dk</div>
</td>
<td class="text-muted">12345678</td>
<td><span class="badge rounded-pill text-bg-success bg-opacity-25 text-success">Aktiv</span></td>
<td>
<span class="badge bg-secondary bg-opacity-10 text-secondary border">Firewall</span>
<span class="badge bg-secondary bg-opacity-10 text-secondary border">Switch</span>
</td>
<td class="text-end">
<button class="btn btn-icon"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-secondary text-white d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">B</div>
<div>
<div class="fw-medium">Byg & Bo ApS</div>
<div class="small text-muted">Aarhus C</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Mette Hansen</div>
<div class="small text-muted">mh@bygbo.dk</div>
</td>
<td class="text-muted">87654321</td>
<td><span class="badge rounded-pill text-bg-success bg-opacity-25 text-success">Aktiv</span></td>
<td>
<span class="badge bg-secondary bg-opacity-10 text-secondary border">Router</span>
</td>
<td class="text-end">
<button class="btn btn-icon"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-danger text-white d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">C</div>
<div>
<div class="fw-medium">Cafe Møller</div>
<div class="small text-muted">Odense M</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Peter Møller</div>
<div class="small text-muted">pm@cafe.dk</div>
</td>
<td class="text-muted">11223344</td>
<td><span class="badge rounded-pill text-bg-warning bg-opacity-25 text-warning">Afventer</span></td>
<td>
<span class="text-muted">-</span>
</td>
<td class="text-end">
<button class="btn btn-icon"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-end mt-4">
<nav>
<ul class="pagination">
<li class="page-item disabled"><a class="page-link border-0 bg-transparent" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link border-0 rounded-circle mx-1" style="background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container);" href="#">1</a></li>
<li class="page-item"><a class="page-link border-0 bg-transparent text-dark" href="#">2</a></li>
<li class="page-item"><a class="page-link border-0 bg-transparent text-dark" href="#">3</a></li>
<li class="page-item"><a class="page-link border-0 bg-transparent text-dark" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,350 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Material Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--md-sys-color-primary: #6750A4;
--md-sys-color-on-primary: #FFFFFF;
--md-sys-color-primary-container: #EADDFF;
--md-sys-color-on-primary-container: #21005D;
--md-sys-color-surface: #FEF7FF;
--md-sys-color-surface-container: #F3EDF7;
--md-sys-color-on-surface: #1D1B20;
--md-sys-color-outline: #79747E;
--md-sys-shape-corner-large: 16px;
--md-sys-shape-corner-medium: 12px;
--md-sys-shape-corner-full: 100px;
}
body {
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
font-family: 'Roboto', sans-serif;
display: flex;
}
/* Navigation Rail */
.nav-rail {
width: 80px;
height: 100vh;
position: fixed;
background: var(--md-sys-color-surface);
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 0;
border-right: 1px solid #E7E0EC;
z-index: 100;
}
.nav-fab {
width: 56px;
height: 56px;
background: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: all 0.2s;
}
.nav-fab:hover {
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
transform: scale(1.05);
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 24px;
text-decoration: none;
color: var(--md-sys-color-on-surface);
width: 100%;
cursor: pointer;
}
.nav-icon-container {
width: 56px;
height: 32px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4px;
transition: background-color 0.2s;
}
.nav-item.active .nav-icon-container {
background-color: var(--md-sys-color-primary-container);
}
.nav-item.active i {
color: var(--md-sys-color-on-primary-container);
}
.nav-label {
font-size: 12px;
font-weight: 500;
letter-spacing: 0.5px;
}
.main-content {
margin-left: 80px;
padding: 24px;
width: 100%;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.search-bar {
background: var(--md-sys-color-surface-container);
border-radius: 28px;
padding: 0 16px;
height: 56px;
display: flex;
align-items: center;
width: 400px;
}
.search-bar input {
border: none;
background: transparent;
margin-left: 12px;
width: 100%;
outline: none;
font-size: 16px;
}
.card-md {
background: var(--md-sys-color-surface-container);
border-radius: var(--md-sys-shape-corner-large);
padding: 24px;
border: none;
height: 100%;
}
.btn-filled {
background: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-radius: 20px;
padding: 10px 24px;
border: none;
font-weight: 500;
letter-spacing: 0.1px;
transition: box-shadow 0.2s;
}
.btn-filled:hover {
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.table th {
font-weight: 500;
color: var(--md-sys-color-outline);
border-bottom: 1px solid #E7E0EC;
}
.table td {
padding: 16px 8px;
vertical-align: middle;
}
</style>
</head>
<body>
<nav class="nav-rail">
<div class="nav-fab">
<i class="bi bi-pencil-fill fs-5"></i>
</div>
<a href="index.html" class="nav-item active">
<div class="nav-icon-container">
<i class="bi bi-grid-fill"></i>
</div>
<span class="nav-label">Home</span>
</a>
<a href="customers.html" class="nav-item">
<div class="nav-icon-container">
<i class="bi bi-people"></i>
</div>
<span class="nav-label">Kunder</span>
</a>
<a href="#" class="nav-item">
<div class="nav-icon-container">
<i class="bi bi-hdd"></i>
</div>
<span class="nav-label">Udstyr</span>
</a>
<a href="#" class="nav-item">
<div class="nav-icon-container">
<i class="bi bi-gear"></i>
</div>
<span class="nav-label">Opsætning</span>
</a>
</nav>
<div class="main-content">
<div class="top-bar">
<h2 class="m-0 fw-normal">Dashboard</h2>
<div class="search-bar">
<i class="bi bi-search text-muted"></i>
<input type="text" placeholder="Søg i BMC Hub...">
</div>
<div class="d-flex align-items-center gap-3">
<button class="btn btn-icon rounded-circle"><i class="bi bi-bell fs-5"></i></button>
<img src="https://ui-avatars.com/api/?name=CT&background=6750A4&color=fff" class="rounded-circle" width="40">
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card-md">
<div class="d-flex align-items-center mb-3">
<i class="bi bi-people fs-4 me-2" style="color: var(--md-sys-color-primary)"></i>
<span class="fw-medium">Kunder</span>
</div>
<h2 class="display-5 fw-bold mb-0">124</h2>
<small class="text-muted">Aktive abonnementer</small>
</div>
</div>
<div class="col-md-3">
<div class="card-md">
<div class="d-flex align-items-center mb-3">
<i class="bi bi-hdd-network fs-4 me-2" style="color: var(--md-sys-color-primary)"></i>
<span class="fw-medium">Hardware</span>
</div>
<h2 class="display-5 fw-bold mb-0">856</h2>
<small class="text-muted">Enheder registreret</small>
</div>
</div>
<div class="col-md-3">
<div class="card-md">
<div class="d-flex align-items-center mb-3">
<i class="bi bi-exclamation-circle fs-4 me-2 text-danger"></i>
<span class="fw-medium">Support</span>
</div>
<h2 class="display-5 fw-bold mb-0">12</h2>
<small class="text-muted">Åbne sager</small>
</div>
</div>
<div class="col-md-3">
<div class="card-md">
<div class="d-flex align-items-center mb-3">
<i class="bi bi-graph-up fs-4 me-2 text-success"></i>
<span class="fw-medium">Omsætning</span>
</div>
<h2 class="display-5 fw-bold mb-0">450k</h2>
<small class="text-muted">Denne måned</small>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-8">
<div class="card-md">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="m-0">Seneste aktivitet</h5>
<button class="btn btn-sm btn-outline-secondary rounded-pill px-3">Se alle</button>
</div>
<table class="table table-hover">
<thead>
<tr>
<th>Kunde</th>
<th>Handling</th>
<th>Status</th>
<th>Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; font-size: 14px;">A</div>
<span class="fw-medium">Advokatgruppen A/S</span>
</div>
</td>
<td>Firewall konfiguration</td>
<td><span class="badge rounded-pill text-bg-success bg-opacity-25 text-success">Fuldført</span></td>
<td class="text-muted">10:23</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-secondary text-white d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; font-size: 14px;">B</div>
<span class="fw-medium">Byg & Bo ApS</span>
</div>
</td>
<td>Licens fornyelse</td>
<td><span class="badge rounded-pill text-bg-warning bg-opacity-25 text-warning">Afventer</span></td>
<td class="text-muted">I går</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-danger text-white d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; font-size: 14px;">C</div>
<span class="fw-medium">Cafe Møller</span>
</div>
</td>
<td>Netværksnedbrud</td>
<td><span class="badge rounded-pill text-bg-danger bg-opacity-25 text-danger">Kritisk</span></td>
<td class="text-muted">I går</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-4">
<div class="card-md h-100">
<h5 class="mb-4">System Status</h5>
<div class="mb-4">
<div class="d-flex justify-content-between mb-1">
<span class="text-muted small">CPU</span>
<span class="fw-bold small">24%</span>
</div>
<div class="progress" style="height: 8px; border-radius: 4px;">
<div class="progress-bar" style="width: 24%; background-color: var(--md-sys-color-primary);"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-1">
<span class="text-muted small">RAM</span>
<span class="fw-bold small">56%</span>
</div>
<div class="progress" style="height: 8px; border-radius: 4px;">
<div class="progress-bar" style="width: 56%; background-color: var(--md-sys-color-primary);"></div>
</div>
</div>
<div class="mt-auto p-3 rounded-4" style="background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container);">
<div class="d-flex">
<i class="bi bi-info-circle-fill me-2"></i>
<small class="fw-medium">System backup kører kl. 03:00</small>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Horizontal Clean Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--primary: #2563eb;
--bg-body: #f3f4f6;
--bg-nav: #ffffff;
}
body {
background-color: var(--bg-body);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
padding-top: 70px;
}
.navbar {
background-color: var(--bg-nav);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
height: 70px;
}
.navbar-brand {
font-weight: 700;
color: #111827;
font-size: 1.25rem;
}
.nav-link {
color: #4b5563;
font-weight: 500;
padding: 0.5rem 1rem !important;
border-radius: 6px;
transition: all 0.2s;
}
.nav-link:hover {
color: var(--primary);
background-color: #eff6ff;
}
.nav-link.active {
color: var(--primary);
background-color: #eff6ff;
}
.card {
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
border-radius: 8px;
}
.btn-primary {
background-color: var(--primary);
border-color: var(--primary);
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="#">
<div class="bg-primary text-white rounded p-1 me-2 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;">
<i class="bi bi-hdd-network-fill" style="font-size: 16px;"></i>
</div>
BMC Hub
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto">
<li class="nav-item">
<a class="nav-link" href="index.html">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="customers.html">Kunder</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Hardware</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Fakturering</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Rapporter</a>
</li>
</ul>
<div class="d-flex align-items-center gap-3">
<button class="btn btn-light rounded-circle border"><i class="bi bi-bell"></i></button>
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none text-dark dropdown-toggle" data-bs-toggle="dropdown">
<img src="https://ui-avatars.com/api/?name=CT&background=random" class="rounded-circle me-2" width="32">
<span class="small fw-bold">Christian</span>
</a>
</div>
</div>
</div>
</div>
</nav>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Kunder</h2>
<div class="d-flex gap-2">
<input type="text" class="form-control" placeholder="Søg kunde..." style="width: 250px;">
<button class="btn btn-primary">Opret Kunde</button>
</div>
</div>
<div class="card">
<div class="card-header bg-white border-bottom py-3 d-flex gap-2">
<button class="btn btn-sm btn-dark rounded-pill px-3">Alle</button>
<button class="btn btn-sm btn-light border rounded-pill px-3">Aktive</button>
<button class="btn btn-sm btn-light border rounded-pill px-3">Inaktive</button>
<button class="btn btn-sm btn-light border rounded-pill px-3">VIP</button>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="bg-light">
<tr>
<th class="border-0 text-muted small text-uppercase fw-bold ps-4">Virksomhed</th>
<th class="border-0 text-muted small text-uppercase fw-bold">Kontakt</th>
<th class="border-0 text-muted small text-uppercase fw-bold">CVR</th>
<th class="border-0 text-muted small text-uppercase fw-bold">Status</th>
<th class="border-0 text-muted small text-uppercase fw-bold">Hardware</th>
<th class="border-0 text-muted small text-uppercase fw-bold pe-4 text-end"></th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="bg-light rounded p-2 me-3 text-primary fw-bold">AG</div>
<div>
<div class="fw-bold">Advokatgruppen A/S</div>
<div class="small text-muted">København K</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Jens Jensen</div>
<div class="small text-muted">jens@advokat.dk</div>
</td>
<td class="text-muted">12345678</td>
<td><span class="badge bg-success bg-opacity-10 text-success rounded-pill px-3">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border fw-normal">Firewall</span>
<span class="badge bg-light text-dark border fw-normal">Switch</span>
</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-light border"><i class="bi bi-three-dots"></i></button>
</td>
</tr>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="bg-light rounded p-2 me-3 text-primary fw-bold">BB</div>
<div>
<div class="fw-bold">Byg & Bo ApS</div>
<div class="small text-muted">Aarhus C</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Mette Hansen</div>
<div class="small text-muted">mh@bygbo.dk</div>
</td>
<td class="text-muted">87654321</td>
<td><span class="badge bg-success bg-opacity-10 text-success rounded-pill px-3">Aktiv</span></td>
<td>
<span class="badge bg-light text-dark border fw-normal">Router</span>
</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-light border"><i class="bi bi-three-dots"></i></button>
</td>
</tr>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="bg-light rounded p-2 me-3 text-primary fw-bold">CM</div>
<div>
<div class="fw-bold">Cafe Møller</div>
<div class="small text-muted">Odense M</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Peter Møller</div>
<div class="small text-muted">pm@cafe.dk</div>
</td>
<td class="text-muted">11223344</td>
<td><span class="badge bg-warning bg-opacity-10 text-warning rounded-pill px-3">Afventer</span></td>
<td>
<span class="text-muted">-</span>
</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-light border"><i class="bi bi-three-dots"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer bg-white py-3">
<nav>
<ul class="pagination justify-content-center mb-0">
<li class="page-item disabled"><a class="page-link border-0" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link border-0 bg-primary rounded-circle mx-1" href="#">1</a></li>
<li class="page-item"><a class="page-link border-0 text-dark" href="#">2</a></li>
<li class="page-item"><a class="page-link border-0 text-dark" href="#">3</a></li>
<li class="page-item"><a class="page-link border-0 text-dark" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Horizontal Clean</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--primary: #2563eb;
--bg-body: #f3f4f6;
--bg-nav: #ffffff;
}
body {
background-color: var(--bg-body);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
padding-top: 70px;
}
.navbar {
background-color: var(--bg-nav);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
height: 70px;
}
.navbar-brand {
font-weight: 700;
color: #111827;
font-size: 1.25rem;
}
.nav-link {
color: #4b5563;
font-weight: 500;
padding: 0.5rem 1rem !important;
border-radius: 6px;
transition: all 0.2s;
}
.nav-link:hover {
color: var(--primary);
background-color: #eff6ff;
}
.nav-link.active {
color: var(--primary);
background-color: #eff6ff;
}
.card {
border: none;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
border-radius: 8px;
}
.stat-label {
color: #6b7280;
font-size: 0.875rem;
font-weight: 500;
}
.stat-value {
color: #111827;
font-size: 1.875rem;
font-weight: 600;
}
.btn-primary {
background-color: var(--primary);
border-color: var(--primary);
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="#">
<div class="bg-primary text-white rounded p-1 me-2 d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;">
<i class="bi bi-hdd-network-fill" style="font-size: 16px;"></i>
</div>
BMC Hub
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mx-auto">
<li class="nav-item">
<a class="nav-link active" href="index.html">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="customers.html">Kunder</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Hardware</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Fakturering</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Rapporter</a>
</li>
</ul>
<div class="d-flex align-items-center gap-3">
<button class="btn btn-light rounded-circle border"><i class="bi bi-bell"></i></button>
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none text-dark dropdown-toggle" data-bs-toggle="dropdown">
<img src="https://ui-avatars.com/api/?name=CT&background=random" class="rounded-circle me-2" width="32">
<span class="small fw-bold">Christian</span>
</a>
</div>
</div>
</div>
</div>
</nav>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Dashboard</h2>
<div class="d-flex gap-2">
<button class="btn btn-white border bg-white">Filter</button>
<button class="btn btn-primary">Ny Opgave</button>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="d-flex justify-content-between">
<div>
<div class="stat-label">Aktive Kunder</div>
<div class="stat-value">124</div>
</div>
<div class="bg-blue-100 text-primary p-2 rounded" style="background: #eff6ff; height: fit-content;">
<i class="bi bi-people"></i>
</div>
</div>
<div class="mt-3 text-success small">
<i class="bi bi-arrow-up"></i> 12% stigning
</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="d-flex justify-content-between">
<div>
<div class="stat-label">Hardware</div>
<div class="stat-value">856</div>
</div>
<div class="bg-green-100 text-success p-2 rounded" style="background: #f0fdf4; height: fit-content;">
<i class="bi bi-hdd"></i>
</div>
</div>
<div class="mt-3 text-muted small">
Enheder online
</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="d-flex justify-content-between">
<div>
<div class="stat-label">Support</div>
<div class="stat-value">12</div>
</div>
<div class="bg-red-100 text-danger p-2 rounded" style="background: #fef2f2; height: fit-content;">
<i class="bi bi-ticket"></i>
</div>
</div>
<div class="mt-3 text-danger small">
3 kræver handling
</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-4 h-100">
<div class="d-flex justify-content-between">
<div>
<div class="stat-label">Omsætning</div>
<div class="stat-value">450k</div>
</div>
<div class="bg-purple-100 text-info p-2 rounded" style="background: #f5f3ff; height: fit-content;">
<i class="bi bi-currency-dollar"></i>
</div>
</div>
<div class="mt-3 text-success small">
+5% vs sidste md.
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-white border-bottom py-3">
<h5 class="card-title mb-0 fw-bold">Seneste Aktiviteter</h5>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="bg-light">
<tr>
<th class="border-0 text-muted small text-uppercase fw-bold ps-4">Kunde</th>
<th class="border-0 text-muted small text-uppercase fw-bold">Handling</th>
<th class="border-0 text-muted small text-uppercase fw-bold">Status</th>
<th class="border-0 text-muted small text-uppercase fw-bold pe-4 text-end">Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td class="ps-4 fw-medium">Advokatgruppen A/S</td>
<td>Firewall konfiguration</td>
<td><span class="badge bg-success bg-opacity-10 text-success rounded-pill px-3">Fuldført</span></td>
<td class="pe-4 text-end text-muted">10:23</td>
</tr>
<tr>
<td class="ps-4 fw-medium">Byg & Bo ApS</td>
<td>Licens fornyelse</td>
<td><span class="badge bg-warning bg-opacity-10 text-warning rounded-pill px-3">Afventer</span></td>
<td class="pe-4 text-end text-muted">I går</td>
</tr>
<tr>
<td class="ps-4 fw-medium">Cafe Møller</td>
<td>Netværksnedbrud</td>
<td><span class="badge bg-danger bg-opacity-10 text-danger rounded-pill px-3">Kritisk</span></td>
<td class="pe-4 text-end text-muted">I går</td>
</tr>
<tr>
<td class="ps-4 fw-medium">Dansk Design Hus</td>
<td>Ny bruger oprettet</td>
<td><span class="badge bg-success bg-opacity-10 text-success rounded-pill px-3">Fuldført</span></td>
<td class="pe-4 text-end text-muted">2 dage siden</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card p-4">
<h5 class="fw-bold mb-4">System Status</h5>
<div class="mb-4">
<div class="d-flex justify-content-between mb-1">
<span class="small fw-bold text-muted">CPU USAGE</span>
<span class="small fw-bold">24%</span>
</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-primary" style="width: 24%"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-1">
<span class="small fw-bold text-muted">MEMORY</span>
<span class="small fw-bold">56%</span>
</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-info" style="width: 56%"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-1">
<span class="small fw-bold text-muted">STORAGE</span>
<span class="small fw-bold">89%</span>
</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-warning" style="width: 89%"></div>
</div>
</div>
<div class="alert alert-success bg-success bg-opacity-10 border-0 d-flex align-items-center mb-0">
<i class="bi bi-check-circle-fill text-success me-2"></i>
<small class="text-success fw-bold">Alle systemer operationelle</small>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,259 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Horizontal Dark Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bs-body-bg: #0b0c10;
--bs-body-color: #c5c6c7;
--accent: #66fcf1;
--accent-dark: #45a29e;
--card-bg: #1f2833;
}
body {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.navbar {
background-color: var(--card-bg);
border-bottom: 1px solid #45a29e;
padding: 1rem 0;
}
.navbar-brand {
color: var(--accent);
font-weight: 700;
letter-spacing: 1px;
}
.nav-link {
color: #c5c6c7;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 1px;
padding: 0.5rem 1.5rem !important;
transition: color 0.2s;
}
.nav-link:hover, .nav-link.active {
color: var(--accent);
}
.card {
background-color: var(--card-bg);
border: none;
border-radius: 0;
}
.btn-accent {
background-color: transparent;
border: 1px solid var(--accent);
color: var(--accent);
border-radius: 0;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 1px;
padding: 0.5rem 1.5rem;
transition: all 0.2s;
}
.btn-accent:hover {
background-color: var(--accent);
color: #0b0c10;
}
.table {
--bs-table-bg: transparent;
--bs-table-color: #c5c6c7;
--bs-table-border-color: #45a29e;
}
.table th {
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
color: var(--accent);
border-bottom-width: 2px;
}
.table td {
vertical-align: middle;
padding: 1rem 0.5rem;
}
.filter-btn {
background: transparent;
border: 1px solid #45a29e;
color: #45a29e;
padding: 0.25rem 1rem;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-right: 0.5rem;
}
.filter-btn.active, .filter-btn:hover {
background: #45a29e;
color: #0b0c10;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg mb-5">
<div class="container">
<a class="navbar-brand" href="#">BMC<span style="color: white;">HUB</span></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="index.html">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="customers.html">Kunder</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Hardware</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Fakturering</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">System</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="d-flex justify-content-between align-items-end mb-5 border-bottom border-secondary pb-3">
<div>
<h1 class="display-4 fw-light text-white mb-0">Kunder</h1>
</div>
<div class="d-flex gap-3">
<input type="text" class="form-control bg-dark border-secondary text-white rounded-0" placeholder="SØG..." style="width: 250px;">
<button class="btn-accent">Opret Kunde</button>
</div>
</div>
<div class="mb-4">
<button class="filter-btn active">Alle</button>
<button class="filter-btn">Aktive</button>
<button class="filter-btn">Inaktive</button>
<button class="filter-btn">VIP</button>
</div>
<div class="card p-4">
<table class="table">
<thead>
<tr>
<th>Virksomhed</th>
<th>Kontakt</th>
<th>CVR</th>
<th>Status</th>
<th>Hardware</th>
<th class="text-end">Handling</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="text-white fw-bold">Advokatgruppen A/S</div>
<div class="small">København K</div>
</td>
<td>
<div class="text-white">Jens Jensen</div>
<div class="small">jens@advokat.dk</div>
</td>
<td>12345678</td>
<td><span class="text-success">AKTIV</span></td>
<td>
<span class="border border-secondary px-2 py-1 small me-1">FIREWALL</span>
<span class="border border-secondary px-2 py-1 small">SWITCH</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary rounded-0"><i class="bi bi-pencil"></i></button>
</td>
</tr>
<tr>
<td>
<div class="text-white fw-bold">Byg & Bo ApS</div>
<div class="small">Aarhus C</div>
</td>
<td>
<div class="text-white">Mette Hansen</div>
<div class="small">mh@bygbo.dk</div>
</td>
<td>87654321</td>
<td><span class="text-success">AKTIV</span></td>
<td>
<span class="border border-secondary px-2 py-1 small">ROUTER</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary rounded-0"><i class="bi bi-pencil"></i></button>
</td>
</tr>
<tr>
<td>
<div class="text-white fw-bold">Cafe Møller</div>
<div class="small">Odense M</div>
</td>
<td>
<div class="text-white">Peter Møller</div>
<div class="small">pm@cafe.dk</div>
</td>
<td>11223344</td>
<td><span class="text-warning">AFVENTER</span></td>
<td>
<span class="text-muted">-</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary rounded-0"><i class="bi bi-pencil"></i></button>
</td>
</tr>
<tr>
<td>
<div class="text-white fw-bold">Dansk Design Hus</div>
<div class="small">København Ø</div>
</td>
<td>
<div class="text-white">Lars Larsen</div>
<div class="small">ll@design.dk</div>
</td>
<td>44332211</td>
<td><span class="text-success">AKTIV</span></td>
<td>
<span class="border border-secondary px-2 py-1 small me-1">FIREWALL</span>
<span class="border border-secondary px-2 py-1 small">+4</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-secondary rounded-0"><i class="bi bi-pencil"></i></button>
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center mt-4">
<nav>
<ul class="pagination">
<li class="page-item disabled"><a class="page-link bg-transparent border-0 text-secondary" href="#">PREV</a></li>
<li class="page-item active"><a class="page-link bg-transparent border border-info text-info rounded-0 mx-1" href="#">1</a></li>
<li class="page-item"><a class="page-link bg-transparent border-0 text-secondary" href="#">2</a></li>
<li class="page-item"><a class="page-link bg-transparent border-0 text-secondary" href="#">3</a></li>
<li class="page-item"><a class="page-link bg-transparent border-0 text-secondary" href="#">NEXT</a></li>
</ul>
</nav>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,254 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Horizontal Dark</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<style>
:root {
--bs-body-bg: #0b0c10;
--bs-body-color: #c5c6c7;
--accent: #66fcf1;
--accent-dark: #45a29e;
--card-bg: #1f2833;
}
body {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.navbar {
background-color: var(--card-bg);
border-bottom: 1px solid #45a29e;
padding: 1rem 0;
}
.navbar-brand {
color: var(--accent);
font-weight: 700;
letter-spacing: 1px;
}
.nav-link {
color: #c5c6c7;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 1px;
padding: 0.5rem 1.5rem !important;
transition: color 0.2s;
}
.nav-link:hover, .nav-link.active {
color: var(--accent);
}
.card {
background-color: var(--card-bg);
border: none;
border-radius: 0;
}
.stat-value {
color: var(--accent);
font-size: 2.5rem;
font-weight: 300;
}
.stat-label {
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
color: #c5c6c7;
}
.btn-accent {
background-color: transparent;
border: 1px solid var(--accent);
color: var(--accent);
border-radius: 0;
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 1px;
padding: 0.5rem 1.5rem;
transition: all 0.2s;
}
.btn-accent:hover {
background-color: var(--accent);
color: #0b0c10;
}
.table {
--bs-table-bg: transparent;
--bs-table-color: #c5c6c7;
--bs-table-border-color: #45a29e;
}
.table th {
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
color: var(--accent);
border-bottom-width: 2px;
}
.table td {
vertical-align: middle;
padding: 1rem 0.5rem;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg mb-5">
<div class="container">
<a class="navbar-brand" href="#">BMC<span style="color: white;">HUB</span></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link active" href="index.html">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="customers.html">Kunder</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Hardware</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Fakturering</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">System</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="d-flex justify-content-between align-items-end mb-5 border-bottom border-secondary pb-3">
<div>
<h1 class="display-4 fw-light text-white mb-0">Oversigt</h1>
</div>
<div class="d-flex gap-3">
<button class="btn-accent">Ny Opgave</button>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-3">
<div class="card p-4">
<div class="stat-label mb-2">Aktive Kunder</div>
<div class="stat-value">124</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-4">
<div class="stat-label mb-2">Hardware</div>
<div class="stat-value">856</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-4">
<div class="stat-label mb-2">Support</div>
<div class="stat-value text-danger">12</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-4">
<div class="stat-label mb-2">Omsætning</div>
<div class="stat-value">450k</div>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-lg-8">
<h5 class="text-uppercase letter-spacing-1 mb-4" style="letter-spacing: 1px; color: var(--accent);">Seneste Log</h5>
<table class="table">
<thead>
<tr>
<th>Kunde</th>
<th>Handling</th>
<th>Status</th>
<th class="text-end">Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-white">Advokatgruppen A/S</td>
<td>Firewall konfiguration</td>
<td><span class="text-success">FULDFØRT</span></td>
<td class="text-end">10:23</td>
</tr>
<tr>
<td class="text-white">Byg & Bo ApS</td>
<td>Licens fornyelse</td>
<td><span class="text-warning">AFVENTER</span></td>
<td class="text-end">I går</td>
</tr>
<tr>
<td class="text-white">Cafe Møller</td>
<td>Netværksnedbrud</td>
<td><span class="text-danger">KRITISK</span></td>
<td class="text-end">I går</td>
</tr>
<tr>
<td class="text-white">Dansk Design Hus</td>
<td>Ny bruger oprettet</td>
<td><span class="text-success">FULDFØRT</span></td>
<td class="text-end">2 dage siden</td>
</tr>
</tbody>
</table>
</div>
<div class="col-lg-4">
<div class="card p-4 border border-secondary">
<h5 class="text-uppercase letter-spacing-1 mb-4" style="letter-spacing: 1px; color: var(--accent);">Server Status</h5>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="small">CPU LOAD</span>
<span class="small text-white">24%</span>
</div>
<div class="progress bg-dark" style="height: 2px;">
<div class="progress-bar" style="width: 24%; background-color: var(--accent);"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="small">MEMORY</span>
<span class="small text-white">56%</span>
</div>
<div class="progress bg-dark" style="height: 2px;">
<div class="progress-bar" style="width: 56%; background-color: var(--accent);"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-2">
<span class="small">STORAGE</span>
<span class="small text-white">89%</span>
</div>
<div class="progress bg-dark" style="height: 2px;">
<div class="progress-bar bg-warning" style="width: 89%"></div>
</div>
</div>
<div class="mt-4 pt-4 border-top border-secondary">
<small class="d-block text-muted mb-1">LAST BACKUP</small>
<span class="text-white">Today, 03:00 AM</span>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,328 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Material Blue Customers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
/* Blue Theme Tokens */
--md-sys-color-primary: #0061A4;
--md-sys-color-on-primary: #FFFFFF;
--md-sys-color-primary-container: #D1E4FF;
--md-sys-color-on-primary-container: #001D36;
--md-sys-color-surface: #FDFCFF;
--md-sys-color-surface-container: #F0F5FA;
--md-sys-color-on-surface: #1A1C1E;
--md-sys-color-outline: #73777F;
--md-sys-shape-corner-large: 16px;
--md-sys-shape-corner-medium: 12px;
--md-sys-shape-corner-full: 100px;
}
body {
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
font-family: 'Roboto', sans-serif;
padding-top: 80px;
}
.top-app-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 80px;
background: var(--md-sys-color-surface-container);
display: flex;
align-items: center;
padding: 0 24px;
z-index: 1000;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.nav-pills-material {
display: flex;
gap: 8px;
margin-left: 40px;
}
.nav-pill {
padding: 8px 24px;
border-radius: 100px;
text-decoration: none;
color: var(--md-sys-color-on-surface);
font-weight: 500;
font-size: 14px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.nav-pill:hover {
background-color: rgba(0, 97, 164, 0.08);
}
.nav-pill.active {
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
}
.card-md {
background: var(--md-sys-color-surface-container);
border-radius: var(--md-sys-shape-corner-large);
padding: 24px;
border: none;
}
.btn-filled {
background: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-radius: 100px;
padding: 10px 24px;
border: none;
font-weight: 500;
letter-spacing: 0.1px;
transition: box-shadow 0.2s;
}
.btn-filled:hover {
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.search-bar {
background: var(--md-sys-color-surface);
border-radius: 28px;
padding: 0 16px;
height: 48px;
display: flex;
align-items: center;
width: 300px;
border: 1px solid transparent;
}
.search-bar:focus-within {
border-color: var(--md-sys-color-primary);
background: var(--md-sys-color-surface-container);
}
.search-bar input {
border: none;
background: transparent;
margin-left: 12px;
width: 100%;
outline: none;
font-size: 14px;
}
.profile-section {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 8px 4px 4px;
border-radius: 100px;
background: var(--md-sys-color-primary);
color: white;
margin-left: auto;
}
.profile-img {
width: 32px;
height: 32px;
border-radius: 50%;
background: white;
border: 2px solid white;
}
.table th {
font-weight: 500;
color: var(--md-sys-color-outline);
border-bottom: 1px solid #E7E0EC;
}
.table td {
padding: 16px 8px;
vertical-align: middle;
}
.chip {
display: inline-flex;
align-items: center;
padding: 6px 16px;
border-radius: 8px;
border: 1px solid #79747E;
margin-right: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
background: transparent;
}
.chip.active {
background: var(--md-sys-color-primary-container);
border-color: transparent;
color: var(--md-sys-color-on-primary-container);
}
.chip:hover:not(.active) {
background: rgba(0,0,0,0.05);
}
</style>
</head>
<body>
<header class="top-app-bar">
<div class="d-flex align-items-center">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 40px; height: 40px; background-color: var(--md-sys-color-primary) !important;">
<i class="bi bi-grid-fill"></i>
</div>
<span class="fw-bold fs-5">BMC Hub</span>
</div>
<nav class="nav-pills-material">
<a href="index.html" class="nav-pill">
<i class="bi bi-grid"></i> Dashboard
</a>
<a href="customers.html" class="nav-pill active">
<i class="bi bi-people"></i> Kunder
</a>
<a href="#" class="nav-pill">
<i class="bi bi-hdd"></i> Hardware
</a>
<a href="#" class="nav-pill">
<i class="bi bi-gear"></i> Opsætning
</a>
</nav>
<div class="d-flex align-items-center gap-3 ms-auto">
<div class="search-bar">
<i class="bi bi-search text-muted"></i>
<input type="text" placeholder="Søg kunde...">
</div>
<div class="profile-section">
<img src="https://ui-avatars.com/api/?name=CT&background=fff&color=0061A4" class="profile-img">
<span class="small fw-medium pe-2">Christian</span>
</div>
</div>
</header>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="m-0 fw-normal">Kunder</h2>
<button class="btn-filled"><i class="bi bi-plus-lg me-2"></i>Opret Kunde</button>
</div>
<div class="mb-4">
<button class="chip active">Alle Kunder</button>
<button class="chip">Aktive</button>
<button class="chip">Inaktive</button>
<button class="chip">VIP</button>
</div>
<div class="card-md">
<table class="table table-hover">
<thead>
<tr>
<th>Virksomhed</th>
<th>Kontaktperson</th>
<th>CVR</th>
<th>Status</th>
<th>Hardware</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle text-white d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px; background: var(--md-sys-color-primary);">A</div>
<div>
<div class="fw-medium">Advokatgruppen A/S</div>
<div class="small text-muted">København K</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Jens Jensen</div>
<div class="small text-muted">jens@advokat.dk</div>
</td>
<td class="text-muted">12345678</td>
<td><span class="badge rounded-pill text-bg-success bg-opacity-25 text-success">Aktiv</span></td>
<td>
<span class="badge bg-secondary bg-opacity-10 text-secondary border">Firewall</span>
<span class="badge bg-secondary bg-opacity-10 text-secondary border">Switch</span>
</td>
<td class="text-end">
<button class="btn btn-icon"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-secondary text-white d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">B</div>
<div>
<div class="fw-medium">Byg & Bo ApS</div>
<div class="small text-muted">Aarhus C</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Mette Hansen</div>
<div class="small text-muted">mh@bygbo.dk</div>
</td>
<td class="text-muted">87654321</td>
<td><span class="badge rounded-pill text-bg-success bg-opacity-25 text-success">Aktiv</span></td>
<td>
<span class="badge bg-secondary bg-opacity-10 text-secondary border">Router</span>
</td>
<td class="text-end">
<button class="btn btn-icon"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-danger text-white d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">C</div>
<div>
<div class="fw-medium">Cafe Møller</div>
<div class="small text-muted">Odense M</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">Peter Møller</div>
<div class="small text-muted">pm@cafe.dk</div>
</td>
<td class="text-muted">11223344</td>
<td><span class="badge rounded-pill text-bg-warning bg-opacity-25 text-warning">Afventer</span></td>
<td>
<span class="text-muted">-</span>
</td>
<td class="text-end">
<button class="btn btn-icon"><i class="bi bi-three-dots-vertical"></i></button>
</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-end mt-4">
<nav>
<ul class="pagination">
<li class="page-item disabled"><a class="page-link border-0 bg-transparent" href="#">Forrige</a></li>
<li class="page-item active"><a class="page-link border-0 rounded-circle mx-1" style="background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container);" href="#">1</a></li>
<li class="page-item"><a class="page-link border-0 bg-transparent text-dark" href="#">2</a></li>
<li class="page-item"><a class="page-link border-0 bg-transparent text-dark" href="#">3</a></li>
<li class="page-item"><a class="page-link border-0 bg-transparent text-dark" href="#">Næste</a></li>
</ul>
</nav>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,331 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Material Blue</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
/* Blue Theme Tokens */
--md-sys-color-primary: #0061A4;
--md-sys-color-on-primary: #FFFFFF;
--md-sys-color-primary-container: #D1E4FF;
--md-sys-color-on-primary-container: #001D36;
--md-sys-color-surface: #FDFCFF;
--md-sys-color-surface-container: #F0F5FA;
--md-sys-color-on-surface: #1A1C1E;
--md-sys-color-outline: #73777F;
--md-sys-shape-corner-large: 16px;
--md-sys-shape-corner-medium: 12px;
--md-sys-shape-corner-full: 100px;
}
body {
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
font-family: 'Roboto', sans-serif;
padding-top: 80px;
}
.top-app-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 80px;
background: var(--md-sys-color-surface-container);
display: flex;
align-items: center;
padding: 0 24px;
z-index: 1000;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.nav-pills-material {
display: flex;
gap: 8px;
margin-left: 40px;
}
.nav-pill {
padding: 8px 24px;
border-radius: 100px;
text-decoration: none;
color: var(--md-sys-color-on-surface);
font-weight: 500;
font-size: 14px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.nav-pill:hover {
background-color: rgba(0, 97, 164, 0.08);
}
.nav-pill.active {
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-on-primary-container);
}
.card-md {
background: var(--md-sys-color-surface-container);
border-radius: var(--md-sys-shape-corner-large);
padding: 24px;
border: none;
height: 100%;
}
.btn-filled {
background: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-radius: 100px;
padding: 10px 24px;
border: none;
font-weight: 500;
letter-spacing: 0.1px;
transition: box-shadow 0.2s;
}
.btn-filled:hover {
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.search-bar {
background: var(--md-sys-color-surface);
border-radius: 28px;
padding: 0 16px;
height: 48px;
display: flex;
align-items: center;
width: 300px;
border: 1px solid transparent;
}
.search-bar:focus-within {
border-color: var(--md-sys-color-primary);
background: var(--md-sys-color-surface-container);
}
.search-bar input {
border: none;
background: transparent;
margin-left: 12px;
width: 100%;
outline: none;
font-size: 14px;
}
.profile-section {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 8px 4px 4px;
border-radius: 100px;
background: var(--md-sys-color-primary);
color: white;
margin-left: auto;
}
.profile-img {
width: 32px;
height: 32px;
border-radius: 50%;
background: white;
border: 2px solid white;
}
.table th {
font-weight: 500;
color: var(--md-sys-color-outline);
border-bottom: 1px solid #E7E0EC;
}
.table td {
padding: 16px 8px;
vertical-align: middle;
}
</style>
</head>
<body>
<header class="top-app-bar">
<div class="d-flex align-items-center">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 40px; height: 40px; background-color: var(--md-sys-color-primary) !important;">
<i class="bi bi-grid-fill"></i>
</div>
<span class="fw-bold fs-5">BMC Hub</span>
</div>
<nav class="nav-pills-material">
<a href="index.html" class="nav-pill active">
<i class="bi bi-grid"></i> Dashboard
</a>
<a href="customers.html" class="nav-pill">
<i class="bi bi-people"></i> Kunder
</a>
<a href="#" class="nav-pill">
<i class="bi bi-hdd"></i> Hardware
</a>
<a href="#" class="nav-pill">
<i class="bi bi-gear"></i> Opsætning
</a>
</nav>
<div class="d-flex align-items-center gap-3 ms-auto">
<div class="search-bar">
<i class="bi bi-search text-muted"></i>
<input type="text" placeholder="Søg...">
</div>
<div class="profile-section">
<img src="https://ui-avatars.com/api/?name=CT&background=fff&color=0061A4" class="profile-img">
<span class="small fw-medium pe-2">Christian</span>
</div>
</div>
</header>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="m-0 fw-normal">Dashboard</h2>
<button class="btn-filled"><i class="bi bi-plus-lg me-2"></i>Ny Opgave</button>
</div>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card-md">
<div class="d-flex align-items-center mb-3">
<i class="bi bi-people fs-4 me-2" style="color: var(--md-sys-color-primary)"></i>
<span class="fw-medium">Kunder</span>
</div>
<h2 class="display-5 fw-bold mb-0">124</h2>
<small class="text-muted">Aktive abonnementer</small>
</div>
</div>
<div class="col-md-3">
<div class="card-md">
<div class="d-flex align-items-center mb-3">
<i class="bi bi-hdd-network fs-4 me-2" style="color: var(--md-sys-color-primary)"></i>
<span class="fw-medium">Hardware</span>
</div>
<h2 class="display-5 fw-bold mb-0">856</h2>
<small class="text-muted">Enheder registreret</small>
</div>
</div>
<div class="col-md-3">
<div class="card-md">
<div class="d-flex align-items-center mb-3">
<i class="bi bi-exclamation-circle fs-4 me-2 text-danger"></i>
<span class="fw-medium">Support</span>
</div>
<h2 class="display-5 fw-bold mb-0">12</h2>
<small class="text-muted">Åbne sager</small>
</div>
</div>
<div class="col-md-3">
<div class="card-md">
<div class="d-flex align-items-center mb-3">
<i class="bi bi-graph-up fs-4 me-2 text-success"></i>
<span class="fw-medium">Omsætning</span>
</div>
<h2 class="display-5 fw-bold mb-0">450k</h2>
<small class="text-muted">Denne måned</small>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-8">
<div class="card-md">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="m-0">Seneste aktivitet</h5>
<button class="btn btn-sm btn-outline-secondary rounded-pill px-3">Se alle</button>
</div>
<table class="table table-hover">
<thead>
<tr>
<th>Kunde</th>
<th>Handling</th>
<th>Status</th>
<th>Tid</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle text-white d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; font-size: 14px; background: var(--md-sys-color-primary);">A</div>
<span class="fw-medium">Advokatgruppen A/S</span>
</div>
</td>
<td>Firewall konfiguration</td>
<td><span class="badge rounded-pill text-bg-success bg-opacity-25 text-success">Fuldført</span></td>
<td class="text-muted">10:23</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-secondary text-white d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; font-size: 14px;">B</div>
<span class="fw-medium">Byg & Bo ApS</span>
</div>
</td>
<td>Licens fornyelse</td>
<td><span class="badge rounded-pill text-bg-warning bg-opacity-25 text-warning">Afventer</span></td>
<td class="text-muted">I går</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle bg-danger text-white d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px; font-size: 14px;">C</div>
<span class="fw-medium">Cafe Møller</span>
</div>
</td>
<td>Netværksnedbrud</td>
<td><span class="badge rounded-pill text-bg-danger bg-opacity-25 text-danger">Kritisk</span></td>
<td class="text-muted">I går</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-4">
<div class="card-md h-100">
<h5 class="mb-4">System Status</h5>
<div class="mb-4">
<div class="d-flex justify-content-between mb-1">
<span class="text-muted small">CPU</span>
<span class="fw-bold small">24%</span>
</div>
<div class="progress" style="height: 8px; border-radius: 4px;">
<div class="progress-bar" style="width: 24%; background-color: var(--md-sys-color-primary);"></div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between mb-1">
<span class="text-muted small">RAM</span>
<span class="fw-bold small">56%</span>
</div>
<div class="progress" style="height: 8px; border-radius: 4px;">
<div class="progress-bar" style="width: 56%; background-color: var(--md-sys-color-primary);"></div>
</div>
</div>
<div class="mt-auto p-3 rounded-4" style="background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container);">
<div class="d-flex">
<i class="bi bi-info-circle-fill me-2"></i>
<small class="fw-medium">System backup kører kl. 03:00</small>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub - Design Templates</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background: #f8f9fa; padding: 40px 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }
.card { transition: transform 0.2s; height: 100%; border: none; box-shadow: 0 2px 15px rgba(0,0,0,0.05); }
.card:hover { transform: translateY(-5px); box-shadow: 0 10px 25px rgba(0,0,0,0.1); }
.preview-box { height: 160px; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 1.5rem; letter-spacing: 1px; border-radius: 6px 6px 0 0; }
.btn-group-vertical { width: 100%; }
</style>
</head>
<body>
<div class="container">
<div class="text-center mb-5">
<h1 class="fw-bold display-5 mb-3">BMC Hub Design Templates</h1>
<p class="text-muted lead">Choose a style below to preview the dashboard and customer overview pages.</p>
</div>
<div class="row g-4 justify-content-center">
<!-- 01 Nordic -->
<div class="col-md-4 col-lg-3">
<div class="card">
<div class="preview-box" style="background: #f8f9fa; color: #0f4c75; border-bottom: 4px solid #0f4c75;">
NORDIC
</div>
<div class="card-body">
<h5 class="card-title fw-bold">01. Nordic Minimalist</h5>
<p class="card-text small text-muted mb-4">Clean, airy, white & deep blue. Professional, calm, and spacious layout.</p>
<div class="d-grid gap-2">
<a href="01_nordic/index.html" class="btn btn-primary">View Dashboard</a>
<a href="01_nordic/customers.html" class="btn btn-outline-primary">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 02 Dark -->
<div class="col-md-4 col-lg-3">
<div class="card" style="background: #1a1d2d; color: white;">
<div class="preview-box" style="background: #0f111a; color: #6c5ce7; border-bottom: 1px solid rgba(255,255,255,0.1);">
DARK
</div>
<div class="card-body">
<h5 class="card-title fw-bold">02. Dark Professional</h5>
<p class="card-text small text-secondary mb-4">High contrast dark theme with purple accents. Developer-focused and modern.</p>
<div class="d-grid gap-2">
<a href="02_dark/index.html" class="btn btn-light" style="background: #6c5ce7; border: none; color: white;">View Dashboard</a>
<a href="02_dark/customers.html" class="btn btn-outline-light">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 03 Swiss -->
<div class="col-md-4 col-lg-3">
<div class="card" style="border: 2px solid black; box-shadow: 5px 5px 0px black;">
<div class="preview-box" style="background: white; color: black; border-bottom: 2px solid black;">
SWISS
</div>
<div class="card-body">
<h5 class="card-title fw-bold">03. Swiss Grid</h5>
<p class="card-text small text-muted mb-4">Bold typography, grid-based, black & white with red accent. Sharp and editorial.</p>
<div class="d-grid gap-2">
<a href="03_swiss/index.html" class="btn btn-dark rounded-0 fw-bold">View Dashboard</a>
<a href="03_swiss/customers.html" class="btn btn-outline-dark rounded-0 fw-bold">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 04 Soft -->
<div class="col-md-4 col-lg-3">
<div class="card" style="border-radius: 20px;">
<div class="preview-box" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 20px 20px 0 0;">
SOFT
</div>
<div class="card-body">
<h5 class="card-title fw-bold">04. Soft Gradient</h5>
<p class="card-text small text-muted mb-4">Rounded corners, soft shadows, and gradients. Friendly and approachable.</p>
<div class="d-grid gap-2">
<a href="04_soft/index.html" class="btn btn-primary" style="background: #667eea; border: none; border-radius: 10px;">View Dashboard</a>
<a href="04_soft/customers.html" class="btn btn-outline-primary" style="border-radius: 10px;">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 05 Compact -->
<div class="col-md-4 col-lg-3">
<div class="card border">
<div class="preview-box" style="background: #e9ecef; color: #495057; font-size: 1.2rem;">
COMPACT
</div>
<div class="card-body p-3">
<h6 class="card-title fw-bold">05. Compact Utility</h6>
<p class="card-text small text-muted mb-3">Data-dense, utility focused. Maximum information density.</p>
<div class="d-grid gap-2">
<a href="05_compact/index.html" class="btn btn-primary btn-sm">View Dashboard</a>
<a href="05_compact/customers.html" class="btn btn-outline-secondary btn-sm">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 06 Glass -->
<div class="col-md-4 col-lg-3">
<div class="card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<div class="preview-box" style="background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255,255,255,0.2);">
GLASS
</div>
<div class="card-body">
<h5 class="card-title fw-bold">06. Glassmorphism</h5>
<p class="card-text small text-white-50 mb-4">Floating sidebar, blurred backgrounds, and gradients. Modern and trendy.</p>
<div class="d-grid gap-2">
<a href="06_glass/index.html" class="btn btn-light bg-opacity-25 border-0 text-white">View Dashboard</a>
<a href="06_glass/customers.html" class="btn btn-outline-light">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 07 Material -->
<div class="col-md-4 col-lg-3">
<div class="card" style="background: #FEF7FF;">
<div class="preview-box" style="background: #EADDFF; color: #21005D;">
MATERIAL
</div>
<div class="card-body">
<h5 class="card-title fw-bold">07. Material 3</h5>
<p class="card-text small text-muted mb-4">Google's Material Design 3. Navigation Rail, pills, and pastel tones.</p>
<div class="d-grid gap-2">
<a href="07_material/index.html" class="btn btn-primary" style="background: #6750A4; border: none; border-radius: 20px;">View Dashboard</a>
<a href="07_material/customers.html" class="btn btn-outline-primary" style="border-radius: 20px;">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 08 Horizontal Clean -->
<div class="col-md-4 col-lg-3">
<div class="card">
<div class="preview-box" style="background: white; border-bottom: 4px solid #2563eb; color: #2563eb;">
TOP NAV
</div>
<div class="card-body">
<h5 class="card-title fw-bold">08. Horizontal Clean</h5>
<p class="card-text small text-muted mb-4">Classic top navigation bar. Clean, white, and blue. Standard SaaS layout.</p>
<div class="d-grid gap-2">
<a href="08_horizontal_clean/index.html" class="btn btn-primary">View Dashboard</a>
<a href="08_horizontal_clean/customers.html" class="btn btn-outline-primary">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 09 Horizontal Dark -->
<div class="col-md-4 col-lg-3">
<div class="card" style="background: #0b0c10; color: #c5c6c7; border: 1px solid #45a29e;">
<div class="preview-box" style="background: #1f2833; color: #66fcf1; border-bottom: 1px solid #45a29e;">
CYBER
</div>
<div class="card-body">
<h5 class="card-title fw-bold">09. Horizontal Dark</h5>
<p class="card-text small text-muted mb-4">Top navigation with a dark, cyberpunk/tech aesthetic. Neon accents.</p>
<div class="d-grid gap-2">
<a href="09_horizontal_dark/index.html" class="btn btn-outline-info rounded-0">View Dashboard</a>
<a href="09_horizontal_dark/customers.html" class="btn btn-outline-secondary rounded-0">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 10 Nordic Topbar -->
<div class="col-md-4 col-lg-3">
<div class="card border">
<div class="preview-box" style="background: #f8f9fa; color: #0f4c75; border-bottom: 1px solid #eee;">
NORDIC TOP
</div>
<div class="card-body">
<h5 class="card-title fw-bold">10. Nordic Topbar</h5>
<p class="card-text small text-muted mb-4">The clean Nordic style but with a top navigation bar instead of sidebar.</p>
<div class="d-grid gap-2">
<a href="10_nordic_top/index.html" class="btn btn-primary" style="background-color: #0f4c75; border-color: #0f4c75;">View Dashboard</a>
<a href="10_nordic_top/customers.html" class="btn btn-outline-secondary">View Customers</a>
</div>
</div>
</div>
</div>
<!-- 11 Material Blue Top -->
<div class="col-md-4 col-lg-3">
<div class="card" style="background: #FDFCFF;">
<div class="preview-box" style="background: #D1E4FF; color: #001D36;">
MAT BLUE
</div>
<div class="card-body">
<h5 class="card-title fw-bold">11. Material Blue</h5>
<p class="card-text small text-muted mb-4">Material 3 with top navigation, blue theme, and custom profile pill.</p>
<div class="d-grid gap-2">
<a href="11_material_top_blue/index.html" class="btn btn-primary" style="background: #0061A4; border: none; border-radius: 20px;">View Dashboard</a>
<a href="11_material_top_blue/customers.html" class="btn btn-outline-primary" style="border-radius: 20px;">View Customers</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,70 +0,0 @@
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-primary">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">🚀 BMC Hub</span>
</div>
</nav>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>Velkommen til BMC Hub</h1>
<p class="lead">Central management system for BMC Networks</p>
</div>
</div>
<div class="row mt-4">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">👥 Customers</h5>
<p class="card-text">Manage customer database</p>
<a href="/api/v1/customers" class="btn btn-primary">View API</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">🖥️ Hardware</h5>
<p class="card-text">Track customer hardware</p>
<a href="/api/v1/hardware" class="btn btn-primary">View API</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">📊 System</h5>
<p class="card-text">Health and configuration</p>
<a href="/api/v1/system/health" class="btn btn-primary">Health Check</a>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">📖 API Documentation</h5>
<p class="card-text">Explore the complete API documentation</p>
<a href="/api/docs" class="btn btn-success">OpenAPI Docs</a>
<a href="/api/redoc" class="btn btn-secondary">ReDoc</a>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>