aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
1850 lines
87 KiB
HTML
1850 lines
87 KiB
HTML
<!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: 8px; 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: 50%; 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: 10px; 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: 12px; 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: 8px; 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: 8px; 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: 6px; 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: 20px; 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: 10px;
|
||
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: 20px; 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: 6px; 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: 8px;
|
||
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: 4px; padding: 3px 8px; color: #fbbf24; font-family: monospace; font-size: 12px; width: 160px; }
|
||
.copy-btn { background: #6366f1; color: #fff; border: none; border-radius: 4px; 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: 4px; 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: 8px;
|
||
padding: 10px 12px; margin-top: 8px;
|
||
}
|
||
.mail-code-result {
|
||
display: none; background: #1e293b; border-radius: 6px; 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: 10px; 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: 8px;
|
||
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: 10px; 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: 8px; 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: 50%; 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: 10px; 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: 16px; 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: 8px; 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>🔄 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()">🔄 Rotera nu</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Employee Offboard Modal -->
|
||
<div class="modal-overlay" id="offboardModal">
|
||
<div class="modal">
|
||
<h3>⚠️ 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()">⚠️ 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')">🏢 LandveX</button>
|
||
<button class="company-tab" data-company="quixzoom" onclick="selectCompany('quixzoom')">⚡ QuiXzoom</button>
|
||
<button class="company-tab" data-company="apifly" onclick="selectCompany('apifly')">🚀 ApiFly</button>
|
||
<button class="company-tab" data-company="corpfitt" onclick="selectCompany('corpfitt')">💪 CorpFitt</button>
|
||
<button class="company-tab" data-company="vyra" onclick="selectCompany('vyra')">✨ 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')">📝 Registrera</button>
|
||
<button class="main-tab" data-tab="jobs" onclick="switchTab('jobs')">⚙️ Jobb</button>
|
||
<button class="main-tab" data-tab="credentials" onclick="switchTab('credentials')">🔐 Credentials</button>
|
||
<button class="main-tab" data-tab="publishing" onclick="switchTab('publishing')">📢 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">🏢 LandveX</option>
|
||
<option value="quixzoom">⚡ QuiXzoom</option>
|
||
<option value="apifly">🚀 ApiFly</option>
|
||
<option value="corpfitt">💪 CorpFitt</option>
|
||
<option value="vyra">✨ 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:8px; 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()">🚀 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()">✅ 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;">🔄 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()">🔄 Rotera lösenord</button>
|
||
<button class="btn btn-danger btn-sm" onclick="document.getElementById('offboardModal').classList.add('visible')">⚠️ Anställd slutar</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="loadCredentials()">🔄 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">📝 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">🏢 LandveX</option>
|
||
<option value="quixzoom">⚡ QuiXzoom</option>
|
||
<option value="apifly">🚀 ApiFly</option>
|
||
<option value="corpfitt">💪 CorpFitt</option>
|
||
<option value="vyra">✨ 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')">⚡ 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:8px; 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:10px;">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">📋 Publiceringsköen</div>
|
||
<button class="btn btn-secondary btn-sm" onclick="loadPostQueue()">🔄 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 — 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 — 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 — 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: '✅',
|
||
title: 'Granska & godkänn',
|
||
mailbox: false,
|
||
steps: [
|
||
'1. Botten har fyllt i formuläret automatiskt',
|
||
'2. Granska skärmdumpen ovan — ser den korrekt ut?',
|
||
'3. Om allt ser bra ut — 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:8px;';
|
||
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('✅ Automation startad för ' + platform + ' (' + company + ')', 'success');
|
||
switchTab('jobs');
|
||
} else {
|
||
toast('❌ Fel: ' + (d.error || d.message || r.status), 'error');
|
||
}
|
||
} catch(e) {
|
||
toast('❌ 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:10px;padding:1px 6px;font-size:10px;margin-left:4px">✅'+c.done+'</span>';
|
||
if (c.running > 0) badges += '<span style="background:#2563eb;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;margin-left:4px">⚡'+c.running+'</span>';
|
||
if (c.awaiting > 0) badges += '<span style="background:#d97706;color:#fff;border-radius:10px;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:10px;padding:1px 6px;font-size:10px;margin-left:4px">❌'+c.error+'</span>';
|
||
if (c.manual > 0) badges += '<span style="background:#7c3aed;color:#fff;border-radius:10px;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">⚠️</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('✅ Alla ' + ok + ' jobb godkända', 'success');
|
||
} else {
|
||
toast('⚠️ 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:4px;color:#6ee7b7;font-size:.85em">' + email + '</code>')
|
||
.replace(/{username}/g, '<code style="background:#1e293b;padding:2px 8px;border-radius:4px;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">🔐 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:8px;margin-top:10px">' +
|
||
info.icon + ' Logga in på ' + esc(job.platform) + ' →</a>';
|
||
|
||
return '<div style="background:#0a0e1a;border:1px solid #1e2a38;border-radius:10px;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 = '✅ Hämtad';
|
||
} else {
|
||
toast('⚠️ 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('❌ Fel vid hämtning: ' + (d.error || r.status), 'error');
|
||
btn.disabled = false;
|
||
btn.textContent = '📨 Hämta kod';
|
||
}
|
||
} catch(e) {
|
||
toast('❌ 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: '✅ Klar',
|
||
failed: '❌ 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">✅ 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)) + '">✅ Godkänn</button> ';
|
||
actions += '<button class="btn btn-danger btn-sm" data-action="reject" data-jobid="' + esc(String(id)) + '">❌ 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('✅ Jobb godkänt', 'success');
|
||
loadJobs();
|
||
} else {
|
||
const d = await r.json().catch(function() { return {}; });
|
||
toast('❌ Godkännande misslyckades: ' + (d.error || r.status), 'error');
|
||
}
|
||
} catch(e) { toast('❌ ' + 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('❌ Radering misslyckades: ' + (d.error || r.status), 'error');
|
||
}
|
||
} catch(e) { toast('❌ ' + 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">⚠️</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">🔐</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('🔄 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('✅ Lösenord roterade för ' + rotated + ' bolag', 'success');
|
||
} else {
|
||
toast('⚠️ 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' ? '✅ ' + (item.platforms || 0) + ' plattformar' : '❌ 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:10px;">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;">⚠️ ' + (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('❌ Fel: ' + (d.error || d.message || r.status), 'error');
|
||
}
|
||
} catch(e) {
|
||
toast('❌ 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">⚠️</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('❌ Radering misslyckades', 'error'); }
|
||
} catch(e) { toast('❌ ' + e.message, 'error'); }
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════
|
||
UTILITY
|
||
═══════════════════════════════════════════════════════════ */
|
||
function esc(s) {
|
||
return String(s)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function copyText(text, btn) {
|
||
navigator.clipboard.writeText(text).then(function() {
|
||
const orig = btn.textContent;
|
||
btn.textContent = '✓';
|
||
setTimeout(function() { btn.textContent = orig; }, 1500);
|
||
}).catch(function() {
|
||
toast('❌ 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('⚠️ 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('❌ 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:16px;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:6px;" onmouseover="this.style.color=\'#e2e8f0\'" onmouseout="this.style.color=\'#64748b\'">✕</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:8px;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:8px;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:8px;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:8px;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:8px;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:8px;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:8px;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:8px;padding:10px 14px;margin-top:12px;color:#86efac;font-size:13px;font-weight:600;">',
|
||
'✅ 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('❌ 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('✅ Plattform "' + name + '" tillagd!', 'success');
|
||
|
||
setTimeout(function() { closeAddPlatformModal(); }, 1800);
|
||
}
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|