Add Material Blue design templates for dashboard and customer overview pages
This commit is contained in:
parent
731a541f00
commit
050e886f22
21
.github/copilot-instructions.md
vendored
21
.github/copilot-instructions.md
vendored
@ -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
|
||||||
|
|||||||
86
app/auth/backend/router.py
Normal file
86
app/auth/backend/router.py
Normal 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
20
app/auth/backend/views.py
Normal 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}
|
||||||
|
)
|
||||||
199
app/auth/frontend/login.html
Normal file
199
app/auth/frontend/login.html
Normal 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 © 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 %}
|
||||||
308
app/contacts/backend/router.py
Normal file
308
app/contacts/backend/router.py
Normal 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))
|
||||||
28
app/contacts/backend/views.py
Normal file
28
app/contacts/backend/views.py
Normal 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
|
||||||
|
})
|
||||||
516
app/contacts/frontend/contact_detail.html
Normal file
516
app/contacts/frontend/contact_detail.html
Normal 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 %}
|
||||||
483
app/contacts/frontend/contacts.html
Normal file
483
app/contacts/frontend/contacts.html
Normal 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 %}
|
||||||
220
app/core/auth_dependencies.py
Normal file
220
app/core/auth_dependencies.py
Normal 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
313
app/core/auth_service.py
Normal 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}")
|
||||||
@ -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)
|
||||||
|
|||||||
357
app/customers/backend/router.py
Normal file
357
app/customers/backend/router.py
Normal 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
|
||||||
24
app/customers/backend/views.py
Normal file
24
app/customers/backend/views.py
Normal 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
|
||||||
|
})
|
||||||
494
app/customers/frontend/customer_detail.html
Normal file
494
app/customers/frontend/customer_detail.html
Normal 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 %}
|
||||||
502
app/customers/frontend/customers.html
Normal file
502
app/customers/frontend/customers.html
Normal 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 %}
|
||||||
13
app/dashboard/backend/views.py
Normal file
13
app/dashboard/backend/views.py
Normal 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})
|
||||||
131
app/dashboard/frontend/index.html
Normal file
131
app/dashboard/frontend/index.html
Normal 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 %}
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
"""Routers package"""
|
|
||||||
@ -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
136
app/services/cvr_service.py
Normal 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()
|
||||||
603
app/shared/frontend/base.html
Normal file
603
app/shared/frontend/base.html
Normal 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>
|
||||||
@ -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:
|
||||||
|
|||||||
562
docs/design_reference/components.html
Normal file
562
docs/design_reference/components.html
Normal 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>
|
||||||
321
docs/design_reference/customers.html
Normal file
321
docs/design_reference/customers.html
Normal 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>
|
||||||
276
docs/design_reference/form.html
Normal file
276
docs/design_reference/form.html
Normal 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>
|
||||||
329
docs/design_reference/index.html
Normal file
329
docs/design_reference/index.html
Normal 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>
|
||||||
119
docs/design_reference/login.html
Normal file
119
docs/design_reference/login.html
Normal 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>
|
||||||
262
docs/design_reference/settings.html
Normal file
262
docs/design_reference/settings.html
Normal 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
139
docs/designplan.md
Normal 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
39
main.py
@ -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"""
|
||||||
|
|||||||
215
migrations/002_auth_system.sql
Normal file
215
migrations/002_auth_system.sql
Normal 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();
|
||||||
71
migrations/003_extend_customers.sql
Normal file
71
migrations/003_extend_customers.sql
Normal 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';
|
||||||
78
migrations/004_contacts_relationships.sql
Normal file
78
migrations/004_contacts_relationships.sql
Normal 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';
|
||||||
60
migrations/005_vendors.sql
Normal file
60
migrations/005_vendors.sql
Normal 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';
|
||||||
@ -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
|
||||||
|
|||||||
245
scripts/import_from_omnisync.py
Normal file
245
scripts/import_from_omnisync.py
Normal 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}")
|
||||||
232
scripts/import_sample_data.py
Normal file
232
scripts/import_sample_data.py
Normal 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()
|
||||||
282
static/design_templates/01_nordic/customers.html
Normal file
282
static/design_templates/01_nordic/customers.html
Normal 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>
|
||||||
292
static/design_templates/01_nordic/index.html
Normal file
292
static/design_templates/01_nordic/index.html
Normal 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>
|
||||||
298
static/design_templates/02_dark/customers.html
Normal file
298
static/design_templates/02_dark/customers.html
Normal 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>
|
||||||
267
static/design_templates/02_dark/index.html
Normal file
267
static/design_templates/02_dark/index.html
Normal 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>
|
||||||
285
static/design_templates/03_swiss/customers.html
Normal file
285
static/design_templates/03_swiss/customers.html
Normal 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>
|
||||||
278
static/design_templates/03_swiss/index.html
Normal file
278
static/design_templates/03_swiss/index.html
Normal 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>
|
||||||
306
static/design_templates/04_soft/customers.html
Normal file
306
static/design_templates/04_soft/customers.html
Normal 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>
|
||||||
321
static/design_templates/04_soft/index.html
Normal file
321
static/design_templates/04_soft/index.html
Normal 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>
|
||||||
226
static/design_templates/05_compact/customers.html
Normal file
226
static/design_templates/05_compact/customers.html
Normal 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>
|
||||||
269
static/design_templates/05_compact/index.html
Normal file
269
static/design_templates/05_compact/index.html
Normal 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>
|
||||||
292
static/design_templates/06_glass/customers.html
Normal file
292
static/design_templates/06_glass/customers.html
Normal 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>
|
||||||
295
static/design_templates/06_glass/index.html
Normal file
295
static/design_templates/06_glass/index.html
Normal 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>
|
||||||
346
static/design_templates/07_material/customers.html
Normal file
346
static/design_templates/07_material/customers.html
Normal 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>
|
||||||
350
static/design_templates/07_material/index.html
Normal file
350
static/design_templates/07_material/index.html
Normal 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>
|
||||||
225
static/design_templates/08_horizontal_clean/customers.html
Normal file
225
static/design_templates/08_horizontal_clean/customers.html
Normal 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>
|
||||||
286
static/design_templates/08_horizontal_clean/index.html
Normal file
286
static/design_templates/08_horizontal_clean/index.html
Normal 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>
|
||||||
259
static/design_templates/09_horizontal_dark/customers.html
Normal file
259
static/design_templates/09_horizontal_dark/customers.html
Normal 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>
|
||||||
254
static/design_templates/09_horizontal_dark/index.html
Normal file
254
static/design_templates/09_horizontal_dark/index.html
Normal 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>
|
||||||
328
static/design_templates/11_material_top_blue/customers.html
Normal file
328
static/design_templates/11_material_top_blue/customers.html
Normal 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>
|
||||||
331
static/design_templates/11_material_top_blue/index.html
Normal file
331
static/design_templates/11_material_top_blue/index.html
Normal 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>
|
||||||
213
static/design_templates/index.html
Normal file
213
static/design_templates/index.html
Normal 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>
|
||||||
@ -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>
|
|
||||||
Loading…
Reference in New Issue
Block a user