Files
boc/landvex-admin-v2/ui/admin-master.html
T
Bernt 48ea61cdcc docs: add quixzoom-auth-core product to AAMOS
- Product documentation in docs/products/
- Updated MEMORY.md with product info
- quiXzoom Auth Core as AAMOS Identity product
2026-07-14 09:58:53 +00:00

4027 lines
161 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="#0f1117">
<title>AAMOS Admin — MASTER SYSTEM</title>
<style>
:root {
--primary: #0A9396;
--primary-light: #14b8a6;
--danger: #E63946;
--warning: #F4A261;
--success: #2A9D8F;
--info: #3b82f6;
--bg: #0f1117;
--card: #1a1d27;
--card-hover: #222633;
--border: #2a2d3d;
--text: #e0e0e0;
--text-muted: #888;
--text-dark: #555;
--radius: 12px;
--radius-sm: 8px;
}
* { 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; -webkit-font-smoothing: antialiased; }
/* Header */
.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; }
.header .status { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; }
.header .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
/* Navigation */
.nav { background: var(--card); border-right: 1px solid var(--border); width: 280px; position: fixed; height: 100vh; padding: 1rem; overflow-y: auto; }
.nav h2 { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text-muted); margin: 1.5rem 0 0.5rem; padding-left: 0.75rem; }
.nav h2:first-child { margin-top: 0; }
.nav a { display: flex; align-items: center; gap: 0.6rem; color: var(--text-muted); text-decoration: none; padding: 0.6rem 0.75rem; border-radius: var(--radius-sm); margin-bottom: 2px; transition: all 0.2s; font-size: 0.875rem; cursor: pointer; }
.nav a:hover, .nav a.active { color: var(--text); background: rgba(255,255,255,0.05); }
.nav a .icon { font-size: 1rem; width: 20px; text-align: center; }
/* Main Content */
.main { margin-left: 280px; padding: 2rem; min-height: 100vh; }
/* Alerts */
.alert { background: rgba(230, 57, 70, 0.1); border: 1px solid rgba(230, 57, 70, 0.3); border-left: 4px solid var(--danger); padding: 1rem 1.5rem; border-radius: var(--radius-sm); margin-bottom: 1.5rem; }
.alert.warning { background: rgba(244, 162, 97, 0.1); border-color: rgba(244, 162, 97, 0.3); border-left-color: var(--warning); }
.alert.success { background: rgba(42, 157, 143, 0.1); border-color: rgba(42, 157, 143, 0.3); border-left-color: var(--success); }
.alert.info { background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); border-left-color: var(--info); }
/* Grid & Cards */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.5rem; transition: all 0.2s; }
.card:hover { background: var(--card-hover); }
.card h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; 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); }
.card .value.success { color: var(--success); }
.card .subtitle { font-size: 0.875rem; color: var(--text-muted); margin-top: 0.25rem; }
/* Tables */
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
th, td { text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--border); }
th { font-weight: 600; color: var(--text-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; }
tr:hover { background: rgba(255,255,255,0.02); }
/* Badges */
.badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
.badge-success { background: rgba(42, 157, 143, 0.2); color: var(--success); }
.badge-warning { background: rgba(244, 162, 97, 0.2); color: var(--warning); }
.badge-danger { background: rgba(230, 57, 70, 0.2); color: var(--danger); }
.badge-blue { background: rgba(59, 130, 246, 0.2); color: var(--info); }
.badge-gray { background: rgba(136, 136, 136, 0.2); color: var(--text-muted); }
/* Buttons */
.btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1.2rem; border-radius: var(--radius-sm); border: none; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-light); }
.btn-danger { background: var(--danger); color: white; }
.btn-warning { background: var(--warning); color: #1a1d27; }
.btn-success { background: var(--success); color: white; }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-outline:hover { background: rgba(255,255,255,0.05); }
.btn-sm { padding: 0.4rem 0.8rem; font-size: 0.8rem; }
/* Forms */
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.4rem; }
.form-group input, .form-group select, .form-group textarea {
width: 100%; padding: 0.6rem 0.8rem; background: var(--bg); border: 1px solid var(--border);
border-radius: var(--radius-sm); color: var(--text); font-size: 0.9rem; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
outline: none; border-color: var(--primary); }
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
/* Modal */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 1000; display: flex; align-items: center; justify-content: center; }
.modal { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); width: 90%; max-width: 700px; max-height: 90vh; overflow-y: auto; }
.modal-header { padding: 1.5rem; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
.modal-body { padding: 1.5rem; }
.modal-footer { padding: 1rem 1.5rem; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 0.5rem; }
/* Tabs */
.tabs { display: flex; gap: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 1.5rem; }
.tab { padding: 0.75rem 1rem; color: var(--text-muted); cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s; }
.tab:hover { color: var(--text); }
.tab.active { color: var(--primary); border-bottom-color: var(--primary); }
/* Utilities */
.hidden { display: none !important; }
.loading { color: var(--text-muted); font-style: italic; }
.error { color: var(--danger); }
.success { color: var(--success); }
.text-right { text-align: right; }
.flex { display: flex; }
.flex-between { display: flex; justify-content: space-between; align-items: center; }
.gap-1 { gap: 0.5rem; }
.gap-2 { gap: 1rem; }
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; }
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 1rem; }
/* Section Title */
.section-title { font-size: 1.25rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
.section-actions { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
/* Mail */
.mail-unread { background: rgba(59, 130, 246, 0.05); border-left: 3px solid var(--info); }
/* Animations */
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.page-section { animation: fadeIn 0.3s ease; }
/* Responsive */
@media (max-width: 768px) {
.nav { width: 100%; position: relative; height: auto; }
.main { margin-left: 0; }
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header class="header">
<div>
<h1>🔁 AAMOS Admin — MASTER SYSTEM</h1>
</div>
<div class="status">
<span class="status-dot"></span>
<span id="system-status">LIVE DATA</span>
<span style="color: var(--text-muted); margin-left: 1rem;">|</span>
<span id="user-info" style="margin-left: 1rem; color: var(--text-muted);">-</span>
</div>
</header>
<nav class="nav">
<h2>Översikt</h2>
<a onclick="showSection('dashboard')" class="active" id="nav-dashboard">
<span class="icon">📊</span> Dashboard
</a>
<h2>quiXzoom</h2>
<a onclick="showSection('missions')" id="nav-missions">
<span class="icon">🎯</span> Missions
</a>
<a onclick="showSection('contributors')" id="nav-contributors">
<span class="icon">👥</span> Contributors
</a>
<h2>CRM & Försäljning</h2>
<a onclick="showSection('customers')" id="nav-customers">
<span class="icon">🏢</span> Kunder
</a>
<a onclick="showSection('leads')" id="nav-leads">
<span class="icon">🎣</span> Leads
</a>
<a onclick="showSection('agreements')" id="nav-agreements">
<span class="icon">📋</span> Avtal
</a>
<a onclick="showSection('invoices')" id="nav-invoices">
<span class="icon">📄</span> Fakturor
</a>
<a onclick="showSection('payments')" id="nav-payments">
<span class="icon">💳</span> Inbetalningar
</a>
<h2>Ekonomi</h2>
<a onclick="showSection('ledger')" id="nav-ledger">
<span class="icon">📒</span> Bokföring
</a>
<a onclick="showSection('accounts')" id="nav-accounts">
<span class="icon">📑</span> Konton
</a>
<a onclick="showSection('reconciliation')" id="nav-reconciliation">
<span class="icon">🔍</span> Avstämning
</a>
<a onclick="showSection('reports')" id="nav-reports">
<span class="icon">📈</span> Rapporter
</a>
<h2>Automation</h2>
<a onclick="showSection('automations')" id="nav-automations">
<span class="icon"></span> Automationer
</a>
<a onclick="showSection('schedules')" id="nav-schedules">
<span class="icon"></span> Scheman
</a>
<h2>Kommunikation</h2>
<a onclick="showSection('mail')" id="nav-mail">
<span class="icon">📧</span> Mail
</a>
<a onclick="showSection('chat')" id="nav-chat">
<span class="icon">💬</span> Intern chatt
<span id="chat-badge" class="badge badge-danger" style="margin-left: auto; display: none;">0</span>
</a>
<h2>Support</h2>
<a onclick="showSection('support')" id="nav-support">
<span class="icon">🎫</span> Ärenden
</a>
<a onclick="showSection('faq')" id="nav-faq">
<span class="icon"></span> FAQ
</a>
<h2>HR</h2>
<a onclick="showSection('employees')" id="nav-employees">
<span class="icon">👔</span> Personal
</a>
<a onclick="showSection('contracts')" id="nav-contracts">
<span class="icon">📄</span> Avtal
</a>
<a onclick="showSection('salary')" id="nav-salary">
<span class="icon">💰</span> Löner
</a>
<a onclick="showSection('documents')" id="nav-documents">
<span class="icon">📁</span> Dokument
</a>
<h2>Notiser & Rapporter</h2>
<a onclick="showSection('notifications')" id="nav-notifications">
<span class="icon">🔔</span> Notiscenter
<span id="notif-badge" class="badge badge-danger" style="margin-left: auto; display: none;">0</span>
</a>
<a onclick="showSection('reports')" id="nav-reports">
<span class="icon">📊</span> Rapportcenter
</a>
<h2>System</h2>
<a onclick="showSection('health')" id="nav-health">
<span class="icon">🩺</span> Hälsa
</a>
<a onclick="showSection('settings')" id="nav-settings">
<span class="icon">⚙️</span> Inställningar
</a>
</nav>
<main class="main">
<!-- ═══════════════════════════════════════════════════════════════
DASHBOARD
═══════════════════════════════════════════════════════════════ -->
<div id="dashboard" class="page-section">
<div id="dashboard-alerts"></div>
<h2 class="section-title">📊 Översikt — LIVE</h2>
<div class="grid">
<div class="card">
<h3>Omsättning (år)</h3>
<div class="value" id="dash-revenue">-</div>
<div class="subtitle">exkl. moms</div>
</div>
<div class="card">
<h3>Fakturor att skicka</h3>
<div class="value warning" id="dash-invoices-draft">-</div>
<div class="subtitle">utkast</div>
</div>
<div class="card">
<h3>Obetalda fakturor</h3>
<div class="value danger" id="dash-invoices-overdue">-</div>
<div class="subtitle">förfallna</div>
</div>
<div class="card">
<h3>Aktiva kunder</h3>
<div class="value success" id="dash-active-customers">-</div>
<div class="subtitle">totalt</div>
</div>
</div>
<h2 class="section-title">quiXzoom — LIVE</h2>
<div class="grid">
<div class="card">
<h3>Totalt Missions</h3>
<div class="value" id="stat-missions-total">-</div>
<div class="subtitle">i databasen</div>
</div>
<div class="card">
<h3>Aktiva Missions</h3>
<div class="value" id="stat-missions-active">-</div>
<div class="subtitle">redo att tas</div>
</div>
<div class="card">
<h3>Submissions</h3>
<div class="value" id="stat-submissions">-</div>
<div class="subtitle">inskickade</div>
</div>
<div class="card">
<h3>Contributors</h3>
<div class="value" id="stat-contributors">-</div>
<div class="subtitle">unika användare</div>
</div>
</div>
<h2 class="section-title">Ekonomi — LIVE</h2>
<div class="grid">
<div class="card">
<h3>Bokföringskonton</h3>
<div class="value" id="stat-accounts">-</div>
<div class="subtitle">i BAS-kontoplan</div>
</div>
<div class="card">
<h3>Kassa (SEK)</h3>
<div class="value" id="stat-cash">-</div>
<div class="subtitle">företagskonto</div>
</div>
<div class="card">
<h3>Trial Balance</h3>
<div class="value" id="stat-balanced">-</div>
<div class="subtitle" id="stat-balanced-sub">-</div>
</div>
</div>
<h2 class="section-title">📧 Mail — LIVE</h2>
<div class="grid">
<div class="card">
<h3>Inkorg</h3>
<div class="value" id="stat-mail-inbox">-</div>
<div class="subtitle">totalt</div>
</div>
<div class="card">
<h3>Olästa</h3>
<div class="value" id="stat-mail-unread">-</div>
<div class="subtitle">kräver åtgärd</div>
</div>
<div class="card">
<h3>Skickat</h3>
<div class="value" id="stat-mail-sent">-</div>
<div class="subtitle">utgående</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
CUSTOMERS (CRM)
═══════════════════════════════════════════════════════════════ -->
<div id="customers" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">🏢 Kunder — FULLT CRM</h2>
<button class="btn btn-primary" onclick="showModal('customer-modal')">
<span>+</span> Ny kund
</button>
</div>
<div class="section-actions">
<input type="text" id="customer-search" placeholder="Sök kund..." style="width: 300px;" onkeyup="searchCustomers()">
<select id="customer-filter" onchange="loadCustomers()">
<option value="">Alla statusar</option>
<option value="active">Aktiva</option>
<option value="inactive">Inaktiva</option>
<option value="prospect">Prospekt</option>
</select>
</div>
<div class="card">
<div id="customers-table"><p class="loading">Laddar kunder...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
LEADS
═══════════════════════════════════════════════════════════════ -->
<div id="leads" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">🎣 Leads</h2>
<button class="btn btn-primary" onclick="showModal('lead-modal')">
<span>+</span> Nytt lead
</button>
</div>
<div class="card">
<div id="leads-table"><p class="loading">Laddar leads...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
AGREEMENTS
═══════════════════════════════════════════════════════════════ -->
<div id="agreements" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">📋 Avtal</h2>
<button class="btn btn-primary" onclick="showModal('agreement-modal')">
<span>+</span> Nytt avtal
</button>
</div>
<div class="card">
<div id="agreements-table"><p class="loading">Laddar avtal...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
INVOICES
═══════════════════════════════════════════════════════════════ -->
<div id="invoices" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">📄 Fakturor</h2>
<button class="btn btn-primary" onclick="showModal('invoice-modal')">
<span>+</span> Ny faktura
</button>
</div>
<div class="tabs">
<div class="tab active" onclick="showInvoiceTab('all')">Alla</div>
<div class="tab" onclick="showInvoiceTab('draft')">Utkast</div>
<div class="tab" onclick="showInvoiceTab('sent')">Skickade</div>
<div class="tab" onclick="showInvoiceTab('paid')">Betalda</div>
<div class="tab" onclick="showInvoiceTab('overdue')">Förfallna</div>
</div>
<div class="card">
<div id="invoices-table"><p class="loading">Laddar fakturor...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
PAYMENTS
═══════════════════════════════════════════════════════════════ -->
<div id="payments" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">💳 Inbetalningar</h2>
<button class="btn btn-primary" onclick="showModal('payment-modal')">
<span>+</span> Registrera inbetalning
</button>
</div>
<div class="card">
<div id="payments-table"><p class="loading">Laddar inbetalningar...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
LEDGER
═══════════════════════════════════════════════════════════════ -->
<div id="ledger" class="page-section hidden">
<h2 class="section-title">📒 Bokföring — LIVE FRÅN DATABAS</h2>
<div class="card">
<h3>Trial Balance</h3>
<div id="trial-balance-table"><p class="loading">Laddar trial balance...</p></div>
</div>
<div class="card mt-2">
<h3>Senaste verifikat</h3>
<div id="journal-table"><p class="loading">Laddar verifikat...</p></div>
</div>
<div class="card mt-2">
<h3>Perioder</h3>
<div id="periods-table"><p class="loading">Laddar perioder...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
ACCOUNTS
═══════════════════════════════════════════════════════════════ -->
<div id="accounts" class="page-section hidden">
<h2 class="section-title">📑 Konton — LIVE FRÅN DATABAS</h2>
<div class="card">
<div id="accounts-table"><p class="loading">Laddar konton...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
RECONCILIATION
═══════════════════════════════════════════════════════════════ -->
<div id="reconciliation" class="page-section hidden">
<h2 class="section-title">🔍 Avstämning</h2>
<div class="card">
<h3>Bankavstämning</h3>
<div id="reconciliation-table"><p class="loading">Laddar avstämning...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
NOTIFICATIONS
═══════════════════════════════════════════════════════════════ -->
<div id="notifications" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">🔔 Notiscenter</h2>
<button class="btn btn-outline" onclick="markAllNotificationsRead()">
Markera alla som lästa
</button>
</div>
<div class="section-actions">
<select id="notif-filter" onchange="loadNotifications()">
<option value="">Alla notiser</option>
<option value="unread">Olästa</option>
<option value="lead">Leads</option>
<option value="invoice">Fakturor</option>
<option value="ticket">Ärenden</option>
<option value="employee">HR</option>
</select>
</div>
<div class="card">
<div id="notifications-list"><p class="loading">Laddar notiser...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
REPORTS
═══════════════════════════════════════════════════════════════ -->
<div id="reports" class="page-section hidden">
<h2 class="section-title">📊 Rapportcenter</h2>
<div class="tabs">
<div class="tab active" onclick="showReportTab('financial')">Ekonomi</div>
<div class="tab" onclick="showReportTab('crm')">CRM</div>
<div class="tab" onclick="showReportTab('support')">Support</div>
<div class="tab" onclick="showReportTab('hr')">HR</div>
</div>
<!-- Financial Reports -->
<div id="report-financial" class="report-tab">
<div class="grid">
<div class="card" style="cursor: pointer;" onclick="generateReport('balance-sheet')">
<h3>📊 Balansräkning</h3>
<div class="subtitle">Tillgångar, skulder, eget kapital</div>
</div>
<div class="card" style="cursor: pointer;" onclick="generateReport('income-statement')">
<h3>📈 Resultaträkning</h3>
<div class="subtitle">Intäkter, kostnader, resultat</div>
</div>
<div class="card" style="cursor: pointer;" onclick="generateReport('vat-report')">
<h3>🧾 Momsrapport</h3>
<div class="subtitle">Momsredovisning per period</div>
</div>
<div class="card" style="cursor: pointer;" onclick="generateReport('accounts-receivable')">
<h3>💰 Kundreskontra</h3>
<div class="subtitle">Obetalda fakturor per kund</div>
</div>
<div class="card" style="cursor: pointer;" onclick="generateReport('revenue')">
<h3>💵 Omsättning</h3>
<div class="subtitle">Månadsvis omsättning</div>
</div>
<div class="card" style="cursor: pointer;" onclick="generateReport('aging')">
<h3>⏰ Fakturaåldersanalys</h3>
<div class="subtitle">Förfallna fakturor</div>
</div>
</div>
</div>
<!-- CRM Reports -->
<div id="report-crm" class="report-tab hidden">
<div class="grid">
<div class="card" style="cursor: pointer;" onclick="generateReport('customer-growth')">
<h3>📈 Kundtillväxt</h3>
<div class="subtitle">Nya kunder över tid</div>
</div>
<div class="card" style="cursor: pointer;" onclick="generateReport('lead-conversion')">
<h3>🎯 Lead-konvertering</h3>
<div class="subtitle">Konverteringsgrad per källa</div>
</div>
</div>
</div>
<!-- Support Reports -->
<div id="report-support" class="report-tab hidden">
<div class="grid">
<div class="card" style="cursor: pointer;" onclick="generateReport('support-performance')">
<h3>🎫 Supportprestanda</h3>
<div class="subtitle">Lösningstider och volym</div>
</div>
<div class="card" style="cursor: pointer;" onclick="generateReport('support-satisfaction')">
<h3>😊 Kundnöjdhet</h3>
<div class="subtitle">Per kategori</div>
</div>
</div>
</div>
<!-- HR Reports -->
<div id="report-hr" class="report-tab hidden">
<div class="grid">
<div class="card" style="cursor: pointer;" onclick="generateReport('headcount')">
<h3>👔 Personalstyrka</h3>
<div class="subtitle">Per avdelning</div>
</div>
<div class="card" style="cursor: pointer;" onclick="generateReport('salary-distribution')">
<h3>💰 Lönedistribution</h3>
<div class="subtitle">Lönespridning</div>
</div>
</div>
</div>
<div id="report-output" class="card mt-2 hidden">
<div id="report-content"></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
AUTOMATIONS
═══════════════════════════════════════════════════════════════ -->
<div id="automations" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">⚡ Automationer</h2>
<button class="btn btn-primary" onclick="showModal('automation-modal')">
<span>+</span> Ny automation
</button>
</div>
<div class="card">
<div id="automations-table"><p class="loading">Laddar automationer...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
SCHEDULES
═══════════════════════════════════════════════════════════════ -->
<div id="schedules" class="page-section hidden">
<h2 class="section-title">⏰ Scheman</h2>
<div class="card">
<div id="schedules-table"><p class="loading">Laddar scheman...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
MISSIONS
═══════════════════════════════════════════════════════════════ -->
<div id="missions" class="page-section hidden">
<h2 class="section-title">🎯 quiXzoom Missions — LIVE FRÅN DATABAS</h2>
<div class="card">
<div id="missions-table"><p class="loading">Laddar missions från PostgreSQL...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
CONTRIBUTORS
═══════════════════════════════════════════════════════════════ -->
<div id="contributors" class="page-section hidden">
<h2 class="section-title">👥 Contributors — LIVE FRÅN DATABAS</h2>
<div class="card">
<div id="contributors-table"><p class="loading">Laddar contributors från PostgreSQL...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
CHAT
═══════════════════════════════════════════════════════════════ -->
<div id="chat" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">💬 Intern chatt</h2>
<button class="btn btn-primary" onclick="showModal('channel-modal')">
<span>+</span> Ny kanal
</button>
</div>
<div class="chat-container" style="display: grid; grid-template-columns: 250px 1fr; gap: 1rem; height: calc(100vh - 200px);">
<!-- Channel List -->
<div class="card" style="overflow-y: auto;">
<h3 style="margin-bottom: 1rem;">Kanaler</h3>
<div id="chat-channels" style="display: flex; flex-direction: column; gap: 0.5rem;">
<p class="loading">Laddar kanaler...</p>
</div>
<h3 style="margin: 1.5rem 0 1rem;">Direktmeddelanden</h3>
<div id="chat-dms" style="display: flex; flex-direction: column; gap: 0.5rem;">
<p style="color: var(--text-muted); font-size: 0.875rem;">Inga DM</p>
</div>
</div>
<!-- Chat Area -->
<div class="card" style="display: flex; flex-direction: column;">
<div id="chat-header" style="border-bottom: 1px solid var(--border); padding-bottom: 1rem; margin-bottom: 1rem;">
<h3 id="chat-channel-name">Välj en kanal</h3>
<p id="chat-channel-desc" style="color: var(--text-muted); font-size: 0.875rem;"></p>
</div>
<div id="chat-messages" style="flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 0.75rem;">
<p style="color: var(--text-muted); text-align: center; margin-top: 2rem;">Välj en kanal för att börja chatta</p>
</div>
<div id="chat-input-area" style="border-top: 1px solid var(--border); padding-top: 1rem; margin-top: 1rem; display: none;">
<div style="display: flex; gap: 0.5rem;">
<input type="text" id="chat-input" placeholder="Skriv ett meddelande..." style="flex: 1;" onkeypress="if(event.key==='Enter')sendChatMessage()">
<button class="btn btn-primary" onclick="sendChatMessage()">Skicka</button>
</div>
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: var(--text-muted);">
Använd @namn för att omnämna någon | Dela ärende med #ticket-id
</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
MAIL
═══════════════════════════════════════════════════════════════ -->
<div id="mail" class="page-section hidden">
<h2 class="section-title">📧 Mail — ALLA KÄLLOR</h2>
<div class="grid">
<div class="card">
<h3>Totalt i inkorg</h3>
<div class="value" id="mail-inbox">-</div>
</div>
<div class="card">
<h3>Olästa</h3>
<div class="value warning" id="mail-unread">-</div>
</div>
<div class="card">
<h3>Skickat</h3>
<div class="value" id="mail-sent">-</div>
</div>
<div class="card">
<h3>quiXzoom väntelista</h3>
<div class="value success" id="mail-quixzoom">-</div>
</div>
</div>
<div class="section-actions">
<button class="btn btn-primary" onclick="showModal('compose-mail-modal')">
<span>✉️</span> Skriv nytt mail
</button>
<select id="mail-source-filter" onchange="loadMail()">
<option value="">Alla källor</option>
<option value="quixzoom_waitlist">quiXzoom väntelista</option>
<option value="landvex_contact">Landvex kontakt</option>
<option value="aamos_contact">AAMOS kontakt</option>
<option value="manual">Manuellt</option>
</select>
<select id="mail-status-filter" onchange="loadMail()">
<option value="">Alla statusar</option>
<option value="unread">Olästa</option>
<option value="read">Lästa</option>
<option value="replied">Besvarade</option>
</select>
</div>
<div class="card">
<h3>Meddelanden</h3>
<div id="mail-messages"><p class="loading">Laddar mail...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
SUPPORT
═══════════════════════════════════════════════════════════════ -->
<div id="support" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">🎫 Supportärenden</h2>
<button class="btn btn-primary" onclick="showModal('ticket-modal')">
<span>+</span> Nytt ärende
</button>
</div>
<div class="grid">
<div class="card">
<h3>Öppna ärenden</h3>
<div class="value warning" id="stat-tickets-open">-</div>
</div>
<div class="card">
<h3>Akuta</h3>
<div class="value danger" id="stat-tickets-urgent">-</div>
</div>
<div class="card">
<h3>Idag</h3>
<div class="value" id="stat-tickets-today">-</div>
</div>
<div class="card">
<h3>Snitt svarstid</h3>
<div class="value" id="stat-response-time">-</div>
</div>
</div>
<div class="section-actions">
<select id="ticket-status-filter" onchange="loadTickets()">
<option value="">Alla statusar</option>
<option value="open">Öppna</option>
<option value="in_progress">Pågående</option>
<option value="waiting">Väntar</option>
<option value="closed">Stängda</option>
</select>
<select id="ticket-priority-filter" onchange="loadTickets()">
<option value="">Alla prioriteringar</option>
<option value="urgent">Akut</option>
<option value="high">Hög</option>
<option value="medium">Medium</option>
<option value="low">Låg</option>
</select>
</div>
<div class="card">
<div id="tickets-table"><p class="loading">Laddar ärenden...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
FAQ
═══════════════════════════════════════════════════════════════ -->
<div id="faq" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">❓ FAQ / Kunskapsbas</h2>
<button class="btn btn-primary" onclick="showModal('faq-modal')">
<span>+</span> Ny fråga
</button>
</div>
<div class="card">
<div id="faq-table"><p class="loading">Laddar FAQ...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
EMPLOYEES
═══════════════════════════════════════════════════════════════ -->
<div id="employees" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">👔 Personal</h2>
<button class="btn btn-primary" onclick="showModal('employee-modal')">
<span>+</span> Ny anställd
</button>
</div>
<div class="grid">
<div class="card">
<h3>Totalt anställda</h3>
<div class="value" id="stat-employees-total">-</div>
</div>
<div class="card">
<h3>Nya denna månad</h3>
<div class="value success" id="stat-employees-new">-</div>
</div>
<div class="card">
<h3>Avtal slutar snart</h3>
<div class="value warning" id="stat-contracts-ending">-</div>
</div>
<div class="card">
<h3>Lönekostnad (mån)</h3>
<div class="value" id="stat-salary-total">-</div>
</div>
</div>
<div class="card mt-2">
<div id="employees-table"><p class="loading">Laddar personal...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
CONTRACTS
═══════════════════════════════════════════════════════════════ -->
<div id="contracts" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">📄 Anställningsavtal</h2>
<button class="btn btn-primary" onclick="showModal('contract-modal')">
<span>+</span> Nytt avtal
</button>
</div>
<div class="card">
<div id="contracts-table"><p class="loading">Laddar avtal...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
SALARY
═══════════════════════════════════════════════════════════════ -->
<div id="salary" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">💰 Löner</h2>
<button class="btn btn-primary" onclick="showModal('salary-modal')">
<span>+</span> Registrera lön
</button>
</div>
<div class="card">
<div id="salary-table"><p class="loading">Laddar löner...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
DOCUMENTS
═══════════════════════════════════════════════════════════════ -->
<div id="documents" class="page-section hidden">
<div class="flex-between mb-2">
<h2 class="section-title">📁 HR-dokument</h2>
<button class="btn btn-primary" onclick="showModal('document-modal')">
<span>+</span> Ladda upp dokument
</button>
</div>
<div class="card">
<div id="documents-table"><p class="loading">Laddar dokument...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
HEALTH
═══════════════════════════════════════════════════════════════ -->
<div id="health" class="page-section hidden">
<h2 class="section-title">🩺 Systemhälsa</h2>
<div class="card">
<div id="health-table"><p class="loading">Laddar systemstatus...</p></div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════
SETTINGS
═══════════════════════════════════════════════════════════════ -->
<div id="settings" class="page-section hidden">
<h2 class="section-title">⚙️ Inställningar</h2>
<div class="tabs">
<div class="tab active" onclick="showSettingsTab('company')">Företag</div>
<div class="tab" onclick="showSettingsTab('modules')">Moduler</div>
<div class="tab" onclick="showSettingsTab('notifications')">Notiser</div>
<div class="tab" onclick="showSettingsTab('integrations')">Integrationer</div>
</div>
<!-- Company Settings -->
<div id="settings-company" class="settings-tab">
<div class="card">
<h3>Företagsinformation</h3>
<div class="form-row mt-2">
<div class="form-group">
<label>Företagsnamn</label>
<input type="text" id="setting-company-name" value="LandveX AB">
</div>
<div class="form-group">
<label>Org.nr</label>
<input type="text" id="setting-org-nr" value="559141-7042">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Momsreg.nr</label>
<input type="text" id="setting-vat-nr" value="SE559141704201">
</div>
<div class="form-group">
<label>Betalningsvillkor (dagar)</label>
<input type="number" id="setting-payment-terms" value="30">
</div>
</div>
<div class="form-group">
<label>Adress</label>
<input type="text" id="setting-address" placeholder="Storgatan 1">
</div>
<div class="form-row">
<div class="form-group">
<label>Postnr</label>
<input type="text" id="setting-postal" placeholder="123 45">
</div>
<div class="form-group">
<label>Ort</label>
<input type="text" id="setting-city" placeholder="Stockholm">
</div>
</div>
<div class="mt-2">
<button class="btn btn-primary" onclick="saveSettings()">Spara inställningar</button>
</div>
</div>
</div>
<!-- Module Settings -->
<div id="settings-modules" class="settings-tab hidden">
<div class="card">
<h3>CRM-inställningar</h3>
<div class="form-group mt-2">
<label>
<input type="checkbox" id="crm-auto-convert"> Automatisk lead-konvertering
</label>
</div>
<div class="form-group">
<label>Standard kundtyp</label>
<select id="crm-default-type">
<option value="company">Företag</option>
<option value="individual">Privatperson</option>
</select>
</div>
<div class="form-group">
<label>Lead-tilldelning</label>
<select id="crm-assignment">
<option value="manual">Manuell</option>
<option value="round_robin">Round Robin</option>
<option value="auto">Automatisk</option>
</select>
</div>
</div>
<div class="card mt-2">
<h3>Faktureringsinställningar</h3>
<div class="form-row mt-2">
<div class="form-group">
<label>Standard moms (%)</label>
<input type="number" id="billing-default-vat" value="25">
</div>
<div class="form-group">
<label>Påminnelseavgift (kr)</label>
<input type="number" id="billing-reminder-fee" value="60">
</div>
</div>
<div class="form-group">
<label>Räntesats (%)</label>
<input type="number" id="billing-interest-rate" value="8">
</div>
</div>
<div class="card mt-2">
<h3>Supportinställningar</h3>
<div class="form-row mt-2">
<div class="form-group">
<label>SLA svarstid (timmar)</label>
<input type="number" id="support-sla-response" value="24">
</div>
<div class="form-group">
<label>SLA lösningstid (timmar)</label>
<input type="number" id="support-sla-resolution" value="72">
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="support-auto-assign"> Automatisk ärendetilldelning
</label>
</div>
</div>
<div class="card mt-2">
<h3>HR-inställningar</h3>
<div class="form-row mt-2">
<div class="form-group">
<label>Probetid (månader)</label>
<input type="number" id="hr-probation" value="6">
</div>
<div class="form-group">
<label>Semesterdagar</label>
<input type="number" id="hr-vacation" value="25">
</div>
</div>
<div class="form-group">
<label>Uppsägningstid (månader)</label>
<input type="number" id="hr-notice" value="3">
</div>
</div>
<div class="mt-2">
<button class="btn btn-primary" onclick="saveModuleSettings()">Spara modulinställningar</button>
</div>
</div>
<!-- Notification Settings -->
<div id="settings-notifications" class="settings-tab hidden">
<div class="card">
<h3>Notifieringsinställningar</h3>
<div class="form-group mt-2">
<label>
<input type="checkbox" id="notif-email" checked> E-postnotifieringar
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="notif-push" checked> Push-notifieringar
</label>
</div>
<h4 style="margin-top: 1.5rem; color: var(--text-muted);">Händelser</h4>
<div class="form-group">
<label>
<input type="checkbox" id="notif-new-lead" checked> Nytt lead
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="notif-new-invoice" checked> Ny faktura
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="notif-overdue-invoice" checked> Förfallen faktura
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="notif-new-ticket" checked> Nytt supportärende
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="notif-ticket-assigned" checked> Ärende tilldelat
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="notif-new-employee" checked> Ny anställd
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="notif-contract-ending" checked> Avtal upphör snart
</label>
</div>
<h4 style="margin-top: 1.5rem; color: var(--text-muted);">Sammanfattningar</h4>
<div class="form-group">
<label>
<input type="checkbox" id="notif-daily-summary" checked> Daglig sammanfattning
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="notif-weekly-summary" checked> Veckosammanfattning
</label>
</div>
<div class="mt-2">
<button class="btn btn-primary" onclick="saveNotificationSettings()">Spara notifieringsinställningar</button>
</div>
</div>
</div>
<!-- Integration Settings -->
<div id="settings-integrations" class="settings-tab hidden">
<div class="card">
<h3>quiXzoom Integration</h3>
<div class="form-group mt-2">
<label>API-URL</label>
<input type="text" id="integration-quixzoom-url" value="https://quixzoom.com/api">
</div>
<div class="form-group">
<label>API-nyckel</label>
<input type="password" id="integration-quixzoom-key" placeholder="••••••••">
</div>
</div>
<div class="card mt-2">
<h3>Landvex Platform</h3>
<div class="form-group mt-2">
<label>API-URL</label>
<input type="text" id="integration-landvex-url" value="https://api.landvex.com/v1">
</div>
<div class="form-group">
<label>API-nyckel</label>
<input type="password" id="integration-landvex-key" placeholder="••••••••">
</div>
</div>
<div class="card mt-2">
<h3>E-post (SMTP)</h3>
<div class="form-row mt-2">
<div class="form-group">
<label>SMTP-server</label>
<input type="text" id="integration-smtp-host" placeholder="smtp.gmail.com">
</div>
<div class="form-group">
<label>Port</label>
<input type="number" id="integration-smtp-port" value="587">
</div>
</div>
<div class="form-group">
<label>Användarnamn</label>
<input type="text" id="integration-smtp-user" placeholder="noreply@landvex.com">
</div>
<div class="form-group">
<label>Lösenord</label>
<input type="password" id="integration-smtp-pass" placeholder="••••••••">
</div>
</div>
<div class="mt-2">
<button class="btn btn-primary" onclick="saveIntegrationSettings()">Spara integrationsinställningar</button>
</div>
</div>
</div>
</main>
<!-- ═══════════════════════════════════════════════════════════════
MODALS
═══════════════════════════════════════════════════════════════ -->
<!-- Channel Modal -->
<div id="channel-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('channel-modal')">
<div class="modal">
<div class="modal-header">
<h3>Ny kanal</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('channel-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Kanalnamn</label>
<input type="text" id="channel-name" placeholder="projekt-x">
</div>
<div class="form-group">
<label>Beskrivning</label>
<input type="text" id="channel-desc" placeholder="Vad handlar kanalen om?">
</div>
<div class="form-group">
<label>Typ</label>
<select id="channel-type">
<option value="general">Allmän</option>
<option value="support">Support</option>
<option value="hr">HR</option>
<option value="sales">Försäljning</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('channel-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveChannel()">Skapa kanal</button>
</div>
</div>
</div>
<!-- Share Ticket Modal -->
<div id="share-ticket-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('share-ticket-modal')">
<div class="modal">
<div class="modal-header">
<h3>Dela ärende</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('share-ticket-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Ärende</label>
<input type="text" id="share-ticket-display" readonly>
<input type="hidden" id="share-ticket-id">
</div>
<div class="form-group">
<label>Dela till kanal</label>
<select id="share-ticket-channel">
<option value="">Välj kanal...</option>
</select>
</div>
<div class="form-group">
<label>Notering (valfritt)</label>
<textarea id="share-ticket-note" rows="2" placeholder="Varför delar du detta ärende?"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('share-ticket-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="shareTicket()">Dela ärende</button>
</div>
</div>
</div>
<!-- Customer Modal -->
<div id="customer-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('customer-modal')">
<div class="modal">
<div class="modal-header">
<h3>Ny kund</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('customer-modal')"></button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group">
<label>Kundtyp</label>
<select id="cust-type">
<option value="company">Företag</option>
<option value="individual">Privatperson</option>
</select>
</div>
<div class="form-group">
<label>Org.nr / Personnr</label>
<input type="text" id="cust-org-nr" placeholder="559141-7042">
</div>
</div>
<div class="form-group">
<label>Företagsnamn / Namn</label>
<input type="text" id="cust-name" placeholder="Acme AB">
</div>
<div class="form-row">
<div class="form-group">
<label>E-post</label>
<input type="email" id="cust-email" placeholder="kontakt@acme.se">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="tel" id="cust-phone" placeholder="070-123 45 67">
</div>
</div>
<div class="form-group">
<label>Adress</label>
<input type="text" id="cust-address" placeholder="Storgatan 1">
</div>
<div class="form-row">
<div class="form-group">
<label>Postnr</label>
<input type="text" id="cust-postal" placeholder="123 45">
</div>
<div class="form-group">
<label>Ort</label>
<input type="text" id="cust-city" placeholder="Stockholm">
</div>
</div>
<div class="form-group">
<label>Fakturareferens</label>
<input type="text" id="cust-reference" placeholder="Erik Svensson">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('customer-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveCustomer()">Spara kund</button>
</div>
</div>
</div>
<!-- Lead Modal -->
<div id="lead-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('lead-modal')">
<div class="modal">
<div class="modal-header">
<h3>Nytt lead</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('lead-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Företagsnamn / Kontaktperson</label>
<input type="text" id="lead-name" placeholder="Acme AB">
</div>
<div class="form-row">
<div class="form-group">
<label>E-post</label>
<input type="email" id="lead-email" placeholder="kontakt@acme.se">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="tel" id="lead-phone" placeholder="070-123 45 67">
</div>
</div>
<div class="form-group">
<label>Källa</label>
<select id="lead-source">
<option value="web">Webb</option>
<option value="referral">Referens</option>
<option value="cold_call">Kallsamtal</option>
<option value="event">Event</option>
<option value="other">Annat</option>
</select>
</div>
<div class="form-group">
<label>Noteringar</label>
<textarea id="lead-notes" rows="3" placeholder="Intresserad av VIMS..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('lead-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveLead()">Spara lead</button>
</div>
</div>
</div>
<!-- Agreement Modal -->
<div id="agreement-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('agreement-modal')">
<div class="modal">
<div class="modal-header">
<h3>Nytt avtal</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('agreement-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Kund</label>
<select id="agreement-customer">
<option value="">Välj kund...</option>
</select>
</div>
<div class="form-group">
<label>Avtalsnamn</label>
<input type="text" id="agreement-title" placeholder="VIMS Premium">
</div>
<div class="form-row">
<div class="form-group">
<label>Startdatum</label>
<input type="date" id="agreement-start">
</div>
<div class="form-group">
<label>Slutdatum</label>
<input type="date" id="agreement-end">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Faktureringsintervall</label>
<select id="agreement-frequency">
<option value="monthly">Månadsvis</option>
<option value="quarterly">Kvartalsvis</option>
<option value="semi_annual">Halvårsvis</option>
<option value="annual">Årsvis</option>
</select>
</div>
<div class="form-group">
<label>Belopp (exkl. moms)</label>
<input type="number" id="agreement-amount" placeholder="10000">
</div>
</div>
<div class="form-group">
<label>Beskrivning</label>
<textarea id="agreement-description" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('agreement-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveAgreement()">Spara avtal</button>
</div>
</div>
</div>
<!-- Invoice Modal -->
<div id="invoice-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('invoice-modal')">
<div class="modal" style="max-width: 900px;">
<div class="modal-header">
<h3>Ny faktura</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('invoice-modal')"></button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group">
<label>Kund</label>
<select id="invoice-customer" onchange="loadCustomerDetails()">
<option value="">Välj kund...</option>
</select>
</div>
<div class="form-group">
<label>Fakturadatum</label>
<input type="date" id="invoice-date" value="">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Förfallodatum</label>
<input type="date" id="invoice-due-date" value="">
</div>
<div class="form-group">
<label>Referens</label>
<input type="text" id="invoice-reference" placeholder="Er referens">
</div>
</div>
<h4 style="margin: 1.5rem 0 0.5rem; color: var(--text-muted);">Fakturarader</h4>
<div id="invoice-lines">
<div class="form-row invoice-line">
<div class="form-group" style="flex: 2;">
<label>Beskrivning</label>
<input type="text" class="line-desc" placeholder="Tjänstebeskrivning">
</div>
<div class="form-group">
<label>Antal</label>
<input type="number" class="line-qty" value="1" min="0" step="0.01">
</div>
<div class="form-group">
<label>Enhet</label>
<select class="line-unit">
<option value="st">st</option>
<option value="tim">tim</option>
<option value="mån">mån</option>
<option value="år">år</option>
</select>
</div>
<div class="form-group">
<label>Á-pris</label>
<input type="number" class="line-price" value="0" min="0" step="0.01">
</div>
<div class="form-group">
<label>Moms %</label>
<select class="line-vat">
<option value="25">25%</option>
<option value="12">12%</option>
<option value="6">6%</option>
<option value="0">0%</option>
</select>
</div>
</div>
</div>
<button class="btn btn-outline btn-sm mt-1" onclick="addInvoiceLine()">+ Lägg till rad</button>
<div class="mt-2" style="text-align: right;">
<div>Netto: <strong id="invoice-subtotal">0,00 kr</strong></div>
<div>Moms: <strong id="invoice-vat">0,00 kr</strong></div>
<div style="font-size: 1.2rem; margin-top: 0.5rem;">Totalt: <strong id="invoice-total">0,00 kr</strong></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('invoice-modal')">Avbryt</button>
<button class="btn btn-warning" onclick="saveInvoice('draft')">Spara utkast</button>
<button class="btn btn-primary" onclick="saveInvoice('sent')">Skapa & skicka</button>
</div>
</div>
</div>
<!-- Payment Modal -->
<div id="payment-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('payment-modal')">
<div class="modal">
<div class="modal-header">
<h3>Registrera inbetalning</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('payment-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Faktura</label>
<select id="payment-invoice">
<option value="">Välj faktura...</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>Belopp</label>
<input type="number" id="payment-amount" placeholder="10000" step="0.01">
</div>
<div class="form-group">
<label>Betalningsdatum</label>
<input type="date" id="payment-date" value="">
</div>
</div>
<div class="form-group">
<label>Betalningsmetod</label>
<select id="payment-method">
<option value="bank_transfer">Banköverföring</option>
<option value="swish">Swish</option>
<option value="card">Kort</option>
<option value="cash">Kontant</option>
<option value="autogiro">Autogiro</option>
</select>
</div>
<div class="form-group">
<label>Referens / OCR</label>
<input type="text" id="payment-reference" placeholder="OCR-nummer">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('payment-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="savePayment()">Registrera inbetalning</button>
</div>
</div>
</div>
<!-- Automation Modal -->
<div id="automation-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('automation-modal')">
<div class="modal">
<div class="modal-header">
<h3>Ny automation</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('automation-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Namn</label>
<input type="text" id="auto-name" placeholder="Månadsfakturering">
</div>
<div class="form-group">
<label>Beskrivning</label>
<textarea id="auto-description" rows="2"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Trigger-typ</label>
<select id="auto-trigger-type">
<option value="scheduled">Schemalagd</option>
<option value="event">Event-baserad</option>
<option value="manual">Manuell</option>
</select>
</div>
<div class="form-group">
<label>Frekvens</label>
<select id="auto-frequency">
<option value="daily">Dagligen</option>
<option value="weekly">Veckovis</option>
<option value="monthly">Månadsvis</option>
<option value="quarterly">Kvartalsvis</option>
</select>
</div>
</div>
<div class="form-group">
<label>Åtgärd</label>
<select id="auto-action">
<option value="create_invoice">Skapa faktura</option>
<option value="send_reminder">Skicka påminnelse</option>
<option value="export_report">Exportera rapport</option>
<option value="backup">Backup</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('automation-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveAutomation()">Spara automation</button>
</div>
</div>
</div>
<!-- Compose Mail Modal -->
<div id="compose-mail-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('compose-mail-modal')">
<div class="modal">
<div class="modal-header">
<h3>Skriv nytt mail</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('compose-mail-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Till</label>
<input type="email" id="compose-to" placeholder="kund@exempel.se">
</div>
<div class="form-group">
<label>Ämne</label>
<input type="text" id="compose-subject" placeholder="Ämnesrad">
</div>
<div class="form-group">
<label>Meddelande</label>
<textarea id="compose-body" rows="6" placeholder="Skriv ditt meddelande här..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('compose-mail-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="sendMail()">Skicka mail</button>
</div>
</div>
</div>
<!-- Ticket Modal -->
<div id="ticket-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('ticket-modal')">
<div class="modal">
<div class="modal-header">
<h3>Nytt supportärende</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('ticket-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Kund</label>
<select id="ticket-customer">
<option value="">Välj kund...</option>
</select>
</div>
<div class="form-group">
<label>Ämne</label>
<input type="text" id="ticket-subject" placeholder="Beskriv problemet">
</div>
<div class="form-group">
<label>Beskrivning</label>
<textarea id="ticket-description" rows="4" placeholder="Detaljerad beskrivning..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Prioritet</label>
<select id="ticket-priority">
<option value="low">Låg</option>
<option value="medium" selected>Medium</option>
<option value="high">Hög</option>
<option value="urgent">Akut</option>
</select>
</div>
<div class="form-group">
<label>Kategori</label>
<select id="ticket-category">
<option value="technical">Tekniskt</option>
<option value="billing">Fakturering</option>
<option value="general">Allmänt</option>
<option value="feature_request">Funktionsönskemål</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('ticket-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveTicket()">Skapa ärende</button>
</div>
</div>
</div>
<!-- FAQ Modal -->
<div id="faq-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('faq-modal')">
<div class="modal">
<div class="modal-header">
<h3>Ny FAQ</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('faq-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Fråga</label>
<input type="text" id="faq-question" placeholder="Hur gör jag för att...">
</div>
<div class="form-group">
<label>Svar</label>
<textarea id="faq-answer" rows="4" placeholder="Beskriv lösningen..."></textarea>
</div>
<div class="form-group">
<label>Kategori</label>
<select id="faq-category">
<option value="general">Allmänt</option>
<option value="billing">Fakturering</option>
<option value="technical">Tekniskt</option>
<option value="account">Konto</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('faq-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveFAQ()">Spara FAQ</button>
</div>
</div>
</div>
<!-- Employee Modal -->
<div id="employee-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('employee-modal')">
<div class="modal">
<div class="modal-header">
<h3>Ny anställd</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('employee-modal')"></button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group">
<label>Förnamn</label>
<input type="text" id="emp-first-name" placeholder="Erik">
</div>
<div class="form-group">
<label>Efternamn</label>
<input type="text" id="emp-last-name" placeholder="Svensson">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>E-post</label>
<input type="email" id="emp-email" placeholder="erik@landvex.com">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="tel" id="emp-phone" placeholder="070-123 45 67">
</div>
</div>
<div class="form-group">
<label>Personnummer</label>
<input type="text" id="emp-personal-number" placeholder="19750101-1234">
</div>
<div class="form-group">
<label>Adress</label>
<input type="text" id="emp-address" placeholder="Storgatan 1">
</div>
<div class="form-row">
<div class="form-group">
<label>Postnr</label>
<input type="text" id="emp-postal" placeholder="123 45">
</div>
<div class="form-group">
<label>Ort</label>
<input type="text" id="emp-city" placeholder="Stockholm">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Avdelning</label>
<select id="emp-department">
<option value="">Välj avdelning...</option>
</select>
</div>
<div class="form-group">
<label>Position</label>
<input type="text" id="emp-position" placeholder="VD">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Startdatum</label>
<input type="date" id="emp-start-date">
</div>
<div class="form-group">
<label>Anställningsform</label>
<select id="emp-type">
<option value="full_time">Heltid</option>
<option value="part_time">Deltid</option>
<option value="contract">Konsult</option>
<option value="intern">Praktikant</option>
</select>
</div>
</div>
<div class="form-group">
<label>Månadslön</label>
<input type="number" id="emp-salary" placeholder="100000">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('employee-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveEmployee()">Spara anställd</button>
</div>
</div>
</div>
<!-- Contract Modal -->
<div id="contract-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('contract-modal')">
<div class="modal">
<div class="modal-header">
<h3>Nytt anställningsavtal</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('contract-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Anställd</label>
<select id="contract-employee">
<option value="">Välj anställd...</option>
</select>
</div>
<div class="form-group">
<label>Avtalstyp</label>
<select id="contract-type">
<option value="permanent">Tillsvidare</option>
<option value="fixed_term">Tidsbegränsat</option>
<option value="consultant">Konsult</option>
<option value="internship">Praktik</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>Startdatum</label>
<input type="date" id="contract-start">
</div>
<div class="form-group">
<label>Slutdatum</label>
<input type="date" id="contract-end">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Månadslön</label>
<input type="number" id="contract-salary" placeholder="100000">
</div>
<div class="form-group">
<label>Arbetstimmar/vecka</label>
<input type="number" id="contract-hours" value="40" step="0.5">
</div>
</div>
<div class="form-group">
<label>Probetid (månader)</label>
<input type="number" id="contract-probation" value="6">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('contract-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveContract()">Spara avtal</button>
</div>
</div>
</div>
<!-- Salary Modal -->
<div id="salary-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('salary-modal')">
<div class="modal">
<div class="modal-header">
<h3>Registrera lön</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('salary-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Anställd</label>
<select id="salary-employee">
<option value="">Välj anställd...</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>År</label>
<input type="number" id="salary-year" value="2026">
</div>
<div class="form-group">
<label>Månad</label>
<input type="number" id="salary-month" value="7" min="1" max="12">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Grundlön</label>
<input type="number" id="salary-base" placeholder="100000">
</div>
<div class="form-group">
<label>Övertid</label>
<input type="number" id="salary-overtime" value="0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Bonus</label>
<input type="number" id="salary-bonus" value="0">
</div>
<div class="form-group">
<label>Avdrag</label>
<input type="number" id="salary-deductions" value="0">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Skatt</label>
<input type="number" id="salary-tax" placeholder="30000">
</div>
<div class="form-group">
<label>Nettolön</label>
<input type="number" id="salary-net" placeholder="70000">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('salary-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveSalary()">Registrera lön</button>
</div>
</div>
</div>
<!-- Document Modal -->
<div id="document-modal" class="modal-overlay hidden" onclick="if(event.target===this)hideModal('document-modal')">
<div class="modal">
<div class="modal-header">
<h3>Ladda upp dokument</h3>
<button class="btn btn-sm btn-outline" onclick="hideModal('document-modal')"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Anställd</label>
<select id="doc-employee">
<option value="">Välj anställd...</option>
</select>
</div>
<div class="form-group">
<label>Dokumenttyp</label>
<select id="doc-type">
<option value="contract">Anställningsavtal</option>
<option value="id">ID-handling</option>
<option value="certificate">Intyg</option>
<option value="review">Medarbetarsamtal</option>
<option value="other">Annat</option>
</select>
</div>
<div class="form-group">
<label>Titel</label>
<input type="text" id="doc-title" placeholder="Anställningsavtal 2026">
</div>
<div class="form-group">
<label>Fil-URL</label>
<input type="text" id="doc-url" placeholder="https://...">
</div>
<div class="form-group">
<label>Giltig till</label>
<input type="date" id="doc-expiry">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" onclick="hideModal('document-modal')">Avbryt</button>
<button class="btn btn-primary" onclick="saveDocument()">Spara dokument</button>
</div>
</div>
</div>
<script>
// ═══════════════════════════════════════════════════════════════
// API CONFIGURATION
// ═══════════════════════════════════════════════════════════════
const API_BASE = '';
// ═══════════════════════════════════════════════════════════════
// NAVIGATION
// ═══════════════════════════════════════════════════════════════
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'));
document.getElementById('nav-' + section).classList.add('active');
// Load section data
switch(section) {
case 'customers': loadCustomers(); break;
case 'leads': loadLeads(); break;
case 'agreements': loadAgreements(); break;
case 'invoices': loadInvoices('all'); break;
case 'payments': loadPayments(); break;
case 'automations': loadAutomations(); break;
case 'schedules': loadSchedules(); break;
case 'reconciliation': loadReconciliation(); break;
case 'support': loadSupport(); break;
case 'faq': loadFAQ(); break;
case 'employees': loadEmployees(); break;
case 'contracts': loadContracts(); break;
case 'salary': loadSalary(); break;
case 'documents': loadDocuments(); break;
case 'notifications': loadNotifications(); break;
case 'reports': loadReports(); break;
case 'chat': loadChat(); break;
}
}
// ═══════════════════════════════════════════════════════════════
// MODALS
// ═══════════════════════════════════════════════════════════════
function showModal(id) {
document.getElementById(id).classList.remove('hidden');
if (id === 'invoice-modal') {
document.getElementById('invoice-date').value = new Date().toISOString().split('T')[0];
const due = new Date();
due.setDate(due.getDate() + 30);
document.getElementById('invoice-due-date').value = due.toISOString().split('T')[0];
loadCustomerSelect();
}
if (id === 'agreement-modal') {
loadCustomerSelect('agreement-customer');
document.getElementById('agreement-start').value = new Date().toISOString().split('T')[0];
}
if (id === 'payment-modal') {
document.getElementById('payment-date').value = new Date().toISOString().split('T')[0];
loadInvoiceSelect();
}
if (id === 'ticket-modal') {
loadCustomerSelect('ticket-customer');
}
if (id === 'contract-modal') {
loadEmployeeSelect('contract-employee');
document.getElementById('contract-start').value = new Date().toISOString().split('T')[0];
}
if (id === 'salary-modal') {
loadEmployeeSelect('salary-employee');
}
if (id === 'document-modal') {
loadEmployeeSelect('doc-employee');
}
if (id === 'employee-modal') {
loadDepartmentSelect();
document.getElementById('emp-start-date').value = new Date().toISOString().split('T')[0];
}
}
function hideModal(id) {
document.getElementById(id).classList.add('hidden');
}
// ═══════════════════════════════════════════════════════════════
// DASHBOARD
// ═══════════════════════════════════════════════════════════════
async function loadDashboard() {
try {
// Revenue
const revRes = await fetch(`${API_BASE}/api/billing/dashboard`);
if (revRes.ok) {
const revData = await revRes.json();
document.getElementById('dash-revenue').textContent = formatCurrency(revData.revenue_ytd || 0);
document.getElementById('dash-invoices-draft').textContent = revData.invoices_draft || 0;
document.getElementById('dash-invoices-overdue').textContent = revData.invoices_overdue || 0;
document.getElementById('dash-active-customers').textContent = revData.active_customers || 0;
}
} catch (e) { console.error('Dashboard revenue error:', e); }
// Quixzoom
try {
const qxRes = await fetch(`${API_BASE}/api/v1/quixzoom/stats`);
if (qxRes.ok) {
const qxData = await qxRes.json();
document.getElementById('stat-missions-total').textContent = qxData.missions_total || '-';
document.getElementById('stat-missions-active').textContent = qxData.missions_active || '-';
document.getElementById('stat-submissions').textContent = qxData.submissions_total || '-';
document.getElementById('stat-contributors').textContent = qxData.contributors_total || '-';
}
} catch (e) { console.error('Quixzoom error:', e); }
// Ledger
try {
const accRes = await fetch(`${API_BASE}/api/ledger/accounts`);
if (accRes.ok) {
const accData = await accRes.json();
document.getElementById('stat-accounts').textContent = accData.accounts?.length || '-';
const bank = accData.accounts?.find(a => a.account_number === '1930');
if (bank) {
document.getElementById('stat-cash').textContent = formatCurrency(bank.balance);
}
}
} catch (e) { console.error('Ledger error:', e); }
try {
const tbRes = await fetch(`${API_BASE}/api/ledger/trial-balance`);
if (tbRes.ok) {
const tbData = await tbRes.json();
document.getElementById('stat-balanced').textContent = tbData.balanced ? '✓' : '✗';
document.getElementById('stat-balanced').style.color = tbData.balanced ? 'var(--success)' : 'var(--danger)';
document.getElementById('stat-balanced-sub').textContent = tbData.balanced ? 'BALANSERAD' : 'OBALANSERAD';
}
} catch (e) { console.error('Trial balance error:', e); }
// Mail
try {
const mailRes = await fetch(`${API_BASE}/api/mail/stats`);
if (mailRes.ok) {
const mailData = await mailRes.json();
document.getElementById('stat-mail-inbox').textContent = mailData.inbox_total || '-';
document.getElementById('stat-mail-unread').textContent = mailData.unread_count || '-';
document.getElementById('stat-mail-sent').textContent = mailData.sent_total || '-';
}
} catch (e) { console.error('Mail error:', e); }
// Alerts
loadAlerts();
}
async function loadAlerts() {
try {
const res = await fetch(`${API_BASE}/api/alerts`);
if (!res.ok) return;
const data = await res.json();
const container = document.getElementById('dashboard-alerts');
container.innerHTML = '';
data.alerts?.forEach(alert => {
const div = document.createElement('div');
div.className = `alert ${alert.severity}`;
div.innerHTML = `<strong>${alert.icon || '🚨'} ${alert.title}:</strong> ${alert.message}`;
container.appendChild(div);
});
} catch (e) { /* silent */ }
}
// ═══════════════════════════════════════════════════════════════
// CUSTOMERS (CRM)
// ═══════════════════════════════════════════════════════════════
async function loadCustomers() {
try {
const filter = document.getElementById('customer-filter')?.value || '';
const res = await fetch(`${API_BASE}/api/billing/customers?status=${filter}`);
const data = await res.json();
let html = '<table><thead><tr><th>Namn</th><th>Org.nr</th><th>E-post</th><th>Telefon</th><th>Status</th><th>Åtgärder</th></tr></thead><tbody>';
data.customers?.forEach(c => {
html += `<tr>
<td><strong>${c.name}</strong></td>
<td>${c.org_nr || '-'}</td>
<td>${c.email || '-'}</td>
<td>${c.phone || '-'}</td>
<td><span class="badge badge-${c.status === 'active' ? 'success' : 'gray'}">${c.status}</span></td>
<td>
<button class="btn btn-sm btn-outline" onclick="viewCustomer('${c.id}')">Visa</button>
<button class="btn btn-sm btn-primary" onclick="createInvoiceForCustomer('${c.id}')">Fakturera</button>
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('customers-table').innerHTML = html;
} catch (e) {
document.getElementById('customers-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
function searchCustomers() {
const query = document.getElementById('customer-search').value.toLowerCase();
const rows = document.querySelectorAll('#customers-table tbody tr');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(query) ? '' : 'none';
});
}
async function saveCustomer() {
const customer = {
customer_type: document.getElementById('cust-type').value,
org_nr: document.getElementById('cust-org-nr').value,
name: document.getElementById('cust-name').value,
email: document.getElementById('cust-email').value,
phone: document.getElementById('cust-phone').value,
address: document.getElementById('cust-address').value,
postal_code: document.getElementById('cust-postal').value,
city: document.getElementById('cust-city').value,
reference: document.getElementById('cust-reference').value
};
try {
const res = await fetch(`${API_BASE}/api/billing/customers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(customer)
});
if (res.ok) {
hideModal('customer-modal');
loadCustomers();
showToast('Kund sparad!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
function viewCustomer(id) {
window.open(`/customers/${id}`, '_blank');
}
function createInvoiceForCustomer(customerId) {
showModal('invoice-modal');
// Pre-select customer
setTimeout(() => {
document.getElementById('invoice-customer').value = customerId;
loadCustomerDetails();
}, 100);
}
// ═══════════════════════════════════════════════════════════════
// LEADS
// ═══════════════════════════════════════════════════════════════
async function loadLeads() {
try {
const res = await fetch(`${API_BASE}/api/crm/leads`);
const data = await res.json();
let html = '<table><thead><tr><th>Namn</th><th>E-post</th><th>Telefon</th><th>Källa</th><th>Status</th><th>Åtgärder</th></tr></thead><tbody>';
data.leads?.forEach(l => {
html += `<tr>
<td><strong>${l.name}</strong></td>
<td>${l.email || '-'}</td>
<td>${l.phone || '-'}</td>
<td>${l.source}</td>
<td><span class="badge badge-${l.status === 'new' ? 'blue' : l.status === 'contacted' ? 'warning' : 'success'}">${l.status}</span></td>
<td>
<button class="btn btn-sm btn-outline" onclick="convertLead('${l.id}')">Konvertera</button>
<button class="btn btn-sm btn-outline" onclick="deleteLead('${l.id}')">Ta bort</button>
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('leads-table').innerHTML = html;
} catch (e) {
document.getElementById('leads-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function saveLead() {
const lead = {
name: document.getElementById('lead-name').value,
email: document.getElementById('lead-email').value,
phone: document.getElementById('lead-phone').value,
source: document.getElementById('lead-source').value,
notes: document.getElementById('lead-notes').value
};
try {
const res = await fetch(`${API_BASE}/api/crm/leads`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(lead)
});
if (res.ok) {
hideModal('lead-modal');
loadLeads();
showToast('Lead sparad!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function convertLead(leadId) {
if (!confirm('Konvertera lead till kund?')) return;
try {
const res = await fetch(`${API_BASE}/api/crm/leads/${leadId}/convert`, { method: 'POST' });
if (res.ok) {
loadLeads();
showToast('Lead konverterat till kund!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// AGREEMENTS
// ═══════════════════════════════════════════════════════════════
async function loadAgreements() {
try {
const res = await fetch(`${API_BASE}/api/billing/agreements`);
const data = await res.json();
let html = '<table><thead><tr><th>Avtalsnr</th><th>Kund</th><th>Typ</th><th>Period</th><th>Belopp</th><th>Status</th><th>Åtgärder</th></tr></thead><tbody>';
data.agreements?.forEach(a => {
html += `<tr>
<td>${a.agreement_number}</td>
<td>${a.customer_name}</td>
<td>${a.agreement_type}</td>
<td>${a.start_date} - ${a.end_date || 'Löpande'}</td>
<td>${formatCurrency(a.amount)}</td>
<td><span class="badge badge-${a.status === 'active' ? 'success' : a.status === 'draft' ? 'gray' : 'warning'}">${a.status}</span></td>
<td>
<button class="btn btn-sm btn-outline" onclick="viewAgreement('${a.id}')">Visa</button>
<button class="btn btn-sm btn-primary" onclick="createInvoiceFromAgreement('${a.id}')">Fakturera</button>
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('agreements-table').innerHTML = html;
} catch (e) {
document.getElementById('agreements-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function saveAgreement() {
const agreement = {
customer_id: document.getElementById('agreement-customer').value,
title: document.getElementById('agreement-title').value,
start_date: document.getElementById('agreement-start').value,
end_date: document.getElementById('agreement-end').value,
billing_frequency: document.getElementById('agreement-frequency').value,
amount: parseFloat(document.getElementById('agreement-amount').value),
description: document.getElementById('agreement-description').value
};
try {
const res = await fetch(`${API_BASE}/api/billing/agreements`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(agreement)
});
if (res.ok) {
hideModal('agreement-modal');
loadAgreements();
showToast('Avtal sparat!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// INVOICES
// ═══════════════════════════════════════════════════════════════
let currentInvoiceTab = 'all';
function showInvoiceTab(tab) {
currentInvoiceTab = tab;
document.querySelectorAll('#invoices .tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
loadInvoices(tab);
}
async function loadInvoices(status = 'all') {
try {
const url = status === 'all'
? `${API_BASE}/api/billing/invoices`
: `${API_BASE}/api/billing/invoices?status=${status}`;
const res = await fetch(url);
const data = await res.json();
let html = '<table><thead><tr><th>Fakturanr</th><th>Kund</th><th>Datum</th><th>Förfallo</th><th>Belopp</th><th>Status</th><th>Åtgärder</th></tr></thead><tbody>';
data.invoices?.forEach(inv => {
const isOverdue = inv.status === 'sent' && new Date(inv.due_date) < new Date();
html += `<tr>
<td>${inv.invoice_number}</td>
<td>${inv.customer_name}</td>
<td>${inv.invoice_date}</td>
<td style="color: ${isOverdue ? 'var(--danger)' : ''}">${inv.due_date}${isOverdue ? ' ⚠️' : ''}</td>
<td>${formatCurrency(inv.total)}</td>
<td><span class="badge badge-${inv.status === 'paid' ? 'success' : inv.status === 'sent' ? 'blue' : inv.status === 'overdue' ? 'danger' : 'gray'}">${inv.status}</span></td>
<td>
<button class="btn btn-sm btn-outline" onclick="viewInvoice('${inv.id}')">Visa</button>
${inv.status === 'draft' ? `<button class="btn btn-sm btn-primary" onclick="sendInvoice('${inv.id}')">Skicka</button>` : ''}
${inv.status === 'sent' || inv.status === 'overdue' ? `<button class="btn btn-sm btn-success" onclick="markPaid('${inv.id}')">Betald</button>` : ''}
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('invoices-table').innerHTML = html;
} catch (e) {
document.getElementById('invoices-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function loadCustomerSelect(selectId = 'invoice-customer') {
try {
const res = await fetch(`${API_BASE}/api/billing/customers?status=active`);
const data = await res.json();
const select = document.getElementById(selectId);
select.innerHTML = '<option value="">Välj kund...</option>';
data.customers?.forEach(c => {
select.innerHTML += `<option value="${c.id}">${c.name}</option>`;
});
} catch (e) { console.error('Load customers error:', e); }
}
async function loadCustomerDetails() {
const customerId = document.getElementById('invoice-customer').value;
if (!customerId) return;
try {
const res = await fetch(`${API_BASE}/api/billing/customers/${customerId}`);
const data = await res.json();
document.getElementById('invoice-reference').value = data.customer?.reference || '';
} catch (e) { console.error('Load customer details error:', e); }
}
function addInvoiceLine() {
const container = document.getElementById('invoice-lines');
const line = document.createElement('div');
line.className = 'form-row invoice-line mt-1';
line.innerHTML = `
<div class="form-group" style="flex: 2;">
<input type="text" class="line-desc" placeholder="Tjänstebeskrivning">
</div>
<div class="form-group">
<input type="number" class="line-qty" value="1" min="0" step="0.01">
</div>
<div class="form-group">
<select class="line-unit">
<option value="st">st</option>
<option value="tim">tim</option>
<option value="mån">mån</option>
<option value="år">år</option>
</select>
</div>
<div class="form-group">
<input type="number" class="line-price" value="0" min="0" step="0.01">
</div>
<div class="form-group">
<select class="line-vat">
<option value="25">25%</option>
<option value="12">12%</option>
<option value="6">6%</option>
<option value="0">0%</option>
</select>
</div>
<div class="form-group">
<button class="btn btn-sm btn-danger" onclick="this.closest('.invoice-line').remove(); calculateInvoiceTotal();">✕</button>
</div>
`;
container.appendChild(line);
}
function calculateInvoiceTotal() {
let subtotal = 0;
let vatTotal = 0;
document.querySelectorAll('.invoice-line').forEach(line => {
const qty = parseFloat(line.querySelector('.line-qty').value) || 0;
const price = parseFloat(line.querySelector('.line-price').value) || 0;
const vat = parseFloat(line.querySelector('.line-vat').value) || 0;
const lineTotal = qty * price;
const lineVat = lineTotal * (vat / 100);
subtotal += lineTotal;
vatTotal += lineVat;
});
document.getElementById('invoice-subtotal').textContent = formatCurrency(subtotal);
document.getElementById('invoice-vat').textContent = formatCurrency(vatTotal);
document.getElementById('invoice-total').textContent = formatCurrency(subtotal + vatTotal);
}
// Recalculate on input change
document.addEventListener('input', function(e) {
if (e.target.classList.contains('line-qty') || e.target.classList.contains('line-price') || e.target.classList.contains('line-vat')) {
calculateInvoiceTotal();
}
});
async function saveInvoice(status) {
const lines = [];
document.querySelectorAll('.invoice-line').forEach((line, idx) => {
lines.push({
line_number: idx + 1,
description: line.querySelector('.line-desc').value,
quantity: parseFloat(line.querySelector('.line-qty').value) || 0,
unit: line.querySelector('.line-unit').value,
unit_price: parseFloat(line.querySelector('.line-price').value) || 0,
vat_rate: parseFloat(line.querySelector('.line-vat').value) || 0
});
});
const invoice = {
customer_id: document.getElementById('invoice-customer').value,
invoice_date: document.getElementById('invoice-date').value,
due_date: document.getElementById('invoice-due-date').value,
customer_reference: document.getElementById('invoice-reference').value,
status: status,
lines: lines
};
try {
const res = await fetch(`${API_BASE}/api/billing/invoices`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(invoice)
});
if (res.ok) {
hideModal('invoice-modal');
loadInvoices(currentInvoiceTab);
showToast(status === 'sent' ? 'Faktura skapad och skickad!' : 'Faktura sparad som utkast!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function sendInvoice(invoiceId) {
try {
const res = await fetch(`${API_BASE}/api/billing/invoices/${invoiceId}/send`, { method: 'POST' });
if (res.ok) {
loadInvoices(currentInvoiceTab);
showToast('Faktura skickad!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function markPaid(invoiceId) {
showModal('payment-modal');
setTimeout(() => {
document.getElementById('payment-invoice').value = invoiceId;
}, 100);
}
function viewInvoice(id) {
window.open(`/invoices/${id}/pdf`, '_blank');
}
// ═══════════════════════════════════════════════════════════════
// PAYMENTS
// ═══════════════════════════════════════════════════════════════
async function loadPayments() {
try {
const res = await fetch(`${API_BASE}/api/billing/payments`);
const data = await res.json();
let html = '<table><thead><tr><th>Datum</th><th>Faktura</th><th>Belopp</th><th>Metod</th><th>Status</th><th>Åtgärder</th></tr></thead><tbody>';
data.payments?.forEach(p => {
html += `<tr>
<td>${p.payment_date}</td>
<td>${p.invoice_number || '-'}</td>
<td>${formatCurrency(p.amount)}</td>
<td>${p.payment_method}</td>
<td><span class="badge badge-${p.reconciliation_status === 'reconciled' ? 'success' : 'warning'}">${p.reconciliation_status}</span></td>
<td>
${p.reconciliation_status !== 'reconciled' ? `<button class="btn btn-sm btn-primary" onclick="reconcilePayment('${p.id}')">Avstäm</button>` : ''}
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('payments-table').innerHTML = html;
} catch (e) {
document.getElementById('payments-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function loadInvoiceSelect() {
try {
const res = await fetch(`${API_BASE}/api/billing/invoices?status=sent,overdue,partially_paid`);
const data = await res.json();
const select = document.getElementById('payment-invoice');
select.innerHTML = '<option value="">Välj faktura...</option>';
data.invoices?.forEach(inv => {
select.innerHTML += `<option value="${inv.id}">${inv.invoice_number} - ${inv.customer_name} (${formatCurrency(inv.amount_due)} kvar)</option>`;
});
} catch (e) { console.error('Load invoices error:', e); }
}
async function savePayment() {
const payment = {
invoice_id: document.getElementById('payment-invoice').value,
amount: parseFloat(document.getElementById('payment-amount').value),
payment_date: document.getElementById('payment-date').value,
payment_method: document.getElementById('payment-method').value,
payment_reference: document.getElementById('payment-reference').value
};
try {
const res = await fetch(`${API_BASE}/api/billing/payments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payment)
});
if (res.ok) {
hideModal('payment-modal');
loadPayments();
loadInvoices(currentInvoiceTab);
showToast('Inbetalning registrerad!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function reconcilePayment(paymentId) {
try {
const res = await fetch(`${API_BASE}/api/billing/payments/${paymentId}/reconcile`, { method: 'POST' });
if (res.ok) {
loadPayments();
showToast('Inbetalning avstämd!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// LEDGER
// ═══════════════════════════════════════════════════════════════
async function loadTrialBalance() {
try {
const res = await fetch(`${API_BASE}/api/ledger/trial-balance`);
const data = await res.json();
let html = '<table><thead><tr><th>Konto</th><th>Namn</th><th>Typ</th><th>Debet</th><th>Kredit</th></tr></thead><tbody>';
data.rows?.forEach(r => {
html += `<tr>
<td>${r.account_number}</td>
<td>${r.name}</td>
<td>${r.account_type}</td>
<td>${formatCurrency(r.total_debit)}</td>
<td>${formatCurrency(r.total_credit)}</td>
</tr>`;
});
html += `<tr style="font-weight: bold; border-top: 2px solid var(--border)">
<td colspan="3">TOTALT</td>
<td>${formatCurrency(data.total_debit)}</td>
<td>${formatCurrency(data.total_credit)}</td>
</tr>`;
html += '</tbody></table>';
html += `<p style="margin-top: 1rem; color: ${data.balanced ? 'var(--success)' : 'var(--danger)'}">
${data.balanced ? '✓ BALANSERAD' : '✗ OBALANSERAD'}
</p>`;
document.getElementById('trial-balance-table').innerHTML = html;
} catch (e) {
document.getElementById('trial-balance-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function loadJournal() {
try {
const res = await fetch(`${API_BASE}/api/ledger/journal`);
const data = await res.json();
let html = '<table><thead><tr><th>#</th><th>Datum</th><th>Beskrivning</th><th>Period</th><th>Belopp</th></tr></thead><tbody>';
data.entries?.forEach(e => {
const amt = e.lines ? e.lines.reduce((s, l) => s + (l.debit || 0), 0) : 0;
html += `<tr>
<td>${e.entry_number}</td>
<td>${e.entry_date}</td>
<td>${e.description}</td>
<td>${e.period}</td>
<td>${formatCurrency(amt)}</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('journal-table').innerHTML = html;
} catch (e) {
document.getElementById('journal-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function loadPeriods() {
try {
const res = await fetch(`${API_BASE}/api/ledger/periods`);
const data = await res.json();
let html = '<table><thead><tr><th>Period</th><th>År</th><th>Status</th><th>Stängd</th><th>Åtgärder</th></tr></thead><tbody>';
data.periods?.forEach(p => {
html += `<tr>
<td>${p.period}</td>
<td>${p.fiscal_year}</td>
<td><span class="badge ${p.status === 'open' ? 'badge-success' : 'badge-warning'}">${p.status}</span></td>
<td>${p.closed_at || '-'}</td>
<td>
${p.status === 'open' ? `<button class="btn btn-sm btn-warning" onclick="closePeriod('${p.period}')">Stäng</button>` : ''}
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('periods-table').innerHTML = html;
} catch (e) {
document.getElementById('periods-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function closePeriod(period) {
if (!confirm(`Stäng period ${period}? Detta kan inte ångras.`)) return;
try {
const res = await fetch(`${API_BASE}/api/ledger/periods/${period}/close`, { method: 'POST' });
if (res.ok) {
loadPeriods();
showToast('Period stängd!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// ACCOUNTS
// ═══════════════════════════════════════════════════════════════
async function loadAccounts() {
try {
const res = await fetch(`${API_BASE}/api/ledger/accounts`);
const data = await res.json();
let html = '<table><thead><tr><th>Konto</th><th>Namn</th><th>Typ</th><th>Debet</th><th>Kredit</th><th>Saldo</th></tr></thead><tbody>';
data.accounts?.forEach(a => {
const balance = parseFloat(a.balance);
html += `<tr>
<td>${a.account_number}</td>
<td>${a.name}</td>
<td>${a.account_type}</td>
<td>${formatCurrency(a.total_debit)}</td>
<td>${formatCurrency(a.total_credit)}</td>
<td style="color: ${balance >= 0 ? 'var(--success)' : 'var(--danger)'}">${formatCurrency(balance)}</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('accounts-table').innerHTML = html;
} catch (e) {
document.getElementById('accounts-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
// ═══════════════════════════════════════════════════════════════
// RECONCILIATION
// ═══════════════════════════════════════════════════════════════
async function loadReconciliation() {
try {
const res = await fetch(`${API_BASE}/api/ledger/reconciliation`);
const data = await res.json();
let html = '<table><thead><tr><th>Datum</th><th>Transaktion</th><th>Belopp</th><th>Status</th><th>Åtgärder</th></tr></thead><tbody>';
data.transactions?.forEach(t => {
html += `<tr>
<td>${t.date}</td>
<td>${t.description}</td>
<td>${formatCurrency(t.amount)}</td>
<td><span class="badge badge-${t.status === 'reconciled' ? 'success' : 'warning'}">${t.status}</span></td>
<td>
${t.status !== 'reconciled' ? `<button class="btn btn-sm btn-primary" onclick="reconcileTransaction('${t.id}')">Matcha</button>` : ''}
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('reconciliation-table').innerHTML = html;
} catch (e) {
document.getElementById('reconciliation-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
// ═══════════════════════════════════════════════════════════════
// REPORTS
// ═══════════════════════════════════════════════════════════════
async function generateReport(type) {
try {
const res = await fetch(`${API_BASE}/api/ledger/reports/${type}`);
const data = await res.json();
document.getElementById('report-output').classList.remove('hidden');
document.getElementById('report-content').innerHTML = `
<h3>${data.title}</h3>
<p>Genererad: ${new Date().toLocaleString('sv-SE')}</p>
<pre style="background: var(--bg); padding: 1rem; border-radius: var(--radius-sm); overflow-x: auto;">${JSON.stringify(data, null, 2)}</pre>
<button class="btn btn-primary mt-1" onclick="exportReport('${type}')">Exportera PDF</button>
`;
} catch (e) {
alert('Fel: ' + e.message);
}
}
function exportReport(type) {
window.open(`${API_BASE}/api/ledger/reports/${type}/pdf`, '_blank');
}
// ═══════════════════════════════════════════════════════════════
// AUTOMATIONS
// ═══════════════════════════════════════════════════════════════
async function loadAutomations() {
try {
const res = await fetch(`${API_BASE}/api/automations`);
const data = await res.json();
let html = '<table><thead><tr><th>Namn</th><th>Trigger</th><th>Frekvens</th><th>Åtgärd</th><th>Status</th><th>Åtgärder</th></tr></thead><tbody>';
data.automations?.forEach(a => {
html += `<tr>
<td><strong>${a.name}</strong></td>
<td>${a.trigger_type}</td>
<td>${a.frequency || '-'}</td>
<td>${a.action}</td>
<td><span class="badge badge-${a.is_active ? 'success' : 'gray'}">${a.is_active ? 'Aktiv' : 'Inaktiv'}</span></td>
<td>
<button class="btn btn-sm btn-outline" onclick="runAutomation('${a.id}')">Kör nu</button>
<button class="btn btn-sm btn-outline" onclick="toggleAutomation('${a.id}')">${a.is_active ? 'Pausa' : 'Aktivera'}</button>
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('automations-table').innerHTML = html;
} catch (e) {
document.getElementById('automations-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function saveAutomation() {
const automation = {
name: document.getElementById('auto-name').value,
description: document.getElementById('auto-description').value,
trigger_type: document.getElementById('auto-trigger-type').value,
frequency: document.getElementById('auto-frequency').value,
action: document.getElementById('auto-action').value
};
try {
const res = await fetch(`${API_BASE}/api/automations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(automation)
});
if (res.ok) {
hideModal('automation-modal');
loadAutomations();
showToast('Automation sparad!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function runAutomation(id) {
try {
const res = await fetch(`${API_BASE}/api/automations/${id}/run`, { method: 'POST' });
if (res.ok) {
showToast('Automation körd!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function toggleAutomation(id) {
try {
const res = await fetch(`${API_BASE}/api/automations/${id}/toggle`, { method: 'POST' });
if (res.ok) {
loadAutomations();
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// SCHEDULES
// ═══════════════════════════════════════════════════════════════
async function loadSchedules() {
try {
const res = await fetch(`${API_BASE}/api/billing/schedules`);
const data = await res.json();
let html = '<table><thead><tr><th>Avtal</th><th>Typ</th><th>Frekvens</th><th>Nästa körning</th><th>Status</th></tr></thead><tbody>';
data.schedules?.forEach(s => {
html += `<tr>
<td>${s.agreement_title}</td>
<td>${s.schedule_type}</td>
<td>${s.frequency}</td>
<td>${s.next_run_date}</td>
<td><span class="badge badge-${s.is_active ? 'success' : 'gray'}">${s.is_active ? 'Aktiv' : 'Pausad'}</span></td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('schedules-table').innerHTML = html;
} catch (e) {
document.getElementById('schedules-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
// ═══════════════════════════════════════════════════════════════
// QIXZOOM
// ═══════════════════════════════════════════════════════════════
async function loadMissions() {
try {
const res = await fetch(`${API_BASE}/api/v1/quixzoom/missions`);
const data = await res.json();
let html = '<table><thead><tr><th>ID</th><th>Titel</th><th>Status</th><th>Belöning</th><th>Skapad</th></tr></thead><tbody>';
data.missions?.forEach(m => {
html += `<tr>
<td>${m.id.substring(0,8)}...</td>
<td>${m.title}</td>
<td><span class="badge badge-success">${m.status}</span></td>
<td>${m.reward_sek} kr</td>
<td>${new Date(m.created_at).toLocaleDateString('sv-SE')}</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('missions-table').innerHTML = html;
} catch (e) {
document.getElementById('missions-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function loadContributors() {
try {
const res = await fetch(`${API_BASE}/api/v1/quixzoom/contributors`);
const data = await res.json();
let html = '<table><thead><tr><th>Namn</th><th>Email</th><th>Status</th><th>Submissions</th><th>Tjänat</th></tr></thead><tbody>';
data.contributors?.forEach(c => {
html += `<tr>
<td>${c.name || '-'}</td>
<td>${c.email}</td>
<td><span class="badge badge-success">${c.status}</span></td>
<td>${c.submission_count}</td>
<td>${c.total_earnings} kr</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('contributors-table').innerHTML = html;
} catch (e) {
document.getElementById('contributors-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
// ═══════════════════════════════════════════════════════════════
// MAIL
// ═══════════════════════════════════════════════════════════════
async function loadMail() {
try {
const statsRes = await fetch(`${API_BASE}/api/mail/stats`);
const statsData = await statsRes.json();
document.getElementById('mail-inbox').textContent = statsData.inbox_total || '-';
document.getElementById('mail-unread').textContent = statsData.unread_count || '-';
document.getElementById('mail-sent').textContent = statsData.sent_total || '-';
// Get quiXzoom count
const qxRes = await fetch(`${API_BASE}/api/mail/messages?source=quixzoom_waitlist&limit=1`);
const qxData = await qxRes.json();
document.getElementById('mail-quixzoom').textContent = qxData.messages?.length || 0;
// Build query params
const sourceFilter = document.getElementById('mail-source-filter')?.value || '';
const statusFilter = document.getElementById('mail-status-filter')?.value || '';
let url = `${API_BASE}/api/mail/messages?folder=inbox`;
if (sourceFilter) url += `&source=${sourceFilter}`;
if (statusFilter) url += `&status=${statusFilter}`;
const msgRes = await fetch(url);
const msgData = await msgRes.json();
let html = '<table><thead><tr><th>Från</th><th>Ämne</th><th>Källa</th><th>Status</th><th>Datum</th><th>Åtgärder</th></tr></thead><tbody>';
msgData.messages?.forEach(m => {
const sourceBadge = {
'quixzoom_waitlist': '<span class="badge badge-success">quiXzoom</span>',
'landvex_contact': '<span class="badge badge-blue">Landvex</span>',
'aamos_contact': '<span class="badge badge-warning">AAMOS</span>',
'manual': '<span class="badge badge-gray">Manuellt</span>'
}[m.source] || `<span class="badge badge-gray">${m.source}</span>`;
html += `<tr class="${m.status === 'unread' ? 'mail-unread' : ''}">
<td><strong>${m.from_name || m.from_email}</strong><br><small style="color: var(--text-muted)">${m.from_email}</small></td>
<td>${m.subject}<br><small style="color: var(--text-muted)">${m.body?.substring(0, 50)}...</small></td>
<td>${sourceBadge}</td>
<td><span class="badge ${m.status === 'read' ? 'badge-success' : m.status === 'replied' ? 'badge-blue' : 'badge-warning'}">${m.status}</span></td>
<td>${new Date(m.created_at).toLocaleDateString('sv-SE')}</td>
<td>
<button class="btn btn-sm btn-outline" onclick="viewMail('${m.id}')">Visa</button>
${m.status !== 'replied' ? `<button class="btn btn-sm btn-primary" onclick="replyMail('${m.id}')">Svara</button>` : ''}
${!m.lead_id ? `<button class="btn btn-sm btn-success" onclick="convertMailToLead('${m.id}')">→ Lead</button>` : ''}
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('mail-messages').innerHTML = html;
} catch (e) {
document.getElementById('mail-messages').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function viewMail(messageId) {
try {
const res = await fetch(`${API_BASE}/api/mail/messages/${messageId}`);
const data = await res.json();
alert(`Från: ${data.message.from_email}\nÄmne: ${data.message.subject}\n\n${data.message.body}`);
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function replyMail(messageId) {
const reply = prompt('Skriv ditt svar:');
if (!reply) return;
try {
const res = await fetch(`${API_BASE}/api/mail/messages/${messageId}/reply`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: reply })
});
if (res.ok) {
loadMail();
showToast('Svar skickat!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function convertMailToLead(messageId) {
try {
const res = await fetch(`${API_BASE}/api/mail/messages/${messageId}/convert-to-lead`, {
method: 'POST'
});
if (res.ok) {
loadMail();
showToast('Konverterat till lead!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function sendMail() {
const to = document.getElementById('compose-to').value;
const subject = document.getElementById('compose-subject').value;
const body = document.getElementById('compose-body').value;
try {
const res = await fetch(`${API_BASE}/api/mail/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to, subject, body })
});
if (res.ok) {
hideModal('compose-mail-modal');
loadMail();
showToast('Mail skickat!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// SUPPORT
// ═══════════════════════════════════════════════════════════════
async function loadSupport() {
try {
// Dashboard stats
const dashRes = await fetch(`${API_BASE}/api/support/dashboard`);
const dashData = await dashRes.json();
document.getElementById('stat-tickets-open').textContent = dashData.open_tickets || 0;
document.getElementById('stat-tickets-urgent').textContent = dashData.urgent_tickets || 0;
document.getElementById('stat-tickets-today').textContent = dashData.tickets_today || 0;
document.getElementById('stat-response-time').textContent = (dashData.avg_response_hours || 0).toFixed(1) + 'h';
// Tickets
const statusFilter = document.getElementById('ticket-status-filter')?.value || '';
const priorityFilter = document.getElementById('ticket-priority-filter')?.value || '';
let url = `${API_BASE}/api/support/tickets`;
if (statusFilter) url += `?status=${statusFilter}`;
if (priorityFilter) url += `${statusFilter ? '&' : '?'}priority=${priorityFilter}`;
const res = await fetch(url);
const data = await res.json();
let html = '<table><thead><tr><th>Ärendenr</th><th>Kund</th><th>Ämne</th><th>Prioritet</th><th>Status</th><th>Tilldelad</th><th>Åtgärder</th></tr></thead><tbody>';
data.tickets?.forEach(t => {
html += `<tr>
<td>${t.ticket_number}</td>
<td>${t.customer_name || '-'}</td>
<td>${t.subject}</td>
<td><span class="badge badge-${t.priority === 'urgent' ? 'danger' : t.priority === 'high' ? 'warning' : 'success'}">${t.priority}</span></td>
<td><span class="badge badge-${t.status === 'open' ? 'warning' : t.status === 'in_progress' ? 'blue' : t.status === 'closed' ? 'success' : 'gray'}">${t.status}</span></td>
<td>${t.assignee_name || '-'}</td>
<td>
<button class="btn btn-sm btn-outline" onclick="viewTicket('${t.id}')">Visa</button>
${t.status !== 'closed' ? `<button class="btn btn-sm btn-primary" onclick="assignTicket('${t.id}')">Tilldela</button>` : ''}
<button class="btn btn-sm btn-warning" onclick="showShareTicketModal('${t.id}', '${t.ticket_number}')">Dela</button>
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('tickets-table').innerHTML = html;
} catch (e) {
document.getElementById('tickets-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function loadFAQ() {
try {
const res = await fetch(`${API_BASE}/api/support/faq`);
const data = await res.json();
let html = '<table><thead><tr><th>Fråga</th><th>Kategori</th><th>Visningar</th><th>Åtgärder</th></tr></thead><tbody>';
data.faq?.forEach(f => {
html += `<tr>
<td><strong>${f.question}</strong><br><small style="color: var(--text-muted)">${f.answer?.substring(0, 80)}...</small></td>
<td>${f.category}</td>
<td>${f.views}</td>
<td>
<button class="btn btn-sm btn-outline" onclick="editFAQ('${f.id}')">Redigera</button>
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('faq-table').innerHTML = html;
} catch (e) {
document.getElementById('faq-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
// ═══════════════════════════════════════════════════════════════
// HR
// ═══════════════════════════════════════════════════════════════
async function loadEmployees() {
try {
// Dashboard stats
const dashRes = await fetch(`${API_BASE}/api/hr/dashboard`);
const dashData = await dashRes.json();
document.getElementById('stat-employees-total').textContent = dashData.total_employees || 0;
document.getElementById('stat-employees-new').textContent = dashData.new_this_month || 0;
document.getElementById('stat-contracts-ending').textContent = dashData.contracts_ending_soon || 0;
document.getElementById('stat-salary-total').textContent = formatCurrency(dashData.total_salary_this_month || 0);
// Employees
const res = await fetch(`${API_BASE}/api/hr/employees`);
const data = await res.json();
let html = '<table><thead><tr><th>Namn</th><th>E-post</th><th>Avdelning</th><th>Position</th><th>Startdatum</th><th>Status</th><th>Åtgärder</th></tr></thead><tbody>';
data.employees?.forEach(e => {
html += `<tr>
<td><strong>${e.first_name} ${e.last_name}</strong></td>
<td>${e.email}</td>
<td>${e.department_name || '-'}</td>
<td>${e.position || '-'}</td>
<td>${e.start_date}</td>
<td><span class="badge badge-${e.status === 'active' ? 'success' : 'warning'}">${e.status}</span></td>
<td>
<button class="btn btn-sm btn-outline" onclick="viewEmployee('${e.id}')">Visa</button>
<button class="btn btn-sm btn-primary" onclick="createContract('${e.id}')">Nytt avtal</button>
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('employees-table').innerHTML = html;
} catch (e) {
document.getElementById('employees-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function loadContracts() {
try {
const res = await fetch(`${API_BASE}/api/hr/contracts`);
const data = await res.json();
let html = '<table><thead><tr><th>Avtalsnr</th><th>Anställd</th><th>Typ</th><th>Period</th><th>Lön</th><th>Status</th><th>Åtgärder</th></tr></thead><tbody>';
data.contracts?.forEach(c => {
html += `<tr>
<td>${c.contract_number}</td>
<td>${c.first_name} ${c.last_name}</td>
<td>${c.contract_type}</td>
<td>${c.start_date} - ${c.end_date || 'Löpande'}</td>
<td>${formatCurrency(c.salary)}</td>
<td><span class="badge badge-${c.status === 'active' ? 'success' : 'gray'}">${c.status}</span></td>
<td>
<button class="btn btn-sm btn-outline" onclick="viewContract('${c.id}')">Visa</button>
${c.status === 'active' ? `<button class="btn btn-sm btn-danger" onclick="terminateContract('${c.id}')">Avsluta</button>` : ''}
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('contracts-table').innerHTML = html;
} catch (e) {
document.getElementById('contracts-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function loadSalary() {
try {
const res = await fetch(`${API_BASE}/api/hr/salary`);
const data = await res.json();
let html = '<table><thead><tr><th>Anställd</th><th>År</th><th>Månad</th><th>Grundlön</th><th>Övertid</th><th>Bonus</th><th>Skatt</th><th>Netto</th></tr></thead><tbody>';
data.salaries?.forEach(s => {
html += `<tr>
<td>${s.first_name} ${s.last_name}</td>
<td>${s.year}</td>
<td>${s.month}</td>
<td>${formatCurrency(s.base_salary)}</td>
<td>${formatCurrency(s.overtime)}</td>
<td>${formatCurrency(s.bonus)}</td>
<td>${formatCurrency(s.tax)}</td>
<td><strong>${formatCurrency(s.net_salary)}</strong></td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('salary-table').innerHTML = html;
} catch (e) {
document.getElementById('salary-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function loadDocuments() {
try {
const res = await fetch(`${API_BASE}/api/hr/documents`);
const data = await res.json();
let html = '<table><thead><tr><th>Anställd</th><th>Typ</th><th>Titel</th><th>Giltig till</th><th>Åtgärder</th></tr></thead><tbody>';
data.documents?.forEach(d => {
html += `<tr>
<td>${d.first_name} ${d.last_name}</td>
<td>${d.document_type}</td>
<td>${d.title}</td>
<td>${d.expiry_date || '-'}</td>
<td>
${d.file_url ? `<a href="${d.file_url}" target="_blank" class="btn btn-sm btn-outline">Öppna</a>` : ''}
</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('documents-table').innerHTML = html;
} catch (e) {
document.getElementById('documents-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
// ═══════════════════════════════════════════════════════════════
// HEALTH
// ═══════════════════════════════════════════════════════════════
async function loadHealth() {
try {
const res = await fetch(`${API_BASE}/api/v1/system/services`);
const data = await res.json();
let html = '<table><thead><tr><th>Tjänst</th><th>Port</th><th>Status</th><th>Senast kontrollerad</th></tr></thead><tbody>';
data.services?.forEach(s => {
html += `<tr>
<td>${s.name}</td>
<td>${s.port}</td>
<td><span class="badge ${s.status === 'running' ? 'badge-success' : 'badge-danger'}">${s.status}</span></td>
<td>${s.last_check || '-'}</td>
</tr>`;
});
html += '</tbody></table>';
document.getElementById('health-table').innerHTML = html;
} catch (e) {
document.getElementById('health-table').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
// ═══════════════════════════════════════════════════════════════
// SETTINGS
// ═══════════════════════════════════════════════════════════════
function showSettingsTab(tab) {
document.querySelectorAll('.settings-tab').forEach(t => t.classList.add('hidden'));
document.getElementById(`settings-${tab}`).classList.remove('hidden');
document.querySelectorAll('#settings .tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
}
async function saveSettings() {
const settings = {
company_name: document.getElementById('setting-company-name').value,
org_nr: document.getElementById('setting-org-nr').value,
vat_nr: document.getElementById('setting-vat-nr').value,
payment_terms_days: parseInt(document.getElementById('setting-payment-terms').value),
address: document.getElementById('setting-address').value,
postal_code: document.getElementById('setting-postal').value,
city: document.getElementById('setting-city').value
};
try {
const res = await fetch(`${API_BASE}/api/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
if (res.ok) {
showToast('Inställningar sparade!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function saveModuleSettings() {
const settings = {
crm: {
auto_convert_leads: document.getElementById('crm-auto-convert').checked,
default_customer_type: document.getElementById('crm-default-type').value,
lead_assignment: document.getElementById('crm-assignment').value
},
billing: {
default_vat_rate: parseFloat(document.getElementById('billing-default-vat').value),
reminder_fee: parseFloat(document.getElementById('billing-reminder-fee').value),
interest_rate: parseFloat(document.getElementById('billing-interest-rate').value)
},
support: {
sla_response_hours: parseInt(document.getElementById('support-sla-response').value),
sla_resolution_hours: parseInt(document.getElementById('support-sla-resolution').value),
auto_assign: document.getElementById('support-auto-assign').checked
},
hr: {
probation_months: parseInt(document.getElementById('hr-probation').value),
vacation_days: parseInt(document.getElementById('hr-vacation').value),
notice_period_months: parseInt(document.getElementById('hr-notice').value)
}
};
try {
for (const [module, config] of Object.entries(settings)) {
await fetch(`${API_BASE}/api/settings/modules/${module}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
}
showToast('Modulinställningar sparade!');
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function saveNotificationSettings() {
const settings = {
email_notifications: document.getElementById('notif-email').checked,
push_notifications: document.getElementById('notif-push').checked,
notify_on_new_lead: document.getElementById('notif-new-lead').checked,
notify_on_new_invoice: document.getElementById('notif-new-invoice').checked,
notify_on_overdue_invoice: document.getElementById('notif-overdue-invoice').checked,
notify_on_new_ticket: document.getElementById('notif-new-ticket').checked,
notify_on_ticket_assigned: document.getElementById('notif-ticket-assigned').checked,
notify_on_new_employee: document.getElementById('notif-new-employee').checked,
notify_on_contract_ending: document.getElementById('notif-contract-ending').checked,
daily_summary: document.getElementById('notif-daily-summary').checked,
weekly_summary: document.getElementById('notif-weekly-summary').checked
};
try {
const res = await fetch(`${API_BASE}/api/notifications/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
if (res.ok) {
showToast('Notifieringsinställningar sparade!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function saveIntegrationSettings() {
const settings = {
quixzoom_url: document.getElementById('integration-quixzoom-url').value,
quixzoom_key: document.getElementById('integration-quixzoom-key').value,
landvex_url: document.getElementById('integration-landvex-url').value,
landvex_key: document.getElementById('integration-landvex-key').value,
smtp_host: document.getElementById('integration-smtp-host').value,
smtp_port: parseInt(document.getElementById('integration-smtp-port').value),
smtp_user: document.getElementById('integration-smtp-user').value,
smtp_pass: document.getElementById('integration-smtp-pass').value
};
try {
const res = await fetch(`${API_BASE}/api/settings/integrations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
if (res.ok) {
showToast('Integrationsinställningar sparade!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// DROPDOWN HELPERS
// ═══════════════════════════════════════════════════════════════
async function loadEmployeeSelect(selectId = 'contract-employee') {
try {
const res = await fetch(`${API_BASE}/api/hr/employees?status=active`);
const data = await res.json();
const select = document.getElementById(selectId);
if (!select) return;
select.innerHTML = '<option value="">Välj anställd...</option>';
data.employees?.forEach(e => {
select.innerHTML += `<option value="${e.id}">${e.first_name} ${e.last_name}</option>`;
});
} catch (e) { console.error('Load employees error:', e); }
}
async function loadDepartmentSelect() {
try {
const res = await fetch(`${API_BASE}/api/hr/departments`);
const data = await res.json();
const select = document.getElementById('emp-department');
if (!select) return;
select.innerHTML = '<option value="">Välj avdelning...</option>';
data.departments?.forEach(d => {
select.innerHTML += `<option value="${d.id}">${d.name}</option>`;
});
} catch (e) { console.error('Load departments error:', e); }
}
// ═══════════════════════════════════════════════════════════════
// UTILITIES
// ═══════════════════════════════════════════════════════════════
function formatCurrency(amount) {
if (amount === undefined || amount === null) return '-';
return new Intl.NumberFormat('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount) + ' kr';
}
function showToast(message) {
const toast = document.createElement('div');
toast.style.cssText = 'position: fixed; bottom: 2rem; right: 2rem; background: var(--success); color: white; padding: 1rem 1.5rem; border-radius: var(--radius-sm); z-index: 10000; animation: fadeIn 0.3s ease;';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// ═══════════════════════════════════════════════════════════════
// CHAT
// ═══════════════════════════════════════════════════════════════
let currentChannelId = null;
async function loadChat() {
await loadChatChannels();
await loadChatUnread();
}
async function loadChatChannels() {
try {
const res = await fetch(`${API_BASE}/api/chat/channels`);
const data = await res.json();
const channelsDiv = document.getElementById('chat-channels');
channelsDiv.innerHTML = '';
data.channels?.forEach(c => {
const unreadBadge = c.unread_count > 0 ? `<span class="badge badge-danger">${c.unread_count}</span>` : '';
channelsDiv.innerHTML += `
<div class="chat-channel-item" onclick="selectChannel('${c.id}', '${c.name}', '${c.description || ''}')" style="padding: 0.5rem; border-radius: var(--radius-sm); cursor: pointer; ${currentChannelId === c.id ? 'background: var(--primary);' : 'hover: background: var(--card-hover);'}">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span><strong>#</strong> ${c.name}</span>
${unreadBadge}
</div>
<small style="color: var(--text-muted);">${c.message_count || 0} meddelanden</small>
</div>
`;
});
} catch (e) {
console.error('Load chat channels error:', e);
}
}
async function selectChannel(channelId, name, description) {
currentChannelId = channelId;
document.getElementById('chat-channel-name').textContent = '#' + name;
document.getElementById('chat-channel-desc').textContent = description;
document.getElementById('chat-input-area').style.display = 'block';
// Load messages
await loadChatMessages(channelId);
// Mark as read
await fetch(`${API_BASE}/api/chat/channels/${channelId}/read`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: 'current_user' })
});
// Refresh unread counts
await loadChatUnread();
}
async function loadChatMessages(channelId) {
try {
const res = await fetch(`${API_BASE}/api/chat/channels/${channelId}/messages`);
const data = await res.json();
const messagesDiv = document.getElementById('chat-messages');
messagesDiv.innerHTML = '';
if (data.messages?.length === 0) {
messagesDiv.innerHTML = '<p style="color: var(--text-muted); text-align: center;">Inga meddelanden än</p>';
return;
}
data.messages?.forEach(m => {
const isTicketShare = m.message_type === 'ticket_share';
const isSystem = m.message_type === 'system';
messagesDiv.innerHTML += `
<div style="padding: 0.75rem; background: ${isSystem ? 'rgba(59, 130, 246, 0.1)' : 'var(--bg)'}; border-radius: var(--radius-sm); border-left: 3px solid ${isTicketShare ? 'var(--warning)' : isSystem ? 'var(--info)' : 'var(--primary)'};">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div>
<strong>${m.author_name || 'System'}</strong>
<small style="color: var(--text-muted); margin-left: 0.5rem;">${new Date(m.created_at).toLocaleTimeString('sv-SE')}</small>
</div>
${isTicketShare ? '<span class="badge badge-warning">Ärende</span>' : ''}
</div>
<div style="margin-top: 0.5rem; white-space: pre-wrap;">${m.body}</div>
</div>
`;
});
// Scroll to bottom
messagesDiv.scrollTop = messagesDiv.scrollHeight;
} catch (e) {
console.error('Load chat messages error:', e);
}
}
async function sendChatMessage() {
const input = document.getElementById('chat-input');
const body = input.value.trim();
if (!body || !currentChannelId) return;
try {
await fetch(`${API_BASE}/api/chat/channels/${currentChannelId}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
body,
author_id: 'current_user',
message_type: 'text'
})
});
input.value = '';
await loadChatMessages(currentChannelId);
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function loadChatUnread() {
try {
const res = await fetch(`${API_BASE}/api/chat/unread?user_id=current_user`);
const data = await res.json();
const totalUnread = data.unread?.reduce((sum, u) => sum + parseInt(u.unread_count), 0) || 0;
const badge = document.getElementById('chat-badge');
if (badge && totalUnread > 0) {
badge.textContent = totalUnread;
badge.style.display = 'inline-block';
} else if (badge) {
badge.style.display = 'none';
}
} catch (e) {
console.error('Load chat unread error:', e);
}
}
async function saveChannel() {
const channel = {
name: document.getElementById('channel-name').value,
description: document.getElementById('channel-desc').value,
type: document.getElementById('channel-type').value
};
try {
const res = await fetch(`${API_BASE}/api/chat/channels`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(channel)
});
if (res.ok) {
hideModal('channel-modal');
loadChatChannels();
showToast('Kanal skapad!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// Ticket sharing
function showShareTicketModal(ticketId, ticketNumber) {
document.getElementById('share-ticket-id').value = ticketId;
document.getElementById('share-ticket-display').value = ticketNumber;
loadChannelSelect('share-ticket-channel');
showModal('share-ticket-modal');
}
async function loadChannelSelect(selectId) {
try {
const res = await fetch(`${API_BASE}/api/chat/channels`);
const data = await res.json();
const select = document.getElementById(selectId);
select.innerHTML = '<option value="">Välj kanal...</option>';
data.channels?.forEach(c => {
select.innerHTML += `<option value="${c.id}">#${c.name}</option>`;
});
} catch (e) {
console.error('Load channels error:', e);
}
}
async function shareTicket() {
const share = {
ticket_id: document.getElementById('share-ticket-id').value,
target_channel_id: document.getElementById('share-ticket-channel').value,
note: document.getElementById('share-ticket-note').value,
shared_by: 'current_user'
};
try {
const res = await fetch(`${API_BASE}/api/chat/share-ticket`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(share)
});
if (res.ok) {
hideModal('share-ticket-modal');
showToast('Ärende delat!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// Poll for new messages every 10 seconds when in chat
setInterval(() => {
if (currentChannelId) {
loadChatMessages(currentChannelId);
loadChatUnread();
}
}, 10000);
// ═══════════════════════════════════════════════════════════════
// NOTIFICATIONS
// ═══════════════════════════════════════════════════════════════
async function loadNotifications() {
try {
const filter = document.getElementById('notif-filter')?.value || '';
let url = `${API_BASE}/api/notifications`;
if (filter === 'unread') url += '?unread_only=true';
const res = await fetch(url);
const data = await res.json();
// Update badge
const badge = document.getElementById('notif-badge');
if (badge && data.unread_count > 0) {
badge.textContent = data.unread_count;
badge.style.display = 'inline-block';
} else if (badge) {
badge.style.display = 'none';
}
let html = '<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
data.notifications?.forEach(n => {
const icon = {
'lead': '🎯',
'invoice': '📄',
'payment': '💰',
'ticket': '🎫',
'employee': '👔',
'contract': '📋',
'system': '⚙️'
}[n.type] || '📢';
html += `<div class="${n.read_at ? '' : 'mail-unread'}" style="padding: 1rem; border-radius: var(--radius-sm); cursor: pointer;" onclick="viewNotification('${n.id}', '${n.entity_type}', '${n.entity_id}')">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div style="display: flex; gap: 0.75rem; align-items: start;">
<span style="font-size: 1.5rem;">${icon}</span>
<div>
<strong>${n.title}</strong>
<p style="color: var(--text-muted); margin-top: 0.25rem;">${n.message}</p>
<small style="color: var(--text-muted);">${n.entity_name || ''}${new Date(n.created_at).toLocaleString('sv-SE')}</small>
</div>
</div>
${!n.read_at ? '<span class="badge badge-danger">Ny</span>' : ''}
</div>
</div>`;
});
html += '</div>';
document.getElementById('notifications-list').innerHTML = html;
} catch (e) {
document.getElementById('notifications-list').innerHTML = `<p class="error">Fel: ${e.message}</p>`;
}
}
async function viewNotification(notifId, entityType, entityId) {
// Mark as read
await fetch(`${API_BASE}/api/notifications/${notifId}/read`, { method: 'POST' });
// Navigate to relevant section
switch(entityType) {
case 'customer': showSection('customers'); break;
case 'invoice': showSection('invoices'); break;
case 'ticket': showSection('support'); break;
case 'lead': showSection('leads'); break;
case 'employee': showSection('employees'); break;
default: loadNotifications();
}
}
async function markAllNotificationsRead() {
try {
await fetch(`${API_BASE}/api/notifications/read-all`, { method: 'POST' });
loadNotifications();
showToast('Alla notiser markerade som lästa!');
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// REPORTS
// ═══════════════════════════════════════════════════════════════
function showReportTab(tab) {
document.querySelectorAll('.report-tab').forEach(t => t.classList.add('hidden'));
document.getElementById(`report-${tab}`).classList.remove('hidden');
document.querySelectorAll('#reports .tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
}
async function loadReports() {
// Reports are static, just ensure tabs work
}
async function generateReport(type) {
try {
let endpoint;
switch(type) {
case 'customer-growth': endpoint = '/api/reports/crm/customer-growth'; break;
case 'lead-conversion': endpoint = '/api/reports/crm/lead-conversion'; break;
case 'revenue': endpoint = '/api/reports/billing/revenue'; break;
case 'aging': endpoint = '/api/reports/billing/aging'; break;
case 'support-performance': endpoint = '/api/reports/support/performance'; break;
case 'support-satisfaction': endpoint = '/api/reports/support/satisfaction'; break;
case 'headcount': endpoint = '/api/reports/hr/headcount'; break;
case 'salary-distribution': endpoint = '/api/reports/hr/salary-distribution'; break;
default: endpoint = `/api/ledger/reports/${type}`;
}
const res = await fetch(`${API_BASE}${endpoint}`);
const data = await res.json();
document.getElementById('report-output').classList.remove('hidden');
document.getElementById('report-content').innerHTML = `
<h3>${data.title}</h3>
<p style="color: var(--text-muted);">Genererad: ${new Date().toLocaleString('sv-SE')}</p>
<div style="margin-top: 1rem;">
<table>
<thead>
<tr>${Object.keys(data.data?.[0] || {}).map(k => `<th>${k}</th>`).join('')}</tr>
</thead>
<tbody>
${data.data?.map(row => `<tr>${Object.values(row).map(v => `<td>${v}</td>`).join('')}</tr>`).join('')}
</tbody>
</table>
</div>
<button class="btn btn-primary mt-2" onclick="exportReport('${type}')">Exportera CSV</button>
`;
} catch (e) {
alert('Fel: ' + e.message);
}
}
function exportReport(type) {
window.open(`${API_BASE}/api/reports/${type}/csv`, '_blank');
}
// ═══════════════════════════════════════════════════════════════
// SUPPORT FUNCTIONS
// ═══════════════════════════════════════════════════════════════
async function saveTicket() {
const ticket = {
customer_id: document.getElementById('ticket-customer').value,
subject: document.getElementById('ticket-subject').value,
description: document.getElementById('ticket-description').value,
priority: document.getElementById('ticket-priority').value,
category: document.getElementById('ticket-category').value
};
try {
const res = await fetch(`${API_BASE}/api/support/tickets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ticket)
});
if (res.ok) {
hideModal('ticket-modal');
loadSupport();
showToast('Ärende skapat!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function viewTicket(ticketId) {
try {
const res = await fetch(`${API_BASE}/api/support/tickets/${ticketId}`);
const data = await res.json();
let msgHtml = '';
data.messages?.forEach(m => {
msgHtml += `<div style="margin-bottom: 1rem; padding: 0.75rem; background: var(--bg); border-radius: var(--radius-sm);">
<strong>${m.author_name || 'Kund'}</strong> <small style="color: var(--text-muted)">${new Date(m.created_at).toLocaleString('sv-SE')}</small>
<p style="margin-top: 0.5rem;">${m.body}</p>
</div>`;
});
alert(`Ärende: ${data.ticket.ticket_number}\nKund: ${data.ticket.customer_name}\nÄmne: ${data.ticket.subject}\n\nMeddelanden:\n${msgHtml}`);
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function assignTicket(ticketId) {
const assigneeId = prompt('Ange handläggare-ID:');
if (!assigneeId) return;
try {
const res = await fetch(`${API_BASE}/api/support/tickets/${ticketId}/assign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ assignee_id: assigneeId })
});
if (res.ok) {
loadSupport();
showToast('Ärende tilldelat!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function saveFAQ() {
const faq = {
question: document.getElementById('faq-question').value,
answer: document.getElementById('faq-answer').value,
category: document.getElementById('faq-category').value
};
try {
const res = await fetch(`${API_BASE}/api/support/faq`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(faq)
});
if (res.ok) {
hideModal('faq-modal');
loadFAQ();
showToast('FAQ sparad!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// HR FUNCTIONS
// ═══════════════════════════════════════════════════════════════
async function saveEmployee() {
const employee = {
first_name: document.getElementById('emp-first-name').value,
last_name: document.getElementById('emp-last-name').value,
email: document.getElementById('emp-email').value,
phone: document.getElementById('emp-phone').value,
personal_number: document.getElementById('emp-personal-number').value,
address: document.getElementById('emp-address').value,
postal_code: document.getElementById('emp-postal').value,
city: document.getElementById('emp-city').value,
department_id: document.getElementById('emp-department').value,
position: document.getElementById('emp-position').value,
start_date: document.getElementById('emp-start-date').value,
employment_type: document.getElementById('emp-type').value,
salary: parseFloat(document.getElementById('emp-salary').value)
};
try {
const res = await fetch(`${API_BASE}/api/hr/employees`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(employee)
});
if (res.ok) {
hideModal('employee-modal');
loadEmployees();
showToast('Anställd sparad!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function viewEmployee(employeeId) {
try {
const res = await fetch(`${API_BASE}/api/hr/employees/${employeeId}`);
const data = await res.json();
alert(`Namn: ${data.employee.first_name} ${data.employee.last_name}\nE-post: ${data.employee.email}\nPosition: ${data.employee.position}\nStartdatum: ${data.employee.start_date}\n\nAvtal: ${data.contracts.length}\nDokument: ${data.documents.length}`);
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function createContract(employeeId) {
showModal('contract-modal');
setTimeout(() => {
document.getElementById('contract-employee').value = employeeId;
}, 100);
}
async function saveContract() {
const contract = {
employee_id: document.getElementById('contract-employee').value,
contract_type: document.getElementById('contract-type').value,
start_date: document.getElementById('contract-start').value,
end_date: document.getElementById('contract-end').value,
salary: parseFloat(document.getElementById('contract-salary').value),
working_hours: parseFloat(document.getElementById('contract-hours').value),
probation_months: parseInt(document.getElementById('contract-probation').value)
};
try {
const res = await fetch(`${API_BASE}/api/hr/contracts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(contract)
});
if (res.ok) {
hideModal('contract-modal');
loadContracts();
showToast('Avtal sparat!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function terminateContract(contractId) {
const reason = prompt('Anledning till avslut:');
if (!reason) return;
try {
const res = await fetch(`${API_BASE}/api/hr/contracts/${contractId}/terminate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ end_date: new Date().toISOString().split('T')[0], reason })
});
if (res.ok) {
loadContracts();
showToast('Avtal avslutat!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function saveSalary() {
const salary = {
employee_id: document.getElementById('salary-employee').value,
year: parseInt(document.getElementById('salary-year').value),
month: parseInt(document.getElementById('salary-month').value),
base_salary: parseFloat(document.getElementById('salary-base').value),
overtime: parseFloat(document.getElementById('salary-overtime').value),
bonus: parseFloat(document.getElementById('salary-bonus').value),
deductions: parseFloat(document.getElementById('salary-deductions').value),
tax: parseFloat(document.getElementById('salary-tax').value),
net_salary: parseFloat(document.getElementById('salary-net').value)
};
try {
const res = await fetch(`${API_BASE}/api/hr/salary`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(salary)
});
if (res.ok) {
hideModal('salary-modal');
loadSalary();
showToast('Lön registrerad!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
async function saveDocument() {
const document = {
employee_id: document.getElementById('doc-employee').value,
document_type: document.getElementById('doc-type').value,
title: document.getElementById('doc-title').value,
file_url: document.getElementById('doc-url').value,
expiry_date: document.getElementById('doc-expiry').value
};
try {
const res = await fetch(`${API_BASE}/api/hr/documents`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(document)
});
if (res.ok) {
hideModal('document-modal');
loadDocuments();
showToast('Dokument sparat!');
}
} catch (e) {
alert('Fel: ' + e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// INITIAL LOAD
// ═══════════════════════════════════════════════════════════════
loadDashboard();
loadNotifications();
// Poll for new notifications every 30 seconds
setInterval(() => {
loadNotifications();
}, 30000);
loadMissions();
loadContributors();
loadAccounts();
loadTrialBalance();
loadJournal();
loadPeriods();
loadMail();
loadHealth();
// Refresh every 30 seconds
setInterval(() => {
loadDashboard();
loadMissions();
loadMail();
}, 30000);
</script>
</body>
</html>