Files
boc/backups/2026-07-12-1023/landvex-prod/admin-social-temp.html
T

1850 lines
99 KiB
HTML
Raw Normal View History

<!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>