48ea61cdcc
- Product documentation in docs/products/ - Updated MEMORY.md with product info - quiXzoom Auth Core as AAMOS Identity product
333 lines
12 KiB
HTML
333 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="sv">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<meta name="theme-color" content="#0A9396">
|
|
<title>LandveX — Min Sida</title>
|
|
<style>
|
|
:root {
|
|
--primary: #0A9396;
|
|
--primary-light: #14b8a6;
|
|
--danger: #E63946;
|
|
--warning: #F4A261;
|
|
--success: #2A9D8F;
|
|
--bg: #f8f9fa;
|
|
--card: #ffffff;
|
|
--border: #e9ecef;
|
|
--text: #212529;
|
|
--text-muted: #6c757d;
|
|
--radius: 12px;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); line-height: 1.5; }
|
|
|
|
.header { background: var(--card); border-bottom: 1px solid var(--border); padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 100; }
|
|
.header h1 { font-size: 1.25rem; font-weight: 600; color: var(--primary); }
|
|
.header .user { display: flex; align-items: center; gap: 0.5rem; }
|
|
|
|
.nav { background: var(--card); border-bottom: 1px solid var(--border); padding: 0 2rem; display: flex; gap: 2rem; }
|
|
.nav a { color: var(--text-muted); text-decoration: none; padding: 1rem 0; border-bottom: 2px solid transparent; transition: all 0.2s; }
|
|
.nav a:hover, .nav a.active { color: var(--primary); border-bottom-color: var(--primary); }
|
|
|
|
.main { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
|
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.5rem; }
|
|
.card h3 { font-size: 0.875rem; color: var(--text-muted); margin-bottom: 0.5rem; }
|
|
.card .value { font-size: 2rem; font-weight: 700; color: var(--primary); }
|
|
.card .value.danger { color: var(--danger); }
|
|
.card .value.warning { color: var(--warning); }
|
|
|
|
.section-title { font-size: 1.5rem; margin-bottom: 1rem; }
|
|
|
|
.invoice-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
.invoice-item { display: flex; justify-content: space-between; align-items: center; padding: 1rem; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); }
|
|
.invoice-item:hover { border-color: var(--primary); }
|
|
.invoice-info h4 { font-size: 1rem; margin-bottom: 0.25rem; }
|
|
.invoice-info p { font-size: 0.875rem; color: var(--text-muted); }
|
|
.invoice-amount { text-align: right; }
|
|
.invoice-amount .amount { font-size: 1.25rem; font-weight: 600; }
|
|
.invoice-amount .status { font-size: 0.75rem; padding: 0.25rem 0.75rem; border-radius: 9999px; }
|
|
.status-paid { background: rgba(42, 157, 143, 0.1); color: var(--success); }
|
|
.status-unpaid { background: rgba(244, 162, 97, 0.1); color: var(--warning); }
|
|
.status-overdue { background: rgba(230, 57, 70, 0.1); color: var(--danger); }
|
|
|
|
.btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1.2rem; border-radius: 8px; border: none; font-size: 0.875rem; font-weight: 500; cursor: pointer; text-decoration: none; }
|
|
.btn-primary { background: var(--primary); color: white; }
|
|
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
|
|
|
.agreement-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.5rem; margin-bottom: 1rem; }
|
|
.agreement-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem; }
|
|
.agreement-header h3 { font-size: 1.125rem; }
|
|
.agreement-details { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
|
.agreement-detail h4 { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.25rem; }
|
|
.agreement-detail p { font-size: 0.875rem; font-weight: 500; }
|
|
|
|
.hidden { display: none !important; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="header">
|
|
<div>
|
|
<h1>🔁 LandveX</h1>
|
|
</div>
|
|
<div class="user">
|
|
<span id="customer-name">Laddar...</span>
|
|
<button class="btn btn-outline" onclick="logout()">Logga ut</button>
|
|
</div>
|
|
</header>
|
|
|
|
<nav class="nav">
|
|
<a href="#overview" class="active" onclick="showSection('overview')">Översikt</a>
|
|
<a href="#agreements" onclick="showSection('agreements')">Mina avtal</a>
|
|
<a href="#invoices" onclick="showSection('invoices')">Fakturor</a>
|
|
<a href="#settings" onclick="showSection('settings')">Inställningar</a>
|
|
</nav>
|
|
|
|
<main class="main">
|
|
|
|
<!-- Översikt -->
|
|
<div id="overview" class="page-section">
|
|
<h2 class="section-title">Översikt</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h3>Aktiva avtal</h3>
|
|
<div class="value" id="stat-agreements">-</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Obetalda fakturor</h3>
|
|
<div class="value warning" id="stat-unpaid">-</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Att betala</h3>
|
|
<div class="value danger" id="stat-due">-</div>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Senaste inbetalning</h3>
|
|
<div class="value success" id="stat-last-payment">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="section-title">Senaste fakturor</h2>
|
|
<div id="recent-invoices" class="invoice-list">
|
|
<p>Laddar...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Avtal -->
|
|
<div id="agreements" class="page-section hidden">
|
|
<h2 class="section-title">Mina avtal</h2>
|
|
<div id="agreements-list">
|
|
<p>Laddar...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fakturor -->
|
|
<div id="invoices" class="page-section hidden">
|
|
<h2 class="section-title">Fakturor</h2>
|
|
<div id="all-invoices" class="invoice-list">
|
|
<p>Laddar...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Inställningar -->
|
|
<div id="settings" class="page-section hidden">
|
|
<h2 class="section-title">Inställningar</h2>
|
|
<div class="card">
|
|
<h3>Företagsinformation</h3>
|
|
<div style="margin-top: 1rem;">
|
|
<p><strong>Namn:</strong> <span id="setting-name">-</span></p>
|
|
<p><strong>Org.nr:</strong> <span id="setting-org">-</span></p>
|
|
<p><strong>E-post:</strong> <span id="setting-email">-</span></p>
|
|
<p><strong>Telefon:</strong> <span id="setting-phone">-</span></p>
|
|
<p><strong>Adress:</strong> <span id="setting-address">-</span></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-top: 1rem;">
|
|
<h3>Betalningsinställningar</h3>
|
|
<div style="margin-top: 1rem;">
|
|
<p><strong>Autogiro:</strong> <span id="setting-autogiro">Ej aktiverat</span></p>
|
|
<p><strong>Fakturamottagare:</strong> <span id="setting-recipient">E-post</span></p>
|
|
</div>
|
|
<button class="btn btn-primary" style="margin-top: 1rem;">Ändra inställningar</button>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<script>
|
|
const API_BASE = '/api';
|
|
let customerId = null;
|
|
|
|
// Check auth
|
|
(function() {
|
|
const token = localStorage.getItem('lv_customer_token');
|
|
if (!token) {
|
|
window.location.href = '/login';
|
|
return;
|
|
}
|
|
// Decode token to get customer ID
|
|
try {
|
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
customerId = payload.customer_id;
|
|
document.getElementById('customer-name').textContent = payload.name || 'Kund';
|
|
} catch (e) {
|
|
logout();
|
|
}
|
|
})();
|
|
|
|
function showSection(section) {
|
|
document.querySelectorAll('.page-section').forEach(s => s.classList.add('hidden'));
|
|
document.getElementById(section).classList.remove('hidden');
|
|
document.querySelectorAll('.nav a').forEach(a => a.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
|
|
if (section === 'agreements') loadAgreements();
|
|
if (section === 'invoices') loadAllInvoices();
|
|
if (section === 'settings') loadSettings();
|
|
}
|
|
|
|
async function loadOverview() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/customer/${customerId}/dashboard`);
|
|
const data = await res.json();
|
|
|
|
document.getElementById('stat-agreements').textContent = data.active_agreements || 0;
|
|
document.getElementById('stat-unpaid').textContent = data.unpaid_invoices || 0;
|
|
document.getElementById('stat-due').textContent = formatCurrency(data.amount_due || 0);
|
|
document.getElementById('stat-last-payment').textContent = data.last_payment
|
|
? formatCurrency(data.last_payment.amount)
|
|
: '-';
|
|
|
|
// Recent invoices
|
|
const invoicesDiv = document.getElementById('recent-invoices');
|
|
invoicesDiv.innerHTML = '';
|
|
data.recent_invoices?.forEach(inv => {
|
|
invoicesDiv.innerHTML += renderInvoiceItem(inv);
|
|
});
|
|
} catch (e) {
|
|
console.error('Load overview error:', e);
|
|
}
|
|
}
|
|
|
|
async function loadAgreements() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/customer/${customerId}/agreements`);
|
|
const data = await res.json();
|
|
|
|
const div = document.getElementById('agreements-list');
|
|
div.innerHTML = '';
|
|
|
|
data.agreements?.forEach(agreement => {
|
|
div.innerHTML += `
|
|
<div class="agreement-card">
|
|
<div class="agreement-header">
|
|
<div>
|
|
<h3>${agreement.title}</h3>
|
|
<p style="color: var(--text-muted); font-size: 0.875rem;">${agreement.agreement_number}</p>
|
|
</div>
|
|
<span class="status-${agreement.status}">${agreement.status}</span>
|
|
</div>
|
|
<div class="agreement-details">
|
|
<div class="agreement-detail">
|
|
<h4>Period</h4>
|
|
<p>${agreement.start_date} - ${agreement.end_date || 'Löpande'}</p>
|
|
</div>
|
|
<div class="agreement-detail">
|
|
<h4>Fakturering</h4>
|
|
<p>${agreement.billing_frequency}</p>
|
|
</div>
|
|
<div class="agreement-detail">
|
|
<h4>Belopp</h4>
|
|
<p>${formatCurrency(agreement.amount)}/${agreement.billing_frequency === 'monthly' ? 'mån' : 'år'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
} catch (e) {
|
|
console.error('Load agreements error:', e);
|
|
}
|
|
}
|
|
|
|
async function loadAllInvoices() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/customer/${customerId}/invoices`);
|
|
const data = await res.json();
|
|
|
|
const div = document.getElementById('all-invoices');
|
|
div.innerHTML = '';
|
|
|
|
data.invoices?.forEach(inv => {
|
|
div.innerHTML += renderInvoiceItem(inv);
|
|
});
|
|
} catch (e) {
|
|
console.error('Load invoices error:', e);
|
|
}
|
|
}
|
|
|
|
function renderInvoiceItem(inv) {
|
|
const statusClass = inv.status === 'paid' ? 'status-paid' :
|
|
inv.status === 'overdue' ? 'status-overdue' : 'status-unpaid';
|
|
const statusText = inv.status === 'paid' ? 'Betald' :
|
|
inv.status === 'overdue' ? 'Förfallen' : 'Obetald';
|
|
|
|
return `
|
|
<div class="invoice-item">
|
|
<div class="invoice-info">
|
|
<h4>Faktura ${inv.invoice_number}</h4>
|
|
<p>Förfallo: ${inv.due_date}</p>
|
|
</div>
|
|
<div class="invoice-amount">
|
|
<div class="amount">${formatCurrency(inv.total)}</div>
|
|
<span class="status ${statusClass}">${statusText}</span>
|
|
</div>
|
|
<div>
|
|
<a href="/invoices/${inv.id}/pdf" target="_blank" class="btn btn-outline">PDF</a>
|
|
${inv.status !== 'paid' ? `<button class="btn btn-primary" onclick="payInvoice('${inv.id}')">Betala</button>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function loadSettings() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/customer/${customerId}`);
|
|
const data = await res.json();
|
|
|
|
document.getElementById('setting-name').textContent = data.customer?.name || '-';
|
|
document.getElementById('setting-org').textContent = data.customer?.org_nr || '-';
|
|
document.getElementById('setting-email').textContent = data.customer?.email || '-';
|
|
document.getElementById('setting-phone').textContent = data.customer?.phone || '-';
|
|
document.getElementById('setting-address').textContent =
|
|
`${data.customer?.address || ''}, ${data.customer?.postal_code || ''} ${data.customer?.city || ''}`;
|
|
} catch (e) {
|
|
console.error('Load settings error:', e);
|
|
}
|
|
}
|
|
|
|
function payInvoice(invoiceId) {
|
|
// Redirect to payment page or open payment modal
|
|
window.open(`/pay/${invoiceId}`, '_blank');
|
|
}
|
|
|
|
function formatCurrency(amount) {
|
|
if (amount === undefined || amount === null) return '-';
|
|
return new Intl.NumberFormat('sv-SE', { minimumFractionDigits: 2 }).format(amount) + ' kr';
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('lv_customer_token');
|
|
window.location.href = '/login';
|
|
}
|
|
|
|
// Load initial data
|
|
loadOverview();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|