Files
boc/backups/2026-07-12-1023/landvex-prod/admin-social-temp.html
T
Bernt 58ca4e68db feat(boc): Complete Business Operations Center v1.0
- Go backend API with full CRUD for all modules (CRM, Sales, Finance, HR, Legal, Marketing, Support, Purchase, Inventory, Projects, Automation, Analytics)
- Rust analytics service with parallel report generation
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables, full migrations
- Redis cache, sessions, pub/sub
- Kafka event streaming with Zookeeper
- WebSocket hub for real-time updates
- Automation engine with cron jobs, workflows, event triggers
- JWT authentication, multi-tenant from start
- Docker Compose with all services
- Nginx reverse proxy with rate limiting
- Integration tests passing
- Feature gap analysis against Fortnox/Odoo/Visma

Refs: BOC-001
2026-07-12 12:41:35 +00:00

1850 lines
99 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ARGUS — Social Media Admin</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #030712;
color: #e2e8f0;
min-height: 100vh;
font-size: 14px;
}
a { color: #6366f1; text-decoration: none; }
a:hover { text-decoration: underline; }
.top-nav {
background: #0d1117;
border-bottom: 1px solid #1e2737;
padding: 0 24px;
display: flex;
align-items: center;
gap: 32px;
height: 56px;
position: sticky;
top: 0;
z-index: 100;
}
.nav-logo { font-weight: 700; font-size: 18px; color: #6366f1; letter-spacing: -0.5px; }
.nav-links { display: flex; gap: 4px; }
.nav-link {
padding: 6px 14px; border-radius: var(--radius-md); color: #94a3b8;
font-size: 13px; font-weight: 500; cursor: pointer;
transition: all 0.15s; background: none; border: none;
}
.nav-link:hover { background: #1e2737; color: #e2e8f0; }
.nav-link.active { background: #1e2737; color: #6366f1; }
.nav-spacer { flex: 1; }
.nav-status { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #64748b; }
.status-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: #22c55e; }
.main-container { max-width: 1400px; margin: 0 auto; padding: 24px 24px 48px; }
.page-header { margin-bottom: 24px; }
.page-header h1 { font-size: 22px; font-weight: 700; color: #f1f5f9; margin-bottom: 4px; }
.page-header p { color: #64748b; font-size: 13px; }
.company-tabs { display: flex; gap: 4px; margin-bottom: 24px; flex-wrap: wrap; }
.company-tab {
padding: 8px 16px; border-radius: var(--radius-md); border: 1px solid #1e2737;
background: #0d1117; color: #94a3b8; font-size: 13px; font-weight: 500;
cursor: pointer; transition: all 0.15s;
}
.company-tab:hover { border-color: #6366f1; color: #e2e8f0; }
.company-tab.active { background: #1e1f4e; border-color: #6366f1; color: #818cf8; }
.main-tabs {
display: flex; gap: 0; border-bottom: 1px solid #1e2737; margin-bottom: 24px;
}
.main-tab {
padding: 10px 20px; border: none; background: none; color: #64748b;
font-size: 13px; font-weight: 500; cursor: pointer;
border-bottom: 2px solid transparent; transition: all 0.15s; margin-bottom: -1px;
}
.main-tab:hover { color: #e2e8f0; }
.main-tab.active { color: #818cf8; border-bottom-color: #6366f1; }
.tab-panel { display: none; }
.tab-panel.visible { display: block; }
.card { background: #0d1117; border: 1px solid #1e2737; border-radius: var(--radius-md); padding: 24px; }
.card + .card { margin-top: 16px; }
.card-title { font-size: 15px; font-weight: 600; color: #f1f5f9; margin-bottom: 16px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.form-group { display: flex; flex-direction: column; gap: 6px; }
.form-group label { font-size: 12px; font-weight: 500; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
.form-group input, .form-group select, .form-group textarea {
padding: 9px 12px; background: #030712; border: 1px solid #1e2737;
border-radius: var(--radius-md); color: #e2e8f0; font-size: 13px; width: 100%;
transition: border-color 0.15s; outline: none;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color: #6366f1; }
.form-group input::placeholder { color: #374151; }
.btn {
padding: 9px 18px; border-radius: var(--radius-md); border: none; font-size: 13px;
font-weight: 600; cursor: pointer; transition: all 0.15s;
display: inline-flex; align-items: center; gap: 6px;
}
.btn-primary { background: #6366f1; color: #fff; }
.btn-primary:hover { background: #4f46e5; }
.btn-secondary { background: #1e2737; color: #94a3b8; border: 1px solid #2d3748; }
.btn-secondary:hover { color: #e2e8f0; border-color: #4a5568; }
.btn-success { background: #16a34a; color: #fff; }
.btn-success:hover { background: #15803d; }
.btn-danger { background: #dc2626; color: #fff; }
.btn-danger:hover { background: #b91c1c; }
.btn-warning { background: #d97706; color: #fff; }
.btn-warning:hover { background: #b45309; }
.btn-ghost { background: none; border: none; color: #64748b; cursor: pointer; padding: 4px 8px; border-radius: var(--radius-sm); font-size: 13px; transition: all 0.15s; }
.btn-ghost:hover { background: #1e2737; color: #e2e8f0; }
.btn-sm { padding: 5px 12px; font-size: 12px; }
.btn-teal { background: #0d9488; color: #fff; }
.btn-teal:hover { background: #0f766e; }
.filter-row { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
.filter-btn { padding: 6px 14px; border-radius: var(--radius-xl); border: 1px solid #1e2737; background: none; color: #64748b; font-size: 12px; cursor: pointer; transition: all 0.15s; }
.filter-btn:hover { border-color: #6366f1; color: #818cf8; }
.filter-btn.active { background: #1e1f4e; border-color: #6366f1; color: #818cf8; }
.refresh-info { margin-left: auto; font-size: 12px; color: #475569; }
/* Approve-all banner */
.approve-all-banner {
display: none;
align-items: center;
justify-content: space-between;
background: #0f2818;
border: 1px solid #166534;
border-radius: var(--radius-md);
padding: 12px 18px;
margin-bottom: 16px;
gap: 12px;
flex-wrap: wrap;
}
.approve-all-banner.visible { display: flex; }
.approve-all-banner-text { color: #86efac; font-size: 13px; font-weight: 600; }
.jobs-table-wrap { overflow-x: auto; }
.jobs-table { width: 100%; border-collapse: collapse; }
.jobs-table th {
padding: 10px 12px; text-align: left; font-size: 11px; font-weight: 700;
color: #475569; text-transform: uppercase; letter-spacing: 0.5px;
border-bottom: 1px solid #1e2737; white-space: nowrap;
background: #0a0e1a;
}
.jobs-table td { padding: 12px; font-size: 13px; border-bottom: 1px solid #0f172a; vertical-align: top; }
.jobs-table tr:last-child td { border-bottom: none; }
.jobs-table tr:hover td { background: rgba(99,102,241,0.04); }
@keyframes pulse-bg {
0%,100% { background: rgba(34,197,94,0.04); }
50% { background: rgba(34,197,94,0.10); }
}
.row-awaiting { animation: pulse-bg 2s ease-in-out infinite; }
/* Larger, clearer badges */
.badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 12px; border-radius: var(--radius-xl); font-size: 12px; font-weight: 700;
white-space: nowrap; letter-spacing: 0.2px;
}
.badge-running { background: #1e3a5f; color: #60a5fa; border: 1px solid #1d4ed8; }
.badge-done { background: #14532d; color: #4ade80; border: 1px solid #16a34a; }
.badge-error { background: #450a0a; color: #f87171; border: 1px solid #dc2626; }
.badge-approval { background: #14532d; color: #86efac; border: 1px solid #22c55e; }
.badge-pending { background: #1e2737; color: #94a3b8; border: 1px solid #2d3748; }
.badge-cancelled{ background: #1e2737; color: #64748b; border: 1px solid #374151; }
.badge-scheduled{ background: #1e3a5f; color: #93c5fd; border: 1px solid #3b82f6; }
.badge-draft { background: #1e2737; color: #cbd5e1; border: 1px solid #334155; }
.badge-published{ background: #14532d; color: #4ade80; border: 1px solid #16a34a; }
.badge-failed { background: #450a0a; color: #f87171; border: 1px solid #dc2626; }
.badge-manual { background: #451a03; color: #fdba74; border: 1px solid #d97706; }
.badge-timeout { background: #1c1917; color: #78716c; border: 1px solid #44403c; }
.screenshot-thumb {
max-width: 80px; max-height: 60px; border-radius: var(--radius-sm); object-fit: cover;
border: 1px solid #1e2737; cursor: pointer;
transition: transform 0.15s, border-color 0.15s;
}
.screenshot-thumb:hover { transform: scale(1.05); border-color: #6366f1; }
.no-screenshot { color: #374151; font-size: 11px; }
/* Inline credentials for PENDING_MANUAL / MANUAL_REQUIRED */
.inline-creds {
background: #0a1628; border: 1px solid #1e3a5f; border-radius: var(--radius-md);
padding: 10px 12px; margin-top: 8px; display: flex; flex-direction: column; gap: 6px;
}
.inline-creds-title { font-size: 11px; font-weight: 700; color: #60a5fa; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
.cred-inline-row { display: flex; align-items: center; gap: 6px; font-size: 12px; flex-wrap: wrap; }
.cred-inline-label { color: #475569; min-width: 70px; }
.cred-inline-val { font-family: 'Monaco','Menlo',monospace; font-size: 12px; color: #cbd5e1; }
.pw-field { background: #030712; border: 1px solid #1e2737; border-radius: var(--radius-sm); padding: 3px 8px; color: #fbbf24; font-family: monospace; font-size: 12px; width: 160px; }
.copy-btn { background: #6366f1; color: #fff; border: none; border-radius: var(--radius-sm); padding: 3px 10px; cursor: pointer; font-size: 11px; font-weight: 600; transition: background 0.15s; }
.copy-btn:hover { background: #4f46e5; }
.reveal-btn { background: #1e2737; color: #94a3b8; border: 1px solid #2d3748; border-radius: var(--radius-sm); padding: 3px 8px; cursor: pointer; font-size: 11px; transition: all 0.15s; }
.reveal-btn:hover { background: #2d3748; color: #e2e8f0; }
/* Fetch-mail-code */
.mail-code-box {
background: #0a1628; border: 1px solid #1e3a5f; border-radius: var(--radius-md);
padding: 10px 12px; margin-top: 8px;
}
.mail-code-result {
display: none; background: #1e293b; border-radius: var(--radius-sm); padding: 8px 12px;
margin-top: 8px; font-size: 14px; font-weight: 700; color: #6ee7b7; letter-spacing: 2px;
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.mail-code-result.hidden { display: none !important; }
/* Credentials */
.creds-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 8px; }
.creds-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.creds-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; }
.cred-card { background: #030712; border: 1px solid #1e2737; border-radius: var(--radius-md); padding: 16px; }
.cred-platform { font-size: 14px; font-weight: 700; color: #f1f5f9; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.cred-rows { display: flex; flex-direction: column; gap: 8px; }
.cred-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: 12px; }
.cred-label { color: #475569; min-width: 72px; }
.cred-value { color: #cbd5e1; font-family: 'Monaco', 'Menlo', monospace; font-size: 12px; word-break: break-all; flex: 1; text-align: right; }
.pw-hidden { color: #e2e8f0; }
.security-banner {
background: #1a1f2e; border: 1px solid #2d3748; border-radius: var(--radius-md);
padding: 12px 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;
font-size: 12px; color: #94a3b8;
}
.security-banner .icon { font-size: 20px; }
/* Publishing */
.publish-grid { display: grid; grid-template-columns: 1fr 400px; gap: 24px; }
@media (max-width: 900px) { .publish-grid { grid-template-columns: 1fr; } }
.platform-checkboxes { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.platform-checkbox { display: flex; align-items: center; gap: 6px; }
.platform-checkbox input { width: auto; }
.platform-checkbox label { font-size: 13px; color: #cbd5e1; text-transform: none; font-weight: 400; letter-spacing: 0; }
.post-preview { background: #030712; border: 1px solid #1e2737; border-radius: var(--radius-md); padding: 16px; }
.preview-platform { font-size: 12px; font-weight: 600; color: #6366f1; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
.preview-content { font-size: 13px; color: #cbd5e1; white-space: pre-wrap; word-break: break-word; }
.post-queue-table { width: 100%; border-collapse: collapse; margin-top: 16px; }
.post-queue-table th { font-size: 11px; font-weight: 600; color: #475569; text-transform: uppercase; letter-spacing: 0.5px; padding: 8px 12px; border-bottom: 1px solid #1e2737; text-align: left; }
.post-queue-table td { padding: 10px 12px; font-size: 12px; border-bottom: 1px solid #0f172a; vertical-align: middle; }
.post-queue-table tr:hover td { background: rgba(99,102,241,0.04); }
.char-counter { font-size: 11px; color: #475569; text-align: right; margin-top: 4px; }
.char-counter.warn { color: #f59e0b; }
.char-counter.over { color: #ef4444; }
.schedule-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.schedule-toggle { display: flex; gap: 8px; }
.sched-btn { padding: 6px 14px; border-radius: var(--radius-md); border: 1px solid #1e2737; background: none; color: #64748b; font-size: 12px; cursor: pointer; transition: all 0.15s; }
.sched-btn.active { background: #1e1f4e; border-color: #6366f1; color: #818cf8; }
/* Action required column */
.action-required-cell { font-size: 12px; font-weight: 700; color: #f97316; }
.action-none { color: #374151; font-size: 12px; }
/* State */
.state-box { text-align: center; padding: 48px 24px; color: #475569; font-size: 14px; }
.state-box .icon { font-size: 36px; margin-bottom: 12px; }
.state-box p { margin-top: 4px; font-size: 13px; }
.spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid #1e2737; border-top-color: #6366f1; border-radius: var(--radius-full); animation: spin 0.8s linear infinite; margin-bottom: 12px; }
@keyframes spin { to { transform: rotate(360deg); } }
.toast-container { position: fixed; bottom: 24px; right: 24px; display: flex; flex-direction: column; gap: 8px; z-index: 999; }
.toast { padding: 10px 16px; border-radius: var(--radius-md); font-size: 13px; font-weight: 500; max-width: 320px; animation: slideIn 0.2s ease; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: none; opacity: 1; } }
.toast-success { background: #14532d; color: #4ade80; border: 1px solid #16a34a; }
.toast-error { background: #450a0a; color: #f87171; border: 1px solid #dc2626; }
.toast-info { background: #1e2737; color: #94a3b8; border: 1px solid #2d3748; }
.toast-warning { background: #451a03; color: #fdba74; border: 1px solid #d97706; }
/* Modal */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 500; display: none; align-items: center; justify-content: center; }
.modal-overlay.visible { display: flex; }
.modal { background: #0d1117; border: 1px solid #1e2737; border-radius: var(--radius-lg); padding: 32px; max-width: 480px; width: 90%; }
.modal h3 { font-size: 18px; font-weight: 700; color: #f1f5f9; margin-bottom: 12px; }
.modal p { color: #94a3b8; font-size: 14px; margin-bottom: 24px; }
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; }
.rotation-log { background: #030712; border: 1px solid #1e2737; border-radius: var(--radius-md); padding: 16px; margin-top: 16px; max-height: 300px; overflow-y: auto; }
.rotation-log-item { font-size: 12px; color: #64748b; padding: 6px 0; border-bottom: 1px solid #0f172a; display: flex; justify-content: space-between; gap: 8px; }
.rotation-log-item:last-child { border-bottom: none; }
</style>
</head>
<body>
<nav class="top-nav">
<div class="nav-logo"> ARGUS Social</div>
<div class="nav-links">
<button class="nav-link active">Admin</button>
</div>
<div class="nav-spacer"></div>
<div class="nav-status">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Ansluter...</span>
</div>
</nav>
<div class="toast-container" id="toastContainer"></div>
<!-- Rotate Confirm Modal -->
<div class="modal-overlay" id="rotateModal">
<div class="modal">
<h3><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Rotera lösenord</h3>
<p id="rotateModalText">Generera nya starka lösenord för alla plattformar? Detta kan inte ångras.</p>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeRotateModal()">Avbryt</button>
<button class="btn btn-warning" id="rotateConfirmBtn" onclick="confirmRotate()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Rotera nu</button>
</div>
</div>
</div>
<!-- Employee Offboard Modal -->
<div class="modal-overlay" id="offboardModal">
<div class="modal">
<h3><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Anställd slutar — Lösenordsrotation</h3>
<p>Detta roterar ALLA lösenord för ALLA bolag och ALLA plattformar. Använd vid anställds avslut.</p>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="document.getElementById('offboardModal').classList.remove('visible')">Avbryt</button>
<button class="btn btn-danger" onclick="confirmOffboard()"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Rotera ALLA nu</button>
</div>
</div>
</div>
<div class="main-container">
<div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:12px;">
<div>
<h1>ARGUS Social Media Admin</h1>
<p>Hantera registrering, automation och publicering av sociala mediekonton</p>
</div>
<button class="btn btn-primary" onclick="openAddPlatformModal()" style="white-space:nowrap;">+ Lägg till plattform</button>
</div>
<!-- Company Tabs -->
<div class="company-tabs">
<button class="company-tab active" data-company="landvex" onclick="selectCompany('landvex')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> LandveX</button>
<button class="company-tab" data-company="quixzoom" onclick="selectCompany('quixzoom')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> QuiXzoom</button>
<button class="company-tab" data-company="apifly" onclick="selectCompany('apifly')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> ApiFly</button>
<button class="company-tab" data-company="corpfitt" onclick="selectCompany('corpfitt')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M5 7C5 7 3 8 3 10C3 12 5 13 7 13H11C13 13 14 11 14 9C14 7 12 6 11 6H9V4C9 3 8 2 7 2C6 2 5 3 5 4V7Z" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/></svg> CorpFitt</button>
<button class="company-tab" data-company="vyra" onclick="selectCompany('vyra')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V4M8 12V14M2 8H4M12 8H14" stroke="#f59e0b" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="8" r="2" fill="#f59e0b"/></svg> VYRA</button>
<button class="company-tab" data-company="aamos" onclick="selectCompany('aamos')"> AAMOS</button>
</div>
<!-- Main Tabs -->
<div class="main-tabs">
<button class="main-tab active" data-tab="register" onclick="switchTab('register')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 13V10L10 3L13 6L6 13H3Z" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 4L12 7" stroke="#666" stroke-width="1.5"/></svg> Registrera</button>
<button class="main-tab" data-tab="jobs" onclick="switchTab('jobs')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="#666" stroke-width="1.5"/><path d="M8 2V4M8 12V14M2 8H4M12 8H14M4.3 4.3L5.7 5.7M10.3 10.3L11.7 11.7M4.3 11.7L5.7 10.3M10.3 5.7L11.7 4.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Jobb</button>
<button class="main-tab" data-tab="credentials" onclick="switchTab('credentials')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg> Credentials</button>
<button class="main-tab" data-tab="publishing" onclick="switchTab('publishing')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 6V10H5L10 14V2L5 6H2Z" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M12 5C13 6 13 10 12 11" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Publishing</button>
</div>
<!-- PANEL: Register -->
<div id="panel-register" class="tab-panel visible">
<div class="card">
<div class="card-title">Registrera nytt konto</div>
<div class="form-grid">
<div class="form-group">
<label>Bolag</label>
<select id="regCompany" onchange="onRegCompanyChange()">
<option value="landvex"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> LandveX</option>
<option value="quixzoom"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> QuiXzoom</option>
<option value="apifly"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> ApiFly</option>
<option value="corpfitt"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M5 7C5 7 3 8 3 10C3 12 5 13 7 13H11C13 13 14 11 14 9C14 7 12 6 11 6H9V4C9 3 8 2 7 2C6 2 5 3 5 4V7Z" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/></svg> CorpFitt</option>
<option value="vyra"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V4M8 12V14M2 8H4M12 8H14" stroke="#f59e0b" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="8" r="2" fill="#f59e0b"/></svg> VYRA</option>
<option value="aamos"> AAMOS</option>
</select>
</div>
<div class="form-group">
<label>Plattform</label>
<select id="regPlatform" onchange="fillForm(this.value)">
<option value="">— Välj plattform —</option>
</select>
</div>
<div class="form-group">
<label>E-post</label>
<input type="email" id="regEmail" placeholder="social@bolag.com">
</div>
<div class="form-group">
<label>Recovery-mail</label>
<input type="email" id="regRecovery" placeholder="recovery@bolag.com">
</div>
<div class="form-group">
<label>Namn</label>
<input type="text" id="regName" placeholder="Johan Berglund">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="text" id="regPhone" placeholder="+46709123223">
</div>
<div class="form-group">
<label>Webbplats</label>
<input type="text" id="regWebsite" placeholder="https://bolag.com">
</div>
<div class="form-group">
<label>Användarnamn / Handle</label>
<input type="text" id="regUsername" placeholder="@bolag">
</div>
</div>
<div style="margin-top:12px; padding:10px 14px; background:#0a1628; border:1px solid #1e3a5f; border-radius: var(--radius-md); font-size:12px; color:#60a5fa; display:flex; gap:8px; align-items:center;">
<span></span>
<span>Lösenord genereras automatiskt av servern (32+ tecken). Aldrig synligt i UI.</span>
</div>
<div style="margin-top:20px; display:flex; gap:12px; flex-wrap:wrap;">
<button class="btn btn-primary" onclick="startRegistration()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Starta automation</button>
<button class="btn btn-secondary" onclick="clearRegForm()"> Rensa</button>
</div>
</div>
</div>
<!-- PANEL: Jobs -->
<div id="panel-jobs" class="tab-panel">
<div class="card">
<!-- Approve-all banner (only shown when awaiting jobs exist) -->
<div class="approve-all-banner" id="approveAllBanner">
<span class="approve-all-banner-text" id="approveAllText">⏳ Jobb väntar på godkännande</span>
<button class="btn btn-success btn-sm" onclick="approveAll()"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Godkänn alla</button>
</div>
<div class="filter-row">
<button class="filter-btn active" data-filter="all" onclick="setJobFilter('all')">Alla</button>
<button class="filter-btn" data-filter="running" onclick="setJobFilter('running')">Aktiva</button>
<button class="filter-btn" data-filter="done" onclick="setJobFilter('done')">Färdiga</button>
<button class="filter-btn" data-filter="error" onclick="setJobFilter('error')">Fel</button>
<button class="filter-btn" data-filter="awaiting" onclick="setJobFilter('awaiting')">⏳ Väntar</button>
<button class="filter-btn" data-filter="manual" onclick="setJobFilter('manual')"> Manuell</button>
<button class="btn btn-secondary btn-sm" onclick="loadJobs()" style="margin-left:4px;"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Ladda om</button>
<span class="refresh-info" id="refreshInfo"></span>
</div>
<div id="jobsContent">
<div class="state-box"><div class="spinner"></div><br>Laddar jobb...</div>
</div>
</div>
</div>
<!-- PANEL: Credentials -->
<div id="panel-credentials" class="tab-panel">
<div class="card">
<div class="creds-header">
<div class="card-title" style="margin-bottom:0">Credentials — <span id="credsCompanyLabel">LandveX</span></div>
<div class="creds-actions">
<button class="btn btn-warning btn-sm" onclick="openRotateModal()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Rotera lösenord</button>
<button class="btn btn-danger btn-sm" onclick="document.getElementById('offboardModal').classList.add('visible')"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Anställd slutar</button>
<button class="btn btn-secondary btn-sm" onclick="loadCredentials()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Ladda om</button>
</div>
</div>
<div class="security-banner">
<span class="icon"></span>
<span>Lösenord lagras krypterat (AES-256). Lösenord visas ALDRIG i klartext — använd knappen för att visa/dölja tillfälligt.</span>
</div>
<div id="credsContent">
<div class="state-box"><div class="spinner"></div><br>Laddar credentials...</div>
</div>
<div id="rotationLog" style="display:none"></div>
</div>
</div>
<!-- PANEL: Publishing -->
<div id="panel-publishing" class="tab-panel">
<!-- Compose -->
<div class="card">
<div class="card-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 13V10L10 3L13 6L6 13H3Z" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 4L12 7" stroke="#666" stroke-width="1.5"/></svg> Skapa inlägg</div>
<div class="publish-grid">
<div>
<div class="form-group" style="margin-bottom:16px;">
<label>Bolag</label>
<select id="pubCompany" onchange="onPubCompanyChange()">
<option value="landvex"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> LandveX</option>
<option value="quixzoom"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> QuiXzoom</option>
<option value="apifly"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> ApiFly</option>
<option value="corpfitt"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M5 7C5 7 3 8 3 10C3 12 5 13 7 13H11C13 13 14 11 14 9C14 7 12 6 11 6H9V4C9 3 8 2 7 2C6 2 5 3 5 4V7Z" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/></svg> CorpFitt</option>
<option value="vyra"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V4M8 12V14M2 8H4M12 8H14" stroke="#f59e0b" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="8" r="2" fill="#f59e0b"/></svg> VYRA</option>
<option value="aamos"> AAMOS</option>
</select>
</div>
<div class="form-group" style="margin-bottom:16px;">
<label>Plattformar</label>
<div class="platform-checkboxes" id="pubPlatformChecks">
<!-- Populated by JS -->
</div>
</div>
<div class="form-group" style="margin-bottom:4px;">
<label>Innehåll</label>
<textarea id="pubContent" rows="6" placeholder="Skriv ditt inlägg här..." oninput="updateCharCounter(); updatePreview()" style="resize:vertical; font-family:inherit;"></textarea>
</div>
<div class="char-counter" id="charCounter">0 tecken</div>
<div class="form-group" style="margin-top:16px;">
<label>Schemaläggning</label>
<div class="schedule-row">
<div class="schedule-toggle">
<button class="sched-btn active" id="schedNowBtn" onclick="setScheduleMode('now')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> Nu</button>
<button class="sched-btn" id="schedLaterBtn" onclick="setScheduleMode('later')"> Schemalägg</button>
</div>
<input type="datetime-local" id="pubScheduleTime" style="display:none; flex:1; background:#030712; border:1px solid #1e2737; border-radius: var(--radius-md); color:#e2e8f0; padding:7px 12px; font-size:13px;" onchange="updatePreview()">
</div>
</div>
<div style="margin-top:20px; display:flex; gap:12px; flex-wrap:wrap;">
<button class="btn btn-primary" onclick="submitPost()"> Publicera</button>
<button class="btn btn-secondary" onclick="saveDraft()"> Spara utkast</button>
<button class="btn btn-ghost" onclick="clearPost()"> Rensa</button>
</div>
</div>
<div>
<div style="font-size:12px; font-weight:600; color:#475569; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:12px;">Förhandsvisning</div>
<div id="pubPreviews">
<div style="color:#475569; font-size:13px; padding:16px; text-align:center; border:1px dashed #1e2737; border-radius: var(--radius-md);">Välj plattformar och skriv innehåll för att se förhandsvisning</div>
</div>
</div>
</div>
</div>
<!-- Post Queue -->
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<div class="card-title" style="margin-bottom:0"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Publiceringsköen</div>
<button class="btn btn-secondary btn-sm" onclick="loadPostQueue()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Ladda om</button>
</div>
<div id="postQueueContent">
<div class="state-box"><div class="spinner"></div><br>Laddar...</div>
</div>
</div>
</div>
</div>
<script>
'use strict';
/*
CONSTANTS
*/
const API = '';
const COMPANY_CREDS = {
landvex: { email:'social@landvex.com', recovery:'recovery@landvex.com', name:'Johan Berglund', phone:'+46709123223', website:'https://landvex.se', company:'LandveX' },
quixzoom: { email:'social@quixzoom.com', recovery:'recovery@quixzoom.com', name:'Johan Berglund', phone:'+46709123223', website:'https://quixzoom.com', company:'QuiXzoom' },
apifly: { email:'social@apifly.com', recovery:'recovery@apifly.com', name:'Johan Berglund', phone:'+46709123223', website:'https://apifly.com', company:'ApiFly' },
corpfitt: { email:'social@corpfitt.com', recovery:'recovery@corpfitt.com', name:'Johan Berglund', phone:'+46709123223', website:'https://corpfitt.com', company:'CorpFitt' },
vyra: { email:'social@vyra.gg', recovery:'recovery@vyra.gg', name:'Johan Berglund', phone:'+46709123223', website:'https://vyra.gg', company:'VYRA' },
aamos: { email:'social@aamos.ai', recovery:'recovery@aamos.ai', name:'Johan Berglund', phone:'+46709123223', website:'https://aamos.ai', company:'AAMOS' },
};
const COMPANY_LABELS = {
landvex:'LandveX', quixzoom:'QuiXzoom', apifly:'ApiFly',
corpfitt:'CorpFitt', vyra:'VYRA', aamos:'AAMOS'
};
const PLATFORM_CHAR_LIMITS = {
twitter: 280, linkedin: 3000, reddit: 40000,
instagram: 2200, facebook: 63206, tiktok: 2200,
pinterest: 500, spotify: 200, youtube: 5000,
snapchat: 250
};
const PLATFORM_ICONS = {
twitter:'', instagram:'', facebook:'', linkedin:'',
tiktok:'', youtube:'', reddit:'', snapchat:'',
pinterest:'', spotify:'', meta:'',
jobteaser:'', hired:'', outbrain:''
};
const PLATFORM_LOGIN = {
pinterest:'https://www.pinterest.com/login/',
tiktok:'https://www.tiktok.com/login/',
instagram:'https://www.instagram.com/accounts/login/',
snapchat:'https://accounts.snapchat.com/',
reddit:'https://www.reddit.com/login/',
linkedin:'https://www.linkedin.com/login/',
twitter:'https://twitter.com/login',
meta:'https://business.facebook.com/',
google:'https://ads.google.com/',
spotify:'https://accounts.spotify.com/login',
indeed:'https://employers.indeed.com/',
monster:'https://hiring.monster.com/',
crunchbase:'https://www.crunchbase.com/login',
producthunt:'https://www.producthunt.com/login',
thehub:'https://thehub.io/login',
stepstone:'https://www.stepstone.com/',
reed:'https://www.reed.co.uk/login',
ziprecruiter:'https://www.ziprecruiter.com/login',
};
/*
PLATFORM STEPS
Fix #5: replaced manual mail instructions with auto-fetch note
*/
const PLATFORM_STEPS = {
tiktok: {
icon: '',
title: 'TikTok for Business',
mailbox: true,
steps: [
'1. Klicka på knappen <b>Öppna TikTok</b> nedan',
'2. Ange e-post: <code>{email}</code>',
'3. TikTok skickar en 6-siffrig verifieringskod till den e-posten',
'4. Vi hämtar koden automatiskt från mailboxen &mdash; klicka <b>Hämta kod</b> nedan',
'5. Klistra in koden på TikTok och slutför inloggningen',
'6. Klart! Tryck <b>Godkänn</b> här när du är klar'
]
},
instagram: {
icon: '',
title: 'Instagram Business',
mailbox: true,
steps: [
'1. Klicka på knappen <b>Öppna Instagram</b> nedan',
'2. Välj "Registrera med e-post"',
'3. Ange e-post: <code>{email}</code>',
'4. Välj användarnamn: <code>{username}</code>',
'5. Vi hämtar koden automatiskt från mailboxen &mdash; klicka <b>Hämta kod</b> nedan',
'6. Ange koden på Instagram och slutför registreringen',
'7. Tryck <b>Godkänn</b> här när kontot är skapat'
]
},
snapchat: {
icon: '',
title: 'Snapchat Business',
mailbox: true,
steps: [
'1. Klicka på knappen <b>Öppna Snapchat</b> nedan',
'2. Klicka "Create Business Account"',
'3. Ange e-post: <code>{email}</code>',
'4. Vi hämtar koden automatiskt från mailboxen &mdash; klicka <b>Hämta kod</b> nedan',
'5. Ange koden och slutför registreringen',
'6. Tryck <b>Godkänn</b> här när klart'
]
},
reddit: {
icon: '',
title: 'Reddit Ads',
mailbox: false,
steps: [
'1. Klicka på knappen <b>Öppna Reddit</b> nedan',
'2. Ange e-post: <code>{email}</code>',
'3. OBS: Användarnamnet <b>"{username}"</b> kan vara taget',
'4. Välj ett alternativt namn om det krävs, t.ex. <b>LandveX_AB</b>',
'5. Slutför registreringen',
'6. Tryck <b>Godkänn</b> här när kontot är skapat'
]
},
default: {
icon: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
title: 'Granska &amp; godkänn',
mailbox: false,
steps: [
'1. Botten har fyllt i formuläret automatiskt',
'2. Granska skärmdumpen ovan &mdash; ser den korrekt ut?',
'3. Om allt ser bra ut &mdash; klicka <b>Godkänn</b> för att slutföra',
'4. Verifieringsmail kan ha skickats till <code>{email}</code> om kontot kräver det'
]
}
};
/*
STATE
*/
let currentCompany = 'landvex';
let currentMainTab = 'register';
let currentJobFilter = 'all';
let allJobs = [];
let platformList = [];
let scheduleMode = 'now';
let countdownTimer = null;
let refreshCountdown = 15;
/*
AUTH
*/
function jwtExpired(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));
return payload.exp ? (payload.exp * 1000 < Date.now() + 60000) : false;
} catch(e) { return true; }
}
async function ensureAuth() {
let tok = localStorage.getItem('aamos_token');
if (tok && !jwtExpired(tok)) return tok;
try {
const r = await fetch(API + '/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'erik@hypbit.com', password: 'WavultCEO2026!' })
});
if (!r.ok) throw new Error('Login HTTP ' + r.status);
const d = await r.json();
tok = d.token || d.access_token || '';
if (tok) localStorage.setItem('aamos_token', tok);
return tok;
} catch(e) {
console.error('ensureAuth failed:', e);
return '';
}
}
function getToken() { return localStorage.getItem('aamos_token') || ''; }
function authHeaders() {
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() };
}
function getAuthToken() {
try {
const keys = ['qz_auth_token','aamos_token','qz_token'];
for (const k of keys) { const t = localStorage.getItem(k); if (t) return t; }
} catch(e) {}
return getToken();
}
/*
TOAST
*/
function toast(msg, type, durationMs) {
type = type || 'info';
durationMs = durationMs || 4000;
const c = document.getElementById('toastContainer');
const t = document.createElement('div');
t.className = 'toast toast-' + type;
t.textContent = msg;
c.appendChild(t);
setTimeout(function() { t.remove(); }, durationMs);
}
/*
PLATFORMS
*/
async function loadPlatforms() {
try {
await ensureAuth();
const r = await fetch(API + '/api/social-account/platforms', { headers: authHeaders() });
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
const raw = Array.isArray(data) ? data : (data.platforms || []);
platformList = raw.map(function(p) {
return typeof p === 'object' ? (p.platform || p.name || p.id || String(p)) : String(p);
});
} catch(e) {
console.warn('loadPlatforms failed, using fallback:', e);
platformList = ['twitter','instagram','facebook','linkedin','tiktok','youtube','reddit','pinterest'];
}
renderPlatformDropdown();
renderPubPlatformChecks();
}
function renderPlatformDropdown() {
const sel = document.getElementById('regPlatform');
if (!sel) return;
sel.innerHTML = '<option value="">— Välj plattform —</option>';
platformList.forEach(function(name) {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = (PLATFORM_ICONS[name] || '') + ' ' + name.charAt(0).toUpperCase() + name.slice(1);
sel.appendChild(opt);
});
}
function renderPubPlatformChecks() {
const container = document.getElementById('pubPlatformChecks');
if (!container) return;
container.innerHTML = '';
platformList.forEach(function(name) {
const id = 'pubChk_' + name;
const wrap = document.createElement('label');
wrap.className = 'platform-checkbox';
wrap.style.cssText = 'display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 10px;border:1px solid #1e2737;border-radius: var(--radius-md);';
wrap.innerHTML = '<input type="checkbox" id="' + id + '" value="' + name + '" onchange="updatePreview()" style="width:auto;cursor:pointer;"><span style="font-size:13px;color:#cbd5e1;">' + (PLATFORM_ICONS[name] || '') + ' ' + name.charAt(0).toUpperCase() + name.slice(1) + '</span>';
container.appendChild(wrap);
});
}
/*
REGISTER FORM
*/
async function fillForm(platform) {
if (!platform) return;
await ensureAuth();
const creds = COMPANY_CREDS[currentCompany] || {};
setVal('regEmail', creds.email || '');
setVal('regRecovery', creds.recovery || '');
setVal('regName', creds.name || '');
setVal('regPhone', creds.phone || '');
setVal('regWebsite', creds.website || '');
try {
const r = await fetch(API + '/api/social-account/credentials/' + encodeURIComponent(currentCompany), {
headers: authHeaders()
});
if (r.ok) {
const data = await r.json();
const acc = data && data.accounts ? data.accounts[platform] : null;
if (acc && acc.username) {
setVal('regUsername', '@' + acc.username);
return;
}
}
} catch(e) { /* fallback */ }
const handleBase = (creds.company || currentCompany).toLowerCase().replace(/[^a-z0-9]/g, '');
setVal('regUsername', '@' + handleBase);
}
function setVal(id, val) {
const el = document.getElementById(id);
if (el) el.value = val;
}
function onRegCompanyChange() {
const sel = document.getElementById('regCompany');
currentCompany = sel ? sel.value : 'landvex';
syncCompanyTabs();
const plat = document.getElementById('regPlatform');
if (plat && plat.value) fillForm(plat.value);
}
function clearRegForm() {
['regEmail','regRecovery','regName','regPhone','regWebsite','regUsername'].forEach(function(id) { setVal(id, ''); });
const ps = document.getElementById('regPlatform');
if (ps) ps.value = '';
}
async function startRegistration() {
const company = document.getElementById('regCompany') ? document.getElementById('regCompany').value : currentCompany;
const platform = document.getElementById('regPlatform') ? document.getElementById('regPlatform').value : '';
const email = document.getElementById('regEmail') ? document.getElementById('regEmail').value : '';
const username = document.getElementById('regUsername') ? document.getElementById('regUsername').value : '';
const recovery = document.getElementById('regRecovery') ? document.getElementById('regRecovery').value : '';
const name = document.getElementById('regName') ? document.getElementById('regName').value : '';
const phone = document.getElementById('regPhone') ? document.getElementById('regPhone').value : '';
const website = document.getElementById('regWebsite') ? document.getElementById('regWebsite').value : '';
if (!platform) { toast('Välj en plattform först', 'error'); return; }
if (!email) { toast('E-post krävs', 'error'); return; }
const payload = {
company: company,
platform: platform,
email: email,
username: username.replace(/^@/, ''),
recovery_email: recovery,
name: name,
phone: phone,
website: website
};
try {
const r = await fetch(API + '/api/social-account/register', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify(payload)
});
const d = await r.json().catch(function() { return {}; });
if (r.ok) {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Automation startad för ' + platform + ' (' + company + ')', 'success');
switchTab('jobs');
} else {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Fel: ' + (d.error || d.message || r.status), 'error');
}
} catch(e) {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel: ' + e.message, 'error');
}
}
/*
COMPANY SELECTION
*/
function selectCompany(company) {
currentCompany = company;
syncCompanyTabs();
const sel = document.getElementById('regCompany');
if (sel) sel.value = company;
document.getElementById('credsCompanyLabel').textContent = COMPANY_LABELS[company] || company;
if (currentMainTab === 'credentials') loadCredentials();
const pubSel = document.getElementById('pubCompany');
if (pubSel) pubSel.value = company;
}
function syncCompanyTabs() {
// Räkna jobb per bolag
const counts = {};
allJobs.forEach(function(j) {
const co = (j.company || '').toLowerCase();
const s = (j.status || '').toLowerCase();
if (!counts[co]) counts[co] = {total:0, done:0, running:0, error:0, manual:0, awaiting:0};
counts[co].total++;
if (s === 'completed' || s === 'done' || s === 'success') counts[co].done++;
else if (s === 'launching' || s === 'navigating' || s === 'filling_form' || s === 'solving_captcha' || s === 'submitting' || s === 'queued') counts[co].running++;
else if (s === 'failed' || s === 'timeout' || s === 'error') counts[co].error++;
else if (s === 'awaiting_approval') counts[co].awaiting++;
else if (s === 'pending_manual' || s === 'manual_required' || s === 'manual_handle_review') counts[co].manual++;
});
document.querySelectorAll('.company-tab').forEach(function(btn) {
const co = btn.dataset.company;
const c = counts[co] || {total:0, done:0, running:0, error:0, awaiting:0, manual:0};
const isActive = co === currentCompany;
btn.classList.toggle('active', isActive);
// Bygg badge-html
let badges = '';
if (c.done > 0) badges += '<span style="background:#16a34a;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'+c.done+'</span>';
if (c.running > 0) badges += '<span style="background:#2563eb;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg>'+c.running+'</span>';
if (c.awaiting > 0) badges += '<span style="background:#d97706;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px">⏳'+c.awaiting+'</span>';
if (c.error > 0) badges += '<span style="background:#dc2626;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg>'+c.error+'</span>';
if (c.manual > 0) badges += '<span style="background:#7c3aed;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px">'+c.manual+'</span>';
// Uppdatera knapp-label (spara original-label om inte redan gjort)
if (!btn.dataset.label) btn.dataset.label = btn.innerHTML;
btn.innerHTML = btn.dataset.label + badges;
});
}
/*
MAIN TAB SWITCHING
*/
function switchTab(tab) {
currentMainTab = tab;
document.querySelectorAll('.main-tab').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.tab === tab);
});
document.querySelectorAll('.tab-panel').forEach(function(p) {
const show = p.id === 'panel-' + tab;
p.classList.toggle('visible', show);
p.style.display = show ? '' : 'none';
});
if (tab === 'jobs') loadJobs();
if (tab === 'credentials') loadCredentials();
if (tab === 'publishing') loadPostQueue();
}
/*
JOBS
*/
function setJobFilter(filter) {
currentJobFilter = filter;
document.querySelectorAll('.filter-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.filter === filter);
});
renderJobs();
}
async function loadJobs() {
const container = document.getElementById('jobsContent');
if (!container) return;
try {
const r = await fetch(API + '/api/social-account/jobs', { headers: authHeaders() });
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
allJobs = Array.isArray(data) ? data : (data.jobs || data.data || []);
renderJobs();
updateApproveAllBanner();
} catch(e) {
container.innerHTML = '<div class="state-box"><div class="icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></div><b>Kunde inte ladda jobb</b><p>' + esc(e.message) + '</p></div>';
}
}
function filterJobs() {
// Filtrera alltid på valt bolag FÖRST
let jobs = allJobs.filter(function(j) {
return (j.company || '').toLowerCase() === (currentCompany || '').toLowerCase();
});
// Sedan filtrera på status
if (currentJobFilter === 'all') return jobs;
return jobs.filter(function(j) {
const s = (j.status || '').toLowerCase();
if (currentJobFilter === 'running') return s === 'running' || s === 'launching' || s === 'navigating' || s === 'filling_form' || s === 'solving_captcha' || s === 'submitting' || s === 'pending' || s === 'queued';
if (currentJobFilter === 'done') return s === 'done' || s === 'completed' || s === 'success';
if (currentJobFilter === 'error') return s === 'error' || s === 'failed' || s === 'timeout';
if (currentJobFilter === 'awaiting') return s === 'awaiting_approval';
if (currentJobFilter === 'manual') return s === 'pending_manual' || s === 'manual_required' || s === 'manual_handle_review';
return true;
});
}
function updateApproveAllBanner() {
const banner = document.getElementById('approveAllBanner');
const text = document.getElementById('approveAllText');
if (!banner) return;
const awaitingJobs = allJobs.filter(function(j) {
return (j.status || '').toLowerCase() === 'awaiting_approval';
});
if (awaitingJobs.length > 0) {
text.textContent = '⏳ ' + awaitingJobs.length + ' jobb väntar på godkännande';
banner.classList.add('visible');
} else {
banner.classList.remove('visible');
}
}
async function approveAll() {
const awaitingJobs = allJobs.filter(function(j) {
return (j.status || '').toLowerCase() === 'awaiting_approval';
});
if (awaitingJobs.length === 0) { toast('Inga jobb att godkänna', 'info'); return; }
toast('Godkänner ' + awaitingJobs.length + ' jobb...', 'info', 3000);
let ok = 0, fail = 0;
for (const job of awaitingJobs) {
const id = job.jobId || job.id || job.job_id;
try {
const r = await fetch(API + '/api/social-account/approve/' + encodeURIComponent(id), {
method: 'POST', headers: authHeaders()
});
if (r.ok) { ok++; } else { fail++; }
} catch(e) { fail++; }
}
if (fail === 0) {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Alla ' + ok + ' jobb godkända', 'success');
} else {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Godkände ' + ok + ', misslyckades ' + fail, 'warning');
}
loadJobs();
}
/* Build steps HTML (Fix #1: "Gör så här", Fix #3: mail-knapp, Fix #4: credentials) */
async function buildStepsHtml(job) {
const plat = (job.platform || '').toLowerCase();
const info = PLATFORM_STEPS[plat] || PLATFORM_STEPS.default;
const prof = job.profile || {};
const platAcct = (prof.accounts || {})[plat] || {};
const email = esc(prof.email || job.profileEmail || '?');
const username = esc(platAcct.username || prof.username || job.profileUsername || (job.extraData || {}).username || '?');
const status = (job.status || '').toLowerCase();
const isManual = ['awaiting_approval','pending_manual','manual_required','manual_handle_review'].includes(status);
const id = job.jobId || job.id || job.job_id || 'x';
const autofillUrl = '/autofill-login.html?company=' + encodeURIComponent(job.company || '') +
'&platform=' + encodeURIComponent(plat) +
'&token=' + encodeURIComponent(getAuthToken());
if (!isManual) {
return '<a href="' + autofillUrl + '" target="_blank" rel="noopener" ' +
'style="display:inline-flex;align-items:center;gap:5px;color:#a5b4fc;font-size:.75rem;font-weight:600;' +
'text-decoration:none;border:1px solid #4338ca;padding:4px 12px;border-radius: 6px">' +
'Logga in på ' + esc(job.platform) + '</a>';
}
/* Build steps list */
const stepsHtml = info.steps.map(function(s) {
return '<li style="margin-bottom:8px;line-height:1.5">' +
s.replace(/{email}/g, '<code style="background:#1e293b;padding:2px 8px;border-radius: var(--radius-sm);color:#6ee7b7;font-size:.85em">' + email + '</code>')
.replace(/{username}/g, '<code style="background:#1e293b;padding:2px 8px;border-radius: var(--radius-sm);color:#a5b4fc;font-size:.85em">' + username + '</code>') +
'</li>';
}).join('');
/* Fix #4: Credentials block for manual-status jobs */
let credsBlock = '';
let revealedPw = platAcct.password || prof.password || '';
if (job.company) {
try {
const credRes = await fetch(API + '/api/social-account/credentials?company=' +
encodeURIComponent(job.company) + '&reveal=1', { headers: authHeaders() });
if (credRes.ok) {
const creds = await credRes.json();
const arr = Array.isArray(creds) ? creds : (creds.credentials || []);
const match = arr.find(function(c) { return c.platform === plat; });
if (match) {
if (match.password && match.password !== '' && match.password !== '•••••') revealedPw = match.password;
}
}
} catch(e) { /* ignore */ }
}
const safeId = String(id).replace(/[^a-z0-9\-]/gi, '');
credsBlock = '<div class="inline-creds">' +
'<div class="inline-creds-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg> Inloggningsuppgifter</div>' +
'<div class="cred-inline-row">' +
'<span class="cred-inline-label">E-post</span>' +
'<span class="cred-inline-val">' + email + '</span>' +
'<button class="copy-btn" onclick="copyText(\'' + email.replace(/'/g, "\\'") + '\', this)">Kopiera</button>' +
'</div>' +
'<div class="cred-inline-row">' +
'<span class="cred-inline-label">Lösenord</span>' +
(revealedPw
? '<input type="password" id="pw-' + safeId + '" value="' + esc(revealedPw) + '" readonly class="pw-field">' +
'<button class="reveal-btn" onclick="togglePw(\'pw-' + safeId + '\', this)"> Visa</button>' +
'<button class="copy-btn" onclick="copyText(\'' + revealedPw.replace(/'/g, "\\'") + '\', this)">Kopiera</button>'
: '<span style="color:#475569;font-size:12px;"> Ej tillgängligt</span>'
) +
'</div>' +
'</div>';
/* Fix #3: Mail-code fetch button */
const mailCodeHtml = info.mailbox
? '<div class="mail-code-box">' +
'<div style="font-size:12px;color:#94a3b8;margin-bottom:8px;"> Väntar på verifieringskod via mail</div>' +
'<button class="btn btn-teal btn-sm" onclick="fetchMailCode(\'' + safeId + '\', \'' + esc(plat) + '\', this)">' +
' Hämta kod</button>' +
'<div id="mail-code-result-' + safeId + '" class="mail-code-result hidden"></div>' +
'<div style="margin-top:6px;">' +
'<a href="https://mail.quixzoom.com/sso/login" target="_blank" rel="noopener" ' +
'style="font-size:11px;color:#475569;">Eller öppna mailboxen manuellt →</a>' +
'</div>' +
'</div>'
: '';
const loginBtn = '<a href="' + autofillUrl + '" target="_blank" rel="noopener" ' +
'style="display:inline-flex;align-items:center;gap:6px;background:#6366f1;color:#fff;font-weight:700;' +
'font-size:.85rem;text-decoration:none;padding:10px 20px;border-radius: var(--radius-md);margin-top:10px">' +
info.icon + ' Logga in på ' + esc(job.platform) + ' &rarr;</a>';
return '<div style="background:#0a0e1a;border:1px solid #1e2a38;border-radius: var(--radius-md);padding:16px;margin-top:10px">' +
'<div style="font-weight:700;color:#e2e8f0;margin-bottom:10px;font-size:.9rem">' + info.icon + ' Gör så här:</div>' +
'<ol style="padding-left:0;list-style:none;margin:0;font-size:.82rem;color:#cbd5e1">' + stepsHtml + '</ol>' +
credsBlock +
mailCodeHtml +
'<div style="display:flex;gap:10px;flex-wrap:wrap">' + loginBtn + '</div>' +
'</div>';
}
/* Fix #3: Fetch mail code */
async function fetchMailCode(jobId, platform, btn) {
const resultEl = document.getElementById('mail-code-result-' + jobId);
btn.disabled = true;
btn.textContent = '⏳ Hämtar...';
try {
const r = await fetch(API + '/api/social-account/fetch-mail-code?jobId=' +
encodeURIComponent(jobId) + '&platform=' + encodeURIComponent(platform), {
headers: authHeaders()
});
if (r.ok) {
const d = await r.json();
const code = d.code || d.verification_code || d.otp || '';
if (code && resultEl) {
resultEl.classList.remove('hidden');
resultEl.innerHTML = 'Kod: <strong style="font-size:1.4em;letter-spacing:4px;">' + esc(code) + '</strong>' +
'<button class="copy-btn" onclick="copyText(\'' + esc(code) + '\', this)">Kopiera</button>';
btn.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Hämtad';
} else {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Ingen kod hittades ännu — försök om ett par sekunder', 'warning');
btn.disabled = false;
btn.textContent = ' Hämta kod';
}
} else if (r.status === 404) {
/* Endpoint doesn't exist — fallback to open mailbox */
toast(' Automatisk hämtning ej tillgänglig — öppna mailboxen manuellt', 'info');
window.open('https://mail.quixzoom.com/sso/login', '_blank');
btn.disabled = false;
btn.textContent = ' Hämta kod';
} else {
const d = await r.json().catch(function() { return {}; });
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Fel vid hämtning: ' + (d.error || r.status), 'error');
btn.disabled = false;
btn.textContent = ' Hämta kod';
}
} catch(e) {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel: ' + e.message, 'error');
btn.disabled = false;
btn.textContent = ' Hämta kod';
}
}
/* Render jobs table */
async function renderJobs() {
const container = document.getElementById('jobsContent');
if (!container) return;
const jobs = filterJobs();
if (jobs.length === 0) {
container.innerHTML = '<div class="state-box"><div class="icon"></div><b>Inga jobb hittades</b><p>Byt filter eller starta ett nytt jobb.</p></div>';
return;
}
let html = '<div class="jobs-table-wrap"><table class="jobs-table">';
html += '<thead><tr>' +
'<th>ID</th><th>Bolag</th><th>Plattform</th>' +
'<th>Status</th><th>Åtgärd krävs</th>' +
'<th>Skapad</th><th>Screenshot</th><th>Åtgärder</th>' +
'</tr></thead><tbody>';
for (const job of jobs) {
const id = job.jobId || job.id || job.job_id || '—';
const company = esc(job.company || job.company_id || '—');
const platform = esc(job.platform || job.platform_id || '—');
const status = (job.status || 'unknown').toLowerCase().replace(/ /g, '_');
const created = (job.createdAt || job.created_at)
? new Date(job.createdAt || job.created_at).toLocaleString('sv-SE')
: '—';
const isAwait = status === 'awaiting_approval';
const isManual = status === 'pending_manual' || status === 'manual_required' || status === 'manual_handle_review';
const badgeMap = {
running: 'badge-running', in_progress: 'badge-running', pending: 'badge-running', submitting: 'badge-running',
done: 'badge-done', completed: 'badge-done', success: 'badge-done',
error: 'badge-error', failed: 'badge-error',
timeout: 'badge-timeout',
awaiting_approval: 'badge-approval',
pending_manual: 'badge-manual', manual_required: 'badge-manual', manual_handle_review: 'badge-manual',
cancelled: 'badge-cancelled'
};
const badgeClass = badgeMap[status] || 'badge-pending';
const statusLabel = {
awaiting_approval: '⏳ Väntar godkännande',
pending_manual: ' Manuell åtgärd',
manual_required: ' Manuell krävs',
manual_handle_review:' Granska handle',
completed: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Klar',
failed: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Misslyckad',
timeout: '⌛ Timeout',
}[status] || status.replace(/_/g, ' ');
/* Action required column */
let actionRequired = '<span class="action-none">—</span>';
if (isAwait) actionRequired = '<span class="action-required-cell"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Godkänn / avvisa</span>';
if (isManual) actionRequired = '<span class="action-required-cell"> Manuell inloggning krävs</span>';
/* Fix #2: Screenshot from /api/social-account/screenshot/:jobId */
const screenshotUrl = API + '/api/social-account/screenshot/' + encodeURIComponent(id);
const showScreenshot = ['awaiting_approval','completed','failed','pending_manual','manual_required','manual_handle_review'].includes(status);
let screenshotHtml = '<span class="no-screenshot">—</span>';
if (showScreenshot) {
screenshotHtml = '<img class="screenshot-thumb" src="' + screenshotUrl + '" ' +
'onerror="this.style.display=\'none\'" ' +
'onclick="window.open(\'' + screenshotUrl + '\',\'_blank\')" ' +
'alt="screenshot" loading="lazy">';
}
/* Action buttons */
let actions = '';
if (isAwait) {
actions += '<button class="btn btn-success btn-sm" data-action="approve" data-jobid="' + esc(String(id)) + '"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Godkänn</button> ';
actions += '<button class="btn btn-danger btn-sm" data-action="reject" data-jobid="' + esc(String(id)) + '"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Avvisa</button>';
} else {
actions += '<button class="btn btn-ghost btn-sm" data-action="delete" data-jobid="' + esc(String(id)) + '"></button>';
}
/* Build the steps block (async) */
const stepsBlock = await buildStepsHtml(job);
html += '<tr' + (isAwait ? ' class="row-awaiting"' : '') + '>';
html += '<td><code style="font-size:11px;color:#6366f1">' + esc(String(id).slice(0, 12)) + '</code></td>';
html += '<td>' + (PLATFORM_ICONS[job.platform] || '') + ' ' + platform + '</td>';
html += '<td><div><span class="badge ' + badgeClass + '">' + esc(statusLabel) + '</span>' + stepsBlock + '</div></td>';
html += '<td>' + actionRequired + '</td>';
html += '<td style="white-space:nowrap;font-size:12px;">' + created + '</td>';
html += '<td>' + screenshotHtml + '</td>';
html += '<td style="white-space:nowrap">' + actions + '</td>';
html += '</tr>';
}
html += '</tbody></table></div>';
container.innerHTML = html;
}
async function approveJob(jobId) {
try {
const r = await fetch(API + '/api/social-account/approve/' + encodeURIComponent(jobId), {
method: 'POST', headers: authHeaders()
});
if (r.ok) {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Jobb godkänt', 'success');
loadJobs();
} else {
const d = await r.json().catch(function() { return {}; });
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Godkännande misslyckades: ' + (d.error || r.status), 'error');
}
} catch(e) { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> ' + e.message, 'error'); }
}
async function rejectJob(jobId) {
if (!confirm('Avvisa jobb ' + jobId + '?')) return;
await deleteJob(jobId);
}
async function deleteJob(jobId) {
try {
const r = await fetch(API + '/api/social-account/jobs/' + encodeURIComponent(jobId), {
method: 'DELETE', headers: authHeaders()
});
if (r.ok) {
toast(' Jobb raderat', 'info');
allJobs = allJobs.filter(function(j) {
const jid = j.jobId || j.id || j.job_id;
return String(jid) !== String(jobId);
});
renderJobs();
} else {
const d = await r.json().catch(function() { return {}; });
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Radering misslyckades: ' + (d.error || r.status), 'error');
}
} catch(e) { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> ' + e.message, 'error'); }
}
/* Fix #6: Auto-refresh every 15s */
function startAutoRefresh() {
stopAutoRefresh();
refreshCountdown = 15;
updateRefreshInfo();
countdownTimer = setInterval(function() {
refreshCountdown--;
updateRefreshInfo();
if (refreshCountdown <= 0) {
refreshCountdown = 15;
if (currentMainTab === 'jobs') loadJobs();
}
}, 1000);
}
function stopAutoRefresh() {
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
}
function updateRefreshInfo() {
const el = document.getElementById('refreshInfo');
if (el) el.textContent = 'Auto-refresh om ' + refreshCountdown + 's';
}
/*
CREDENTIALS
*/
async function loadCredentials() {
const container = document.getElementById('credsContent');
if (!container) return;
container.innerHTML = '<div class="state-box"><div class="spinner"></div><br>Laddar credentials för ' + esc(currentCompany) + '...</div>';
document.getElementById('credsCompanyLabel').textContent = COMPANY_LABELS[currentCompany] || currentCompany;
try {
await ensureAuth();
const r = await fetch(API + '/api/social-account/credentials?company=' +
encodeURIComponent(currentCompany) + '&reveal=1', { headers: authHeaders() });
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
let creds = [];
if (Array.isArray(data)) {
creds = data;
} else if (data.credentials) {
creds = data.credentials;
} else if (data.accounts) {
const email = data.email || '';
creds = Object.entries(data.accounts).map(function(entry) {
return { platform: entry[0], email: email, username: entry[1].username || '', password: entry[1].password || '' };
});
}
renderCredentials(creds);
} catch(e) {
container.innerHTML = '<div class="state-box"><div class="icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></div><b>Kunde inte ladda credentials</b><p>' + esc(e.message) + '</p></div>';
}
}
function renderCredentials(creds) {
const container = document.getElementById('credsContent');
if (!container) return;
if (!creds || creds.length === 0) {
container.innerHTML = '<div class="state-box"><div class="icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg></div><b>Inga credentials för ' + esc(currentCompany) + '</b><p>Registrera konton för att se credentials här.</p></div>';
return;
}
let html = '<div class="creds-grid">';
creds.forEach(function(c, idx) {
const platform = esc(c.platform || '?');
const email = esc(c.email || '—');
const username = esc(c.username || '—');
const icon = PLATFORM_ICONS[c.platform] || '';
const hasPw = c.password && c.password !== '' && c.password !== '•••••';
const pwId = 'cred-pw-' + idx;
html += '<div class="cred-card">';
html += '<div class="cred-platform">' + icon + ' ' + platform + '</div>';
html += '<div class="cred-rows">';
html += '<div class="cred-row"><span class="cred-label">E-post</span>' +
'<span class="cred-value">' + email + '</span>' +
'<button class="copy-btn" onclick="copyText(\'' + email.replace(/'/g, "\\'") + '\', this)">Kopiera</button>' +
'</div>';
html += '<div class="cred-row"><span class="cred-label">Handle</span>' +
'<span class="cred-value">@' + username + '</span></div>';
html += '<div class="cred-row"><span class="cred-label">Lösenord</span>' +
(hasPw
? '<input type="password" id="' + pwId + '" value="' + esc(c.password) + '" readonly class="pw-field">' +
'<button class="reveal-btn" onclick="togglePw(\'' + pwId + '\', this)"> Visa</button>' +
'<button class="copy-btn" onclick="copyText(\'' + c.password.replace(/'/g, "\\'") + '\', this)">Kopiera</button>'
: '<span class="cred-value" style="color:#475569;"> Krypterat (AES-256)</span>'
) +
'</div>';
html += '</div></div>';
});
html += '</div>';
container.innerHTML = html;
}
/* Password Rotation */
let rotateTarget = null;
function openRotateModal() {
rotateTarget = currentCompany;
document.getElementById('rotateModalText').textContent =
'Generera nya starka lösenord (32+ tecken) för alla plattformar på ' +
(COMPANY_LABELS[currentCompany] || currentCompany) + '? Detta kan inte ångras.';
document.getElementById('rotateModal').classList.add('visible');
}
function closeRotateModal() {
document.getElementById('rotateModal').classList.remove('visible');
}
async function confirmRotate() {
closeRotateModal();
const companies = rotateTarget === 'ALL' ? Object.keys(COMPANY_LABELS) : [rotateTarget];
let rotated = 0, failed = 0;
const logItems = [];
toast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Roterar lösenord...', 'info', 10000);
for (const company of companies) {
try {
const url = company === 'ALL'
? API + '/api/social-account/credentials-all/rotate'
: API + '/api/social-account/credentials/' + company + '/rotate';
const r = await fetch(url, {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ performedBy: 'admin' })
});
if (r.ok) {
const d = await r.json();
rotated++;
logItems.push({ company: company, status: 'ok', platforms: (d.rotated || []).length, ts: new Date().toLocaleString('sv-SE') });
} else {
failed++;
logItems.push({ company: company, status: 'error', ts: new Date().toLocaleString('sv-SE') });
}
} catch(e) {
failed++;
logItems.push({ company: company, status: 'error', error: e.message, ts: new Date().toLocaleString('sv-SE') });
}
}
if (failed === 0) {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Lösenord roterade för ' + rotated + ' bolag', 'success');
} else {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Roterade ' + rotated + ', misslyckades ' + failed, 'warning');
}
const logDiv = document.getElementById('rotationLog');
if (logDiv) {
let logHtml = '<div style="margin-top:16px;"><div class="card-title" style="font-size:13px;">Rotationslogg</div><div class="rotation-log">';
logItems.forEach(function(item) {
logHtml += '<div class="rotation-log-item">';
logHtml += '<span>' + esc(COMPANY_LABELS[item.company] || item.company) + '</span>';
logHtml += '<span>' + (item.status === 'ok' ? '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> ' + (item.platforms || 0) + ' plattformar' : '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Fel') + '</span>';
logHtml += '<span style="color:#374151;">' + esc(item.ts) + '</span>';
logHtml += '</div>';
});
logHtml += '</div></div>';
logDiv.innerHTML = logHtml;
logDiv.style.display = '';
}
if (currentMainTab === 'credentials') loadCredentials();
}
async function confirmOffboard() {
document.getElementById('offboardModal').classList.remove('visible');
rotateTarget = 'ALL';
document.getElementById('rotateModalText').textContent =
'VARNING: Roterar ALLA lösenord för ALLA 6 bolag och alla plattformar. Använd vid anställds avslut.';
await confirmRotate();
}
/*
PUBLISHING MODULE
*/
function onPubCompanyChange() {
const sel = document.getElementById('pubCompany');
currentCompany = sel ? sel.value : 'landvex';
syncCompanyTabs();
}
function setScheduleMode(mode) {
scheduleMode = mode;
document.getElementById('schedNowBtn').classList.toggle('active', mode === 'now');
document.getElementById('schedLaterBtn').classList.toggle('active', mode === 'later');
const timeInput = document.getElementById('pubScheduleTime');
if (timeInput) timeInput.style.display = mode === 'later' ? '' : 'none';
updatePreview();
}
function getSelectedPlatforms() {
const checks = document.querySelectorAll('#pubPlatformChecks input[type=checkbox]:checked');
return Array.from(checks).map(function(c) { return c.value; });
}
function updateCharCounter() {
const text = document.getElementById('pubContent') ? document.getElementById('pubContent').value : '';
const len = text.length;
const el = document.getElementById('charCounter');
if (!el) return;
const platforms = getSelectedPlatforms();
const minLimit = platforms.length > 0
? Math.min.apply(null, platforms.map(function(p) { return PLATFORM_CHAR_LIMITS[p] || 99999; }))
: 280;
el.textContent = len + ' / ' + minLimit + ' tecken';
el.className = 'char-counter' + (len > minLimit ? ' over' : len > minLimit * 0.9 ? ' warn' : '');
}
function updatePreview() {
const container = document.getElementById('pubPreviews');
if (!container) return;
const text = document.getElementById('pubContent') ? document.getElementById('pubContent').value : '';
const platforms = getSelectedPlatforms();
updateCharCounter();
if (platforms.length === 0 || !text.trim()) {
container.innerHTML = '<div style="color:#475569;font-size:13px;padding:16px;text-align:center;border:1px dashed #1e2737;border-radius: var(--radius-md);">Välj plattformar och skriv innehåll för att se förhandsvisning</div>';
return;
}
let html = '';
platforms.forEach(function(plat) {
const limit = PLATFORM_CHAR_LIMITS[plat] || 99999;
const preview = text.length > limit ? text.slice(0, limit - 3) + '...' : text;
const icon = PLATFORM_ICONS[plat] || '';
html += '<div class="post-preview" style="margin-bottom:12px;">';
html += '<div class="preview-platform">' + icon + ' ' + esc(plat) + '</div>';
html += '<div class="preview-content">' + esc(preview) + '</div>';
if (text.length > limit) {
html += '<div style="font-size:11px;color:#ef4444;margin-top:6px;"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> ' + (text.length - limit) + ' tecken för långt</div>';
}
html += '</div>';
});
container.innerHTML = html;
}
async function submitPost(asDraft) {
asDraft = asDraft || false;
const company = document.getElementById('pubCompany') ? document.getElementById('pubCompany').value : currentCompany;
const platforms = getSelectedPlatforms();
const text = document.getElementById('pubContent') ? document.getElementById('pubContent').value.trim() : '';
const timeInput = document.getElementById('pubScheduleTime') ? document.getElementById('pubScheduleTime').value : '';
const scheduledAt = (scheduleMode === 'later' && timeInput) ? new Date(timeInput).toISOString() : null;
if (platforms.length === 0) { toast('Välj minst en plattform', 'error'); return; }
if (!text) { toast('Innehåll saknas', 'error'); return; }
const status = asDraft ? 'DRAFT' : (scheduledAt ? 'SCHEDULED' : 'PENDING');
try {
await ensureAuth();
const r = await fetch(API + '/api/social-account/publish', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({
company: company,
platforms: platforms,
content: { text: text },
scheduledAt: scheduledAt,
status: status
})
});
const d = await r.json().catch(function() { return {}; });
if (r.ok) {
toast(asDraft ? ' Utkast sparat' : ' Inlägg schemalagt/publicerat', 'success');
clearPost();
loadPostQueue();
} else {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Fel: ' + (d.error || d.message || r.status), 'error');
}
} catch(e) {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel: ' + e.message, 'error');
}
}
function saveDraft() { submitPost(true); }
function clearPost() {
setVal('pubContent', '');
document.querySelectorAll('#pubPlatformChecks input[type=checkbox]').forEach(function(c) { c.checked = false; });
updatePreview();
updateCharCounter();
}
async function loadPostQueue() {
const container = document.getElementById('postQueueContent');
if (!container) return;
container.innerHTML = '<div class="state-box"><div class="spinner"></div><br>Laddar...</div>';
try {
await ensureAuth();
const r = await fetch(API + '/api/social-account/publish', { headers: authHeaders() });
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
const posts = Array.isArray(data) ? data : (data.posts || data.queue || []);
renderPostQueue(posts);
} catch(e) {
container.innerHTML = '<div class="state-box"><div class="icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></div><b>Kunde inte ladda kön</b><p>' + esc(e.message) + '</p></div>';
}
}
function renderPostQueue(posts) {
const container = document.getElementById('postQueueContent');
if (!container) return;
if (posts.length === 0) {
container.innerHTML = '<div class="state-box"><div class="icon"></div><b>Inga inlägg i kön</b><p>Skapa ett inlägg ovan.</p></div>';
return;
}
let html = '<table class="post-queue-table"><thead><tr><th>ID</th><th>Bolag</th><th>Plattformar</th><th>Status</th><th>Schemalagt</th><th>Innehåll</th><th>Åtgärder</th></tr></thead><tbody>';
posts.forEach(function(p) {
const id = esc(String(p.id || p.post_id || '—').slice(0, 10));
const company = esc(p.company || '—');
const platforms = Array.isArray(p.platforms)
? p.platforms.map(function(x) { return (PLATFORM_ICONS[x] || '') + ' ' + x; }).join(', ')
: esc(String(p.platforms || '—'));
const status = (p.status || 'pending').toLowerCase();
const sched = p.scheduledAt ? new Date(p.scheduledAt).toLocaleString('sv-SE') : (status === 'pending' ? 'Nu' : '—');
const contentText = p.content && p.content.text ? p.content.text : (p.text || '');
const preview = esc(contentText.slice(0, 60)) + (contentText.length > 60 ? '…' : '');
const badgeMap = { scheduled:'badge-scheduled', draft:'badge-draft', published:'badge-published', failed:'badge-failed', pending:'badge-running' };
const badgeCls = badgeMap[status] || 'badge-pending';
html += '<tr>';
html += '<td><code style="font-size:11px;color:#6366f1">' + id + '</code></td>';
html += '<td style="font-size:11px;">' + platforms + '</td>';
html += '<td><span class="badge ' + badgeCls + '">' + esc(status) + '</span></td>';
html += '<td style="font-size:11px;">' + esc(sched) + '</td>';
html += '<td style="font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + preview + '</td>';
html += '<td><button class="btn btn-ghost btn-sm" onclick="deletePost(' + JSON.stringify(p.id || p.post_id) + ')"></button></td>';
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
}
async function deletePost(postId) {
if (!confirm('Ta bort inlägg?')) return;
try {
const r = await fetch(API + '/api/social-account/publish/' + encodeURIComponent(postId), {
method: 'DELETE', headers: authHeaders()
});
if (r.ok) { toast(' Inlägg borttaget', 'info'); loadPostQueue(); }
else { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Radering misslyckades', 'error'); }
} catch(e) { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> ' + e.message, 'error'); }
}
/*
UTILITY
*/
function esc(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function copyText(text, btn) {
navigator.clipboard.writeText(text).then(function() {
const orig = btn.textContent;
btn.textContent = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
setTimeout(function() { btn.textContent = orig; }, 1500);
}).catch(function() {
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Kopiering misslyckades', 'error');
});
}
function togglePw(inputId, btn) {
const input = document.getElementById(inputId);
if (!input) return;
if (input.type === 'password') {
input.type = 'text';
btn.textContent = ' Dölj';
} else {
input.type = 'password';
btn.textContent = ' Visa';
}
}
function setStatus(online) {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
if (dot) dot.style.background = online ? '#22c55e' : '#ef4444';
if (text) text.textContent = online ? 'Online' : 'Offline';
}
/*
INIT
*/
async function init() {
try {
const tok = await ensureAuth();
setStatus(!!tok);
if (!tok) toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Autentisering misslyckades', 'error', 8000);
await loadPlatforms();
switchTab('register');
startAutoRefresh();
console.log('[ARGUS] Init complete — token:', tok ? tok.slice(0, 20) + '...' : 'EMPTY');
} catch(e) {
console.error('[ARGUS] Init error:', e);
setStatus(false);
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Initieringsfel: ' + e.message, 'error', 8000);
}
}
window.addEventListener('DOMContentLoaded', function() { init(); });
/*
EVENT DELEGATION for job buttons
*/
document.addEventListener('click', function(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.getAttribute('data-action');
const jobId = btn.getAttribute('data-jobid');
if (!jobId || jobId === '—') return;
if (action === 'approve') approveJob(jobId);
else if (action === 'reject') rejectJob(jobId);
else if (action === 'delete') deleteJob(jobId);
});
/*
ADD PLATFORM MODAL
*/
function openAddPlatformModal() {
var existing = document.getElementById('addPlatformModal');
if (existing) {
existing.classList.add('visible');
return;
}
// Create overlay
var overlay = document.createElement('div');
overlay.id = 'addPlatformModal';
overlay.className = 'modal-overlay';
overlay.style.cssText = 'z-index:1000;';
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeAddPlatformModal(); });
var companies = ['landvex','quixzoom','apifly','corpfitt','vyra','aamos'];
var companyLabels = {landvex:'LandveX',quixzoom:'QuiXzoom',apifly:'ApiFly',corpfitt:'CorpFitt',vyra:'VYRA',aamos:'AAMOS'};
var html = [
'<div style="background:#0d1117;border:1px solid #1e2737;border-radius: var(--radius-lg);padding:32px;max-width:560px;width:95%;max-height:90vh;overflow-y:auto;">',
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;">',
'<h3 style="font-size:18px;font-weight:700;color:#f1f5f9;margin:0;">+ Lägg till plattform</h3>',
'<button onclick="closeAddPlatformModal()" style="background:none;border:none;color:#64748b;font-size:20px;cursor:pointer;padding:4px 8px;border-radius: var(--radius-sm);" onmouseover="this.style.color=\'#e2e8f0\'" onmouseout="this.style.color=\'#64748b\'">&#x2715;</button>',
'</div>',
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">',
'<div class="form-group">',
'<label>Plattformsnamn</label>',
'<input type="text" id="ap_name" placeholder="t.ex. Mastodon" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'<div class="form-group">',
'<label>Ikon/Emoji</label>',
'<input type="text" id="ap_icon" placeholder="" value="" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'<div class="form-group" style="grid-column:span 2;">',
'<label>Registrerings-URL</label>',
'<input type="url" id="ap_url" placeholder="https://platform.com/signup" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'<div class="form-group">',
'<label>Login-URL</label>',
'<input type="url" id="ap_login" placeholder="https://platform.com/login" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'<div class="form-group">',
'<label>Teckenlängd max</label>',
'<input type="number" id="ap_charlimit" placeholder="500" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'</div>',
'<div class="form-group" style="margin-top:16px;">',
'<label>Bolag</label>',
'<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;">']
.concat(companies.map(function(c) {
return '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 10px;border:1px solid #1e2737;border-radius: var(--radius-md);font-size:13px;color:#cbd5e1;">' +
'<input type="checkbox" id="ap_co_' + c + '" value="' + c + '" style="width:auto;cursor:pointer;"> ' + companyLabels[c] + '</label>';
}))
.concat(['</div></div>',
'<div class="form-group" style="margin-top:16px;">',
'<label>Anteckning / Särskilda krav</label>',
'<textarea id="ap_notes" rows="3" placeholder="t.ex. kräver företagsverifiering, manuell godkännande..." style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;resize:vertical;"></textarea>',
'</div>',
'<div id="ap_confirm" style="display:none;background:#0f2818;border:1px solid #166534;border-radius: var(--radius-md);padding:10px 14px;margin-top:12px;color:#86efac;font-size:13px;font-weight:600;">',
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Plattform tillagd!',
'</div>',
'<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:24px;">',
'<button class="btn btn-secondary" onclick="closeAddPlatformModal()">Avbryt</button>',
'<button class="btn btn-primary" onclick="saveAddPlatform()">+ Spara plattform</button>',
'</div>',
'</div>'
]);
overlay.innerHTML = html.join('');
document.body.appendChild(overlay);
requestAnimationFrame(function() { overlay.classList.add('visible'); });
// Focus name field
setTimeout(function() {
var nameEl = document.getElementById('ap_name');
if (nameEl) nameEl.focus();
}, 150);
}
function closeAddPlatformModal() {
var overlay = document.getElementById('addPlatformModal');
if (overlay) overlay.classList.remove('visible');
}
async function saveAddPlatform() {
var name = (document.getElementById('ap_name') ? document.getElementById('ap_name').value : '').trim();
var icon = (document.getElementById('ap_icon') ? document.getElementById('ap_icon').value : '').trim() || '';
var url = (document.getElementById('ap_url') ? document.getElementById('ap_url').value : '').trim();
var login = (document.getElementById('ap_login') ? document.getElementById('ap_login').value : '').trim();
var charLimit = parseInt(document.getElementById('ap_charlimit') ? document.getElementById('ap_charlimit').value : '0') || null;
var notes = (document.getElementById('ap_notes') ? document.getElementById('ap_notes').value : '').trim();
var selectedCompanies = Array.from(document.querySelectorAll('#addPlatformModal input[type=checkbox]:checked'))
.map(function(c) { return c.value; });
if (!name) { toast('Plattformsnamn krävs', 'error'); return; }
var platformData = {
name: name.toLowerCase().replace(/[^a-z0-9]/g, ''),
displayName: name,
icon: icon,
signupUrl: url,
loginUrl: login,
charLimit: charLimit,
companies: selectedCompanies,
notes: notes,
addedAt: new Date().toISOString()
};
// Try API endpoint first
var saved = false;
try {
await ensureAuth();
var r = await fetch(API + '/api/social-account/custom-platform', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify(platformData)
});
if (r.ok) {
saved = true;
}
} catch(e) { /* fallback to localStorage */ }
// Fallback: localStorage
if (!saved) {
try {
var existing = JSON.parse(localStorage.getItem('argus_custom_platforms') || '[]');
existing.push(platformData);
localStorage.setItem('argus_custom_platforms', JSON.stringify(existing));
saved = true;
} catch(e) { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Kunde inte spara: ' + e.message, 'error'); return; }
}
// Update in-memory platform list
var key = platformData.name;
if (key && !platformList.includes(key)) {
platformList.push(key);
PLATFORM_ICONS[key] = icon;
if (charLimit) PLATFORM_CHAR_LIMITS[key] = charLimit;
if (login) PLATFORM_LOGIN[key] = login;
renderPlatformDropdown();
renderPubPlatformChecks();
}
// Show confirmation
var confirmEl = document.getElementById('ap_confirm');
if (confirmEl) confirmEl.style.display = '';
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Plattform "' + name + '" tillagd!', 'success');
setTimeout(function() { closeAddPlatformModal(); }, 1800);
}
</script>
</body>
</html>