48ea61cdcc
- Product documentation in docs/products/ - Updated MEMORY.md with product info - quiXzoom Auth Core as AAMOS Identity product
4027 lines
161 KiB
HTML
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> |