762 lines
43 KiB
HTML
762 lines
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>PORTA · Lösungen (Solution-Schicht Mockup)</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&display=swap" rel="stylesheet" />
|
||
<style>
|
||
:root {
|
||
--color-bg: #F8F9FA;
|
||
--bg-primary: #ffffff;
|
||
--bg-secondary: #F7FAFC;
|
||
--bg-dark: #EDF2F7;
|
||
--surface-color: #F7FAFC;
|
||
--text-primary: #1A202C;
|
||
--text-secondary: #4A5568;
|
||
--text-tertiary: #718096;
|
||
--border-color: #E2E8F0;
|
||
--border-dark: #CBD5E0;
|
||
--primary-color: #4A6FA5;
|
||
--primary-color-dark: #3D5D8A;
|
||
--primary-light: rgba(74, 111, 165, 0.1);
|
||
--primary-color-light: rgba(74, 111, 165, 0.15);
|
||
--success: #38A169;
|
||
--success-bg: #C6F6D5;
|
||
--warning: #D69E2E;
|
||
--warning-bg: #FAF089;
|
||
--error: #C53030;
|
||
--error-bg: #FED7D7;
|
||
--gray: #718096;
|
||
--radius-lg: 10px;
|
||
--radius-md: 8px;
|
||
--radius-sm: 6px;
|
||
--font: "DM Sans", system-ui, -apple-system, sans-serif;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body { margin: 0; padding: 0; height: 100%; }
|
||
body {
|
||
font-family: var(--font);
|
||
background: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
font-size: 14px;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
/* ===== App shell ===== */
|
||
.app { display: flex; height: 100vh; overflow: hidden; }
|
||
.sidebar {
|
||
width: 280px; min-width: 280px; height: 100%;
|
||
background: var(--surface-color);
|
||
border-right: 1px solid var(--border-color);
|
||
display: flex; flex-direction: column;
|
||
}
|
||
.logo {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 20px 18px; border-bottom: 1px solid var(--border-color);
|
||
font-size: 22px; font-weight: 700; letter-spacing: -0.02em;
|
||
}
|
||
.logo .dot { width: 28px; height: 28px; border-radius: 8px; background: linear-gradient(135deg, var(--primary-color), var(--primary-color-dark)); display:flex; align-items:center; justify-content:center; color:#fff; font-size:15px; }
|
||
.logo .pow { color: var(--text-primary); }
|
||
.logo .on { color: var(--primary-color); }
|
||
.nav { flex: 1; overflow-y: auto; padding: 12px 10px; }
|
||
.nav .group { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-tertiary); padding: 14px 10px 6px; }
|
||
.nav a {
|
||
display: flex; align-items: center; gap: 10px;
|
||
padding: 9px 12px; border-radius: var(--radius-sm);
|
||
color: var(--text-secondary); text-decoration: none; font-weight: 500;
|
||
margin: 1px 0; cursor: pointer; font-size: 13.5px;
|
||
}
|
||
.nav a:hover { background: rgba(0,0,0,0.03); color: var(--text-primary); }
|
||
.nav a.active { background: var(--primary-light); color: var(--primary-color-dark); font-weight: 600; }
|
||
.nav a .ic { width: 18px; text-align: center; opacity: 0.85; }
|
||
.nav .sub { padding-left: 14px; border-left: 1px solid var(--border-color); margin-left: 16px; }
|
||
.userbox { padding: 14px; border-top: 1px solid var(--border-color); display: flex; align-items: center; gap: 10px; }
|
||
.avatar { width: 34px; height: 34px; border-radius: 50%; background: var(--primary-color); color: #fff; display:flex; align-items:center; justify-content:center; font-weight:600; font-size:13px; }
|
||
.userbox .meta { line-height: 1.2; }
|
||
.userbox .nm { font-weight: 600; font-size: 13px; }
|
||
.userbox .rl { font-size: 11px; color: var(--text-tertiary); }
|
||
|
||
/* ===== Main ===== */
|
||
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||
.topbar {
|
||
height: 56px; flex-shrink: 0; border-bottom: 1px solid var(--border-color);
|
||
display: flex; align-items: center; justify-content: space-between; padding: 0 24px;
|
||
background: var(--bg-primary);
|
||
}
|
||
.crumb { font-size: 13px; color: var(--text-tertiary); }
|
||
.crumb b { color: var(--text-primary); font-weight: 600; }
|
||
.topbar .right { display: flex; align-items: center; gap: 14px; color: var(--text-tertiary); font-size: 13px; }
|
||
.content { flex: 1; overflow-y: auto; padding: 24px 28px; background: var(--bg-primary); }
|
||
.wrap { max-width: 1180px; margin: 0 auto; }
|
||
|
||
/* ===== Page header ===== */
|
||
.pagehead { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
||
.pagehead h1 { margin: 0 0 4px; font-size: 23px; font-weight: 700; letter-spacing: -0.01em; }
|
||
.pagehead p { margin: 0; color: var(--text-secondary); font-size: 13.5px; max-width: 640px; }
|
||
|
||
/* ===== Buttons ===== */
|
||
.btn { display: inline-flex; align-items: center; gap: 8px; border: none; border-radius: var(--radius-sm); font-family: var(--font); font-weight: 500; font-size: 14px; padding: 8px 18px; cursor: pointer; transition: all .18s cubic-bezier(.4,0,.2,1); letter-spacing: .01em; white-space: nowrap; }
|
||
.btn-primary { background: linear-gradient(180deg, var(--primary-color), var(--primary-color-dark)); color: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.12), inset 0 1px 0 rgba(255,255,255,.12); }
|
||
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,.16); }
|
||
.btn-secondary { background: #fff; color: var(--text-secondary); border: 1px solid var(--border-color); box-shadow: 0 1px 2px rgba(0,0,0,.06); }
|
||
.btn-secondary:hover { background: var(--gray); color: #fff; border-color: var(--gray); }
|
||
.btn-success { background: linear-gradient(180deg, var(--success), #2F855A); color: #fff; }
|
||
.btn-success:hover { transform: translateY(-1px); }
|
||
.btn-ghost { background: transparent; color: var(--primary-color); border: 1px solid transparent; }
|
||
.btn-ghost:hover { background: var(--primary-light); }
|
||
.btn-sm { padding: 6px 13px; font-size: 12.5px; }
|
||
.btn[disabled] { opacity: .5; cursor: not-allowed; }
|
||
|
||
/* ===== Cards / list ===== */
|
||
.toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
|
||
.search { flex: 1; max-width: 320px; position: relative; }
|
||
.search input { width: 100%; padding: 8px 12px 8px 32px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-family: var(--font); font-size: 13.5px; background: #fff; }
|
||
.search .ic { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-tertiary); }
|
||
.seg { display: inline-flex; border: 1px solid var(--border-color); border-radius: var(--radius-sm); overflow: hidden; }
|
||
.seg button { border: none; background: #fff; padding: 7px 14px; font-family: var(--font); font-size: 13px; color: var(--text-secondary); cursor: pointer; }
|
||
.seg button.on { background: var(--primary-light); color: var(--primary-color-dark); font-weight: 600; }
|
||
.seg button + button { border-left: 1px solid var(--border-color); }
|
||
|
||
.sol-list { display: grid; gap: 12px; }
|
||
.sol-card {
|
||
border: 1px solid var(--border-color); border-radius: var(--radius-md); background: #fff;
|
||
padding: 16px 18px; display: grid; grid-template-columns: 42px 1fr auto; gap: 14px; align-items: center;
|
||
cursor: pointer; transition: all .15s ease;
|
||
}
|
||
.sol-card:hover { border-color: var(--border-dark); box-shadow: 0 4px 14px rgba(0,0,0,.06); transform: translateY(-1px); }
|
||
.sol-ic { width: 42px; height: 42px; border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; font-size: 20px; background: var(--primary-light); }
|
||
.sol-main .nm { font-weight: 600; font-size: 15px; margin-bottom: 3px; }
|
||
.sol-main .ds { color: var(--text-secondary); font-size: 13px; }
|
||
.sol-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; text-align: right; }
|
||
.sol-meta .line { font-size: 12px; color: var(--text-tertiary); }
|
||
.sol-meta .line b { color: var(--text-secondary); font-weight: 600; }
|
||
|
||
.badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; padding: 3px 9px; border-radius: 999px; }
|
||
.badge .d { width: 7px; height: 7px; border-radius: 50%; }
|
||
.b-active { background: var(--success-bg); color: #22643f; }
|
||
.b-active .d { background: var(--success); }
|
||
.b-inactive { background: #EDF2F7; color: var(--text-secondary); }
|
||
.b-inactive .d { background: var(--gray); }
|
||
.b-error { background: var(--error-bg); color: #822; }
|
||
.b-error .d { background: var(--error); }
|
||
.b-running { background: var(--primary-light); color: var(--primary-color-dark); }
|
||
.b-running .d { background: var(--primary-color); animation: pulse 1.2s infinite; }
|
||
.b-warn { background: #FEFCBF; color: #744210; }
|
||
.b-warn .d { background: var(--warning); }
|
||
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:.3;} }
|
||
|
||
/* ===== Detail ===== */
|
||
.backlink { display: inline-flex; align-items: center; gap: 6px; color: var(--text-secondary); font-size: 13px; cursor: pointer; margin-bottom: 14px; background:none; border:none; font-family:var(--font); padding:0;}
|
||
.backlink:hover { color: var(--primary-color); }
|
||
.detail-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 18px; }
|
||
.detail-head .sol-ic { width: 52px; height: 52px; font-size: 25px; }
|
||
.detail-head h1 { margin: 0 0 5px; font-size: 21px; }
|
||
.detail-head .sub { color: var(--text-secondary); font-size: 13px; }
|
||
.detail-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
|
||
|
||
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border-color); margin-bottom: 22px; }
|
||
.tabs button { border: none; background: none; font-family: var(--font); font-size: 14px; font-weight: 500; color: var(--text-secondary); padding: 11px 16px; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
||
.tabs button:hover { color: var(--text-primary); }
|
||
.tabs button.on { color: var(--primary-color-dark); border-bottom-color: var(--primary-color); font-weight: 600; }
|
||
|
||
.tabpane { display: none; }
|
||
.tabpane.on { display: block; animation: fade .2s ease; }
|
||
@keyframes fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
||
|
||
.grid2 { display: grid; grid-template-columns: 1fr 320px; gap: 22px; align-items: start; }
|
||
@media (max-width: 900px){ .grid2 { grid-template-columns: 1fr; } }
|
||
|
||
.card { border: 1px solid var(--border-color); border-radius: var(--radius-md); background: #fff; overflow: hidden; }
|
||
.card-h { padding: 13px 16px; border-bottom: 1px solid var(--border-color); font-weight: 600; font-size: 14px; background: var(--bg-secondary); display:flex; align-items:center; justify-content:space-between; }
|
||
.card-h .hint { font-weight: 500; font-size: 11.5px; color: var(--text-tertiary); }
|
||
.card-b { padding: 16px; }
|
||
|
||
.field { margin-bottom: 16px; }
|
||
.field:last-child { margin-bottom: 0; }
|
||
.field label { display: block; font-weight: 600; font-size: 12.5px; margin-bottom: 6px; }
|
||
.field .desc { font-size: 11.5px; color: var(--text-tertiary); margin: -3px 0 7px; }
|
||
.field input[type=text], .field input[type=number], .field select, .field textarea {
|
||
width: 100%; padding: 8px 11px; border: 1px solid var(--border-color); border-radius: var(--radius-sm);
|
||
font-family: var(--font); font-size: 13.5px; background: #fff; color: var(--text-primary);
|
||
}
|
||
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px var(--primary-light); }
|
||
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
.chips { display: flex; flex-wrap: wrap; gap: 7px; }
|
||
.chip { display: inline-flex; align-items: center; gap: 6px; background: var(--primary-light); color: var(--primary-color-dark); border-radius: 999px; padding: 5px 11px; font-size: 12.5px; font-weight: 500; }
|
||
.chip .x { cursor: pointer; opacity: .6; }
|
||
.chip.add { background: #fff; border: 1px dashed var(--border-dark); color: var(--text-secondary); cursor: pointer; }
|
||
.toggle { display: inline-flex; align-items: center; gap: 9px; cursor: pointer; }
|
||
.switch { width: 38px; height: 22px; border-radius: 999px; background: var(--success); position: relative; transition: background .2s; }
|
||
.switch::after { content: ""; position: absolute; top: 2px; left: 18px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left .2s; box-shadow: 0 1px 2px rgba(0,0,0,.2); }
|
||
.switch.off { background: var(--border-dark); }
|
||
.switch.off::after { left: 2px; }
|
||
|
||
.sidecard .kv { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px dashed var(--border-color); font-size: 13px; }
|
||
.sidecard .kv:last-child { border-bottom: none; }
|
||
.sidecard .kv span { color: var(--text-tertiary); }
|
||
.sidecard .kv b { font-weight: 600; }
|
||
|
||
table.runs { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
table.runs th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-tertiary); padding: 9px 14px; border-bottom: 1px solid var(--border-color); background: var(--bg-secondary); }
|
||
table.runs td { padding: 11px 14px; border-bottom: 1px solid var(--border-color); }
|
||
table.runs tr:hover td { background: var(--bg-secondary); }
|
||
table.runs tr:last-child td { border-bottom: none; }
|
||
.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12px; color: var(--text-secondary); }
|
||
|
||
/* run trace steps */
|
||
.trace { display: grid; gap: 8px; }
|
||
.step { display: grid; grid-template-columns: 24px 1fr auto; gap: 11px; align-items: center; padding: 10px 13px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); background: #fff; }
|
||
.step .dotc { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #fff; }
|
||
.step .nm { font-weight: 500; }
|
||
.step .nm small { color: var(--text-tertiary); font-weight: 400; }
|
||
.step .dur { font-size: 12px; color: var(--text-tertiary); }
|
||
.ok { background: var(--success); }
|
||
.warnc { background: var(--warning); }
|
||
|
||
/* output files */
|
||
.files { display: grid; gap: 10px; }
|
||
.file { display: grid; grid-template-columns: 38px 1fr auto; gap: 12px; align-items: center; padding: 11px 14px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); background: #fff; }
|
||
.file .pic { width: 38px; height: 38px; border-radius: var(--radius-sm); background: var(--error-bg); color: #9b2c2c; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 11px; }
|
||
.file .nm { font-weight: 500; }
|
||
.file .mt { font-size: 12px; color: var(--text-tertiary); }
|
||
|
||
.graph-strip { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 14px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-md); margin-top: 14px; }
|
||
.gnode { background: #fff; border: 1px solid var(--border-dark); border-radius: var(--radius-sm); padding: 7px 11px; font-size: 12px; font-weight: 500; display: flex; align-items: center; gap: 6px; }
|
||
.gnode .tag { font-size: 10px; background: var(--primary-light); color: var(--primary-color-dark); padding: 1px 6px; border-radius: 4px; font-weight: 600; }
|
||
.garrow { color: var(--text-tertiary); }
|
||
.gnode.ai { border-color: var(--primary-color); }
|
||
|
||
.note { display: flex; gap: 9px; padding: 11px 13px; border-radius: var(--radius-sm); font-size: 12.5px; line-height: 1.5; }
|
||
.note-info { background: var(--primary-light); color: var(--primary-color-dark); }
|
||
.note-warn { background: #FEFCBF; color: #744210; }
|
||
|
||
/* ===== Modal ===== */
|
||
.overlay { position: fixed; inset: 0; background: rgba(26,32,44,.5); display: none; align-items: center; justify-content: center; z-index: 100; padding: 24px; }
|
||
.overlay.on { display: flex; }
|
||
.modal { background: #fff; border-radius: var(--radius-lg); width: 100%; max-width: 720px; max-height: 88vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 24px 60px rgba(0,0,0,.3); }
|
||
.modal-h { padding: 18px 22px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; justify-content: space-between; }
|
||
.modal-h h2 { margin: 0; font-size: 18px; }
|
||
.modal-h .close { background: none; border: none; font-size: 22px; color: var(--text-tertiary); cursor: pointer; line-height: 1; }
|
||
.modal-b { padding: 22px; overflow-y: auto; }
|
||
.modal-f { padding: 16px 22px; border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; gap: 10px; }
|
||
|
||
.tmpl-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
@media (max-width: 640px){ .tmpl-grid { grid-template-columns: 1fr; } }
|
||
.tmpl { border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 14px; cursor: pointer; transition: all .15s; }
|
||
.tmpl:hover, .tmpl.sel { border-color: var(--primary-color); background: var(--primary-light); }
|
||
.tmpl .ti { font-size: 22px; margin-bottom: 8px; }
|
||
.tmpl .tn { font-weight: 600; font-size: 14px; margin-bottom: 3px; }
|
||
.tmpl .td { font-size: 12px; color: var(--text-secondary); }
|
||
.divider { display: flex; align-items: center; gap: 12px; margin: 20px 0; color: var(--text-tertiary); font-size: 12px; }
|
||
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--border-color); }
|
||
|
||
.toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px); background: var(--text-primary); color: #fff; padding: 12px 20px; border-radius: var(--radius-md); font-size: 13.5px; font-weight: 500; box-shadow: 0 8px 24px rgba(0,0,0,.25); opacity: 0; transition: all .25s; z-index: 200; display: flex; align-items: center; gap: 9px; }
|
||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||
.toast .d { width: 8px; height: 8px; border-radius: 50%; background: var(--success); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<!-- Sidebar -->
|
||
<aside class="sidebar">
|
||
<div class="logo"><span class="dot">P</span><span><span class="pow">Power</span><span class="on">On</span></span></div>
|
||
<nav class="nav">
|
||
<div class="group">Plattform</div>
|
||
<a><span class="ic">▦</span> Dashboard</a>
|
||
<a><span class="ic">✦</span> Workspace</a>
|
||
<div class="group">Feature · Trustee</div>
|
||
<a><span class="ic">▤</span> Übersicht</a>
|
||
<a><span class="ic">🧾</span> Belege</a>
|
||
<a><span class="ic">📊</span> Buchhaltungssystem</a>
|
||
<a><span class="ic">🔌</span> Datenquellen</a>
|
||
<a class="active"><span class="ic">⚙</span> Lösungen</a>
|
||
<div class="group">Administration</div>
|
||
<a><span class="ic">👥</span> Benutzer & Rollen</a>
|
||
<a><span class="ic">💳</span> Abrechnung</a>
|
||
</nav>
|
||
<div class="userbox">
|
||
<div class="avatar">PB</div>
|
||
<div class="meta"><div class="nm">Päde Bruderer</div><div class="rl">trustee-admin · Pling</div></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main -->
|
||
<div class="main">
|
||
<div class="topbar">
|
||
<div class="crumb">Trustee · <b id="crumb">Lösungen</b></div>
|
||
<div class="right"><span>Kaffee-Klatsch Holding</span><span>·</span><span>◑ Hell</span></div>
|
||
</div>
|
||
<div class="content">
|
||
<div class="wrap">
|
||
|
||
<!-- ===================== VIEW: LIST ===================== -->
|
||
<section id="viewList">
|
||
<div class="pagehead">
|
||
<div>
|
||
<h1>Lösungen</h1>
|
||
<p>Wiederkehrende Aufgaben als konfigurierte Workflows — einmal einstellen, automatisch laufen lassen, Ergebnisse ansehen. Kein Code nötig.</p>
|
||
</div>
|
||
<button class="btn btn-primary" onclick="openCatalog()"><span>+</span> Neue Lösung</button>
|
||
</div>
|
||
|
||
<div class="toolbar">
|
||
<div class="search"><span class="ic">⌕</span><input type="text" placeholder="Lösungen durchsuchen…" /></div>
|
||
<div class="seg">
|
||
<button class="on">Alle</button>
|
||
<button>Aktiv</button>
|
||
<button>Inaktiv</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sol-list" id="solList"></div>
|
||
</section>
|
||
|
||
<!-- ===================== VIEW: DETAIL ===================== -->
|
||
<section id="viewDetail" style="display:none;">
|
||
<button class="backlink" onclick="showList()">← Zurück zu Lösungen</button>
|
||
<div class="detail-head">
|
||
<div class="sol-ic" id="dIcon">📊</div>
|
||
<div>
|
||
<h1 id="dName">—</h1>
|
||
<div class="sub" id="dSub">—</div>
|
||
</div>
|
||
<div class="detail-actions">
|
||
<button class="btn btn-secondary btn-sm" onclick="toast('Workflow im Graphical Editor geöffnet')">Im Editor öffnen</button>
|
||
<button class="btn btn-success btn-sm" id="dRunBtn" onclick="runNow()">▶ Jetzt ausführen</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<button class="on" onclick="switchTab(event,'tEinst')">Einstellungen</button>
|
||
<button onclick="switchTab(event,'tTest')">Testlauf</button>
|
||
<button onclick="switchTab(event,'tRuns')">Läufe</button>
|
||
<button onclick="switchTab(event,'tOut')">Ausgabe</button>
|
||
</div>
|
||
|
||
<!-- TAB: Einstellungen -->
|
||
<div class="tabpane on" id="tEinst">
|
||
<div class="grid2">
|
||
<div style="display:grid; gap:18px;">
|
||
<div class="card">
|
||
<div class="card-h">Konfiguration <span class="hint">aus settingsSchema generiert</span></div>
|
||
<div class="card-b" id="settingsBody"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h">Trigger & Zeitplan</div>
|
||
<div class="card-b" id="triggerBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid; gap:18px;">
|
||
<div class="card sidecard">
|
||
<div class="card-h">Status</div>
|
||
<div class="card-b" id="statusBody"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-h">Workflow</div>
|
||
<div class="card-b">
|
||
<div style="font-size:12.5px; color:var(--text-secondary); margin-bottom:8px;">Diese Lösung kapselt einen Graphen aus vorhandenen Bausteinen:</div>
|
||
<div id="graphStrip"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top:18px;">
|
||
<button class="btn btn-secondary" onclick="showList()">Abbrechen</button>
|
||
<button class="btn btn-primary" onclick="toast('Einstellungen gespeichert')">Speichern</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Testlauf -->
|
||
<div class="tabpane" id="tTest">
|
||
<div class="card">
|
||
<div class="card-h">Probelauf (Dry-Run) <span class="hint">keine Mails, keine Seiteneffekte</span></div>
|
||
<div class="card-b">
|
||
<p style="margin-top:0; color:var(--text-secondary); font-size:13.5px;">Führt die Lösung mit den aktuellen Einstellungen testweise aus. Reports werden erzeugt und angezeigt, aber <b>nicht</b> versendet.</p>
|
||
<button class="btn btn-primary" onclick="startTest()">▶ Testlauf starten</button>
|
||
<div id="testTrace" style="margin-top:18px;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Läufe -->
|
||
<div class="tabpane" id="tRuns">
|
||
<div class="card">
|
||
<div class="card-h">Lauf-Historie</div>
|
||
<table class="runs" id="runsTable"></table>
|
||
</div>
|
||
<div class="card" style="margin-top:18px;">
|
||
<div class="card-h">Lauf-Trace <span class="hint">Lauf #1042 · 15.05.2026 06:00</span></div>
|
||
<div class="card-b">
|
||
<div class="trace" id="traceBody"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Ausgabe -->
|
||
<div class="tabpane" id="tOut">
|
||
<div class="card">
|
||
<div class="card-h">Erzeugte Dokumente <span class="hint">archiviert im Mandanten-Datenraum</span></div>
|
||
<div class="card-b"><div class="files" id="filesBody"></div></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===================== MODAL: Katalog / Neu ===================== -->
|
||
<div class="overlay" id="catalog">
|
||
<div class="modal">
|
||
<div class="modal-h">
|
||
<h2>Neue Lösung anlegen</h2>
|
||
<button class="close" onclick="closeCatalog()">×</button>
|
||
</div>
|
||
<div class="modal-b">
|
||
<div style="font-weight:600; margin-bottom:12px; font-size:14px;">Aus Vorlage (Use-Case-Katalog)</div>
|
||
<div class="tmpl-grid">
|
||
<div class="tmpl" onclick="selTmpl(this)"><div class="ti">🔄</div><div class="tn">Systeme synchronisieren</div><div class="td">Quelle → Ziel, periodisch (z. B. SelectLine → RMA)</div></div>
|
||
<div class="tmpl" onclick="selTmpl(this)"><div class="ti">📈</div><div class="tn">Periodisches Reporting</div><div class="td">Daten konsolidieren, PDF erzeugen, verteilen</div></div>
|
||
<div class="tmpl" onclick="selTmpl(this)"><div class="ti">📥</div><div class="tn">Dokumente verarbeiten</div><div class="td">Dateien einlesen, KI-Auswertung, Antwort</div></div>
|
||
<div class="tmpl" onclick="selTmpl(this)"><div class="ti">✉️</div><div class="tn">Benachrichtigung</div><div class="td">Bei Ereignis rollenbasiert mailen</div></div>
|
||
</div>
|
||
<div class="divider">oder mit KI beschreiben</div>
|
||
<div class="field">
|
||
<label>Beschreibe dein Bedürfnis — die KI baut den Workflow</label>
|
||
<div class="desc">Der Workflow-Agent komponiert daraus einen Graphen aus vorhandenen Bausteinen. Du konfigurierst danach nur noch die Einstellungen.</div>
|
||
<textarea rows="3" placeholder="z. B. «Schicke mir am 15. jedes Monats einen konsolidierten Bericht über alle 5 Store-Buchhaltungen und je einen Kennzahlen-Report an jeden Store-Manager.»"></textarea>
|
||
</div>
|
||
<div class="note note-info"><span>✦</span><span>Vorlagen werden deterministisch instanziiert (kein KI-Risiko). KI-Generierung erzeugt einen Entwurf, den du vor dem Aktivieren prüfst.</span></div>
|
||
</div>
|
||
<div class="modal-f">
|
||
<button class="btn btn-secondary" onclick="closeCatalog()">Abbrechen</button>
|
||
<button class="btn btn-primary" onclick="generateSolution()">✦ Lösung erstellen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"><span class="d"></span><span id="toastMsg">Gespeichert</span></div>
|
||
|
||
<script>
|
||
const solutions = {
|
||
pling: {
|
||
icon: "📊",
|
||
name: "Monatsreporting Kaffee-Klatsch",
|
||
sub: "Periodisches Reporting · 5 RMA-Buchhaltungen · aus Vorlage",
|
||
status: "active",
|
||
statusLabel: "Aktiv",
|
||
lastRun: "15.05.2026 06:00 · erfolgreich",
|
||
nextRun: "15.06.2026 06:00",
|
||
restricted: true,
|
||
kv: [
|
||
["Status", '<span class="badge b-active"><span class="d"></span>Aktiv</span>'],
|
||
["Letzter Lauf", "15.05.2026 · OK"],
|
||
["Nächster Lauf", "15.06.2026 06:00"],
|
||
["Läufe gesamt", "7"],
|
||
["Erfolgsquote", "100%"],
|
||
["Instanzen", "5 Stores"]
|
||
],
|
||
settings: `
|
||
<div class="field">
|
||
<label>Feature-Instanzen (Fan-out)</label>
|
||
<div class="desc">Über welche Buchhaltungen läuft die Lösung — eine Solution, mehrere Instanzen.</div>
|
||
<div class="chips">
|
||
<span class="chip">Coffeeshop Zürich <span class="x">×</span></span>
|
||
<span class="chip">Coffeeshop Bern <span class="x">×</span></span>
|
||
<span class="chip">Coffeeshop Basel <span class="x">×</span></span>
|
||
<span class="chip">Coffeeshop Luzern <span class="x">×</span></span>
|
||
<span class="chip">Coffeeshop St. Gallen <span class="x">×</span></span>
|
||
<span class="chip add">+ Instanz</span>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>Berichts-Periode</label>
|
||
<div class="row2">
|
||
<select><option>Vormonat (Standard)</option><option>Year-to-date</option><option>Freie Periode</option></select>
|
||
<input type="text" value="Mai 2026" />
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>Konten-Bereiche</label>
|
||
<div class="desc">Kundenspezifisch — als Settings, nicht im Code.</div>
|
||
<div class="row2">
|
||
<div><div style="font-size:11px;color:var(--text-tertiary);margin-bottom:4px;">Umsatz</div><input type="text" value="3000–3999"/></div>
|
||
<div><div style="font-size:11px;color:var(--text-tertiary);margin-bottom:4px;">Warenkosten</div><input type="text" value="6000–6299"/></div>
|
||
</div>
|
||
<div class="row2" style="margin-top:10px;">
|
||
<div><div style="font-size:11px;color:var(--text-tertiary);margin-bottom:4px;">Personalaufwand</div><input type="text" value="5000–5099"/></div>
|
||
<div></div>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>Empfänger (rollenbasiert)</label>
|
||
<div class="desc">Verteiler wird automatisch aus Trustee-Rollen aufgelöst — keine Listen pflegen (rbac.queryUsersByRole).</div>
|
||
<div class="chips">
|
||
<span class="chip">Holding-Bericht → trustee-accountant <span class="x">×</span></span>
|
||
<span class="chip">Store-Kennzahlen → trustee-client <span class="x">×</span></span>
|
||
<span class="chip">Lauf-Protokoll → trustee-admin <span class="x">×</span></span>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label class="toggle"><span class="switch off"></span> Bei unvollständiger Buchhaltung blockieren</label>
|
||
<div class="desc">Aus = durchlaufen & im Bericht markieren (stopOnIncomplete: false). Ein = warten bis alle 5 fertig.</div>
|
||
</div>`,
|
||
trigger: `
|
||
<div class="field">
|
||
<label class="toggle"><span class="switch"></span> Geplant (Zeitplan)</label>
|
||
<div class="desc" style="margin-left:48px;">Cron: <span class="mono">0 6 15 * *</span> · Europe/Zurich — am 15. um 06:00.</div>
|
||
</div>
|
||
<div class="row2">
|
||
<div class="field"><label>Tag im Monat</label><input type="number" value="15"/></div>
|
||
<div class="field"><label>Uhrzeit</label><input type="text" value="06:00"/></div>
|
||
</div>
|
||
<div class="field">
|
||
<label class="toggle"><span class="switch"></span> Manueller Start erlaubt</label>
|
||
</div>
|
||
<div class="field">
|
||
<label>Wer darf manuell starten</label>
|
||
<div class="desc">Zugriffskontrolle am Trigger (accessControl.requiredRoles).</div>
|
||
<select><option>Nur trustee-admin</option><option>trustee-admin + trustee-accountant</option><option>Alle mit Zugriff</option></select>
|
||
</div>`,
|
||
graph: [
|
||
["trigger", "Zeitplan / Manuell"],
|
||
["loop", "5 Instanzen"],
|
||
["trustee", "Salden holen"],
|
||
["data", "Konsolidieren"],
|
||
["ai", "6 PDFs erzeugen", true],
|
||
["rbac", "Empfänger auflösen"],
|
||
["outlook", "Versenden"]
|
||
],
|
||
runs: [
|
||
["#1042", "15.05.2026 06:00", "Zeitplan", "active", "Erfolgreich", "2m 14s", "6 PDFs · 8 Mails"],
|
||
["#1041", "15.04.2026 06:00", "Zeitplan", "warn", "Mit Hinweis", "2m 41s", "1 BuHa unvollständig"],
|
||
["#1038", "15.03.2026 06:00", "Zeitplan", "active", "Erfolgreich", "2m 02s", "6 PDFs · 8 Mails"],
|
||
["#1031", "21.02.2026 14:22", "Manuell · Päde", "active", "Erfolgreich", "2m 10s", "Ad-hoc YTD"]
|
||
],
|
||
trace: [
|
||
["ok", "trigger.scheduled", "Cron 0 6 15 * *", "0.1s"],
|
||
["ok", "flow.loop · 5 Instanzen", "parallel, concurrency 5", "1.2s"],
|
||
["ok", "trustee.refreshAccountingData", "5/5 Buchhaltungen gesynct", "44s"],
|
||
["ok", "trustee.queryData · Salden", "akt. + Vorjahr", "9s"],
|
||
["ok", "data.consolidate", "sumByKey → HoldingFigures", "0.6s"],
|
||
["ok", "ai.generateDocument · Holding", "1 PDF, neutralisiert", "31s"],
|
||
["ok", "ai.generateDocument · Store-KPI ×5", "5 PDFs", "26s"],
|
||
["ok", "rbac.queryUsersByRole", "8 Empfänger aufgelöst", "0.4s"],
|
||
["ok", "outlook.send", "8 Mails versendet", "3s"]
|
||
],
|
||
files: [
|
||
["Monatsbericht Holding Mai 2026", "PDF · 412 KB · Holding-Instanz", "PDF"],
|
||
["Kennzahlen Coffeeshop Zürich", "PDF · 88 KB · Store-Instanz", "PDF"],
|
||
["Kennzahlen Coffeeshop Bern", "PDF · 86 KB · Store-Instanz", "PDF"],
|
||
["Kennzahlen Coffeeshop Basel", "PDF · 87 KB · Store-Instanz", "PDF"],
|
||
["Kennzahlen Coffeeshop Luzern", "PDF · 85 KB · Store-Instanz", "PDF"],
|
||
["Kennzahlen Coffeeshop St. Gallen", "PDF · 84 KB · Store-Instanz", "PDF"]
|
||
]
|
||
},
|
||
selectline: {
|
||
icon: "🔄",
|
||
name: "SelectLine → RMA Sync",
|
||
sub: "Systeme synchronisieren · täglich · aus Vorlage",
|
||
status: "active", statusLabel: "Aktiv",
|
||
lastRun: "04.06.2026 06:00 · erfolgreich", nextRun: "05.06.2026 06:00",
|
||
restricted: false,
|
||
kv: [
|
||
["Status", '<span class="badge b-active"><span class="d"></span>Aktiv</span>'],
|
||
["Letzter Lauf", "04.06.2026 · OK"],
|
||
["Nächster Lauf", "05.06.2026 06:00"],
|
||
["Läufe gesamt", "63"],
|
||
["Erfolgsquote", "98%"]
|
||
],
|
||
settings: `
|
||
<div class="field"><label>Quell-System</label><select><option>SelectLine (API)</option></select></div>
|
||
<div class="field"><label>Ziel-System</label><select><option>RunMyAccounts (RMA)</option></select></div>
|
||
<div class="field">
|
||
<label>Konten-Mapping</label>
|
||
<div class="desc">Zuordnung Quell- zu Zielkonten — kundenspezifisch, als Settings.</div>
|
||
<div class="chips"><span class="chip">1000 → 1020 <span class="x">×</span></span><span class="chip">3400 → 3000 <span class="x">×</span></span><span class="chip add">+ Mapping</span></div>
|
||
</div>
|
||
<div class="field"><label class="toggle"><span class="switch"></span> Duplikat-Prüfung aktiv</label></div>`,
|
||
trigger: `
|
||
<div class="field"><label class="toggle"><span class="switch"></span> Geplant (täglich)</label><div class="desc" style="margin-left:48px;">Cron <span class="mono">0 6 * * *</span> · Europe/Zurich</div></div>
|
||
<div class="field"><label>Uhrzeit</label><input type="text" value="06:00"/></div>`,
|
||
graph: [["trigger","Täglich 06:00"],["selectline","Rechnungen lesen"],["data","Mapping"],["rma","Buchen", true]],
|
||
runs: [
|
||
["#0631","04.06.2026 06:00","Zeitplan","active","Erfolgreich","48s","14 Rechnungen"],
|
||
["#0630","03.06.2026 06:00","Zeitplan","active","Erfolgreich","51s","9 Rechnungen"],
|
||
["#0629","02.06.2026 06:00","Zeitplan","error","Fehler","12s","API-Timeout SelectLine"]
|
||
],
|
||
trace: [
|
||
["ok","trigger.scheduled","Cron 0 6 * * *","0.1s"],
|
||
["ok","selectline.listInvoices","14 neue Rechnungen","6s"],
|
||
["ok","data.mapAccounts","Mapping angewandt","0.3s"],
|
||
["ok","rma.pushInvoice","14 gebucht, 0 Duplikate","41s"]
|
||
],
|
||
files: [["Sync-Protokoll 04.06.2026","CSV · 12 KB","CSV"]]
|
||
},
|
||
pwg: {
|
||
icon: "📥",
|
||
name: "Mietzins-Bestätigungen (PWG)",
|
||
sub: "Dokumente verarbeiten · Pilot · per KI generiert",
|
||
status: "inactive", statusLabel: "Inaktiv (Entwurf)",
|
||
lastRun: "—", nextRun: "nicht aktiviert",
|
||
restricted: false,
|
||
kv: [
|
||
["Status", '<span class="badge b-inactive"><span class="d"></span>Inaktiv</span>'],
|
||
["Herkunft", "KI-Entwurf"],
|
||
["Letzter Lauf", "—"],
|
||
["Nächster Lauf", "nicht aktiviert"]
|
||
],
|
||
settings: `
|
||
<div class="field"><label>SharePoint-Ordner</label><input type="text" value="/Mietzins/Eingang 2026"/></div>
|
||
<div class="field"><label>Abacus-Mandant</label><select><option>PWG Immobilien AG</option></select></div>
|
||
<div class="field"><label>Empfänger (Übersicht)</label><div class="chips"><span class="chip">trustee-accountant <span class="x">×</span></span><span class="chip add">+ Rolle</span></div></div>
|
||
<div class="note note-warn"><span>⚠</span><span>KI-Entwurf — bitte Einstellungen prüfen und einen Testlauf machen, bevor du aktivierst.</span></div>`,
|
||
trigger: `<div class="field"><label class="toggle"><span class="switch off"></span> Geplant</label><div class="desc" style="margin-left:48px;">Noch nicht aktiviert.</div></div>`,
|
||
graph: [["trigger","Manuell"],["sharepoint","Dateien lesen"],["loop","pro Datei"],["ai","Extrahieren", true],["data","Schreiben"],["email","Antwort"]],
|
||
runs: [],
|
||
trace: [],
|
||
files: []
|
||
}
|
||
};
|
||
|
||
const listOrder = ["pling","selectline","pwg"];
|
||
|
||
function _badge(s){
|
||
const map = { active:["b-active","Aktiv"], inactive:["b-inactive","Inaktiv"], error:["b-error","Fehler"], warn:["b-warn","Hinweis"], running:["b-running","Läuft"] };
|
||
const [cls,lbl] = map[s] || map.inactive;
|
||
return `<span class="badge ${cls}"><span class="d"></span>${lbl}</span>`;
|
||
}
|
||
|
||
function renderList(){
|
||
const el = document.getElementById("solList");
|
||
el.innerHTML = listOrder.map(id => {
|
||
const s = solutions[id];
|
||
return `<div class="sol-card" onclick="showDetail('${id}')">
|
||
<div class="sol-ic">${s.icon}</div>
|
||
<div class="sol-main">
|
||
<div class="nm">${s.name}</div>
|
||
<div class="ds">${s.sub}</div>
|
||
</div>
|
||
<div class="sol-meta">
|
||
${_badge(s.status)}
|
||
<div class="line">Letzter Lauf: <b>${s.lastRun}</b></div>
|
||
<div class="line">Nächster: <b>${s.nextRun}</b></div>
|
||
</div>
|
||
</div>`;
|
||
}).join("");
|
||
}
|
||
|
||
let current = null;
|
||
function showDetail(id){
|
||
current = id;
|
||
const s = solutions[id];
|
||
document.getElementById("crumb").textContent = s.name;
|
||
document.getElementById("dIcon").textContent = s.icon;
|
||
document.getElementById("dName").textContent = s.name;
|
||
document.getElementById("dSub").textContent = s.sub;
|
||
document.getElementById("settingsBody").innerHTML = s.settings;
|
||
document.getElementById("triggerBody").innerHTML = s.trigger;
|
||
document.getElementById("statusBody").innerHTML = s.kv.map(([k,v]) => `<div class="kv"><span>${k}</span><b>${v}</b></div>`).join("");
|
||
|
||
document.getElementById("graphStrip").innerHTML = `<div class="graph-strip">` + s.graph.map((g,i) => {
|
||
const arrow = i < s.graph.length-1 ? '<span class="garrow">→</span>' : '';
|
||
return `<div class="gnode ${g[2]?'ai':''}"><span class="tag">${g[0]}</span>${g[1]}</div>${arrow}`;
|
||
}).join("") + `</div>`;
|
||
|
||
// run button restriction
|
||
const rb = document.getElementById("dRunBtn");
|
||
if (s.restricted){ rb.title = "Nur trustee-admin"; }
|
||
|
||
// runs table
|
||
const rt = document.getElementById("runsTable");
|
||
if (s.runs.length){
|
||
rt.innerHTML = `<thead><tr><th>Lauf</th><th>Zeitpunkt</th><th>Auslöser</th><th>Status</th><th>Dauer</th><th>Ergebnis</th></tr></thead><tbody>` +
|
||
s.runs.map(r => `<tr><td class="mono">${r[0]}</td><td>${r[1]}</td><td>${r[2]}</td><td>${_badge(r[3])}</td><td>${r[5]}</td><td>${r[6]}</td></tr>`).join("") +
|
||
`</tbody>`;
|
||
} else {
|
||
rt.innerHTML = `<tbody><tr><td style="padding:24px; text-align:center; color:var(--text-tertiary);">Noch keine Läufe — Lösung ist nicht aktiviert.</td></tr></tbody>`;
|
||
}
|
||
|
||
// trace
|
||
document.getElementById("traceBody").innerHTML = s.trace.length
|
||
? s.trace.map(t => `<div class="step"><span class="dotc ${t[0]==='ok'?'ok':'warnc'}">${t[0]==='ok'?'✓':'!'}</span><span class="nm">${t[1]} <small>· ${t[2]}</small></span><span class="dur">${t[3]}</span></div>`).join("")
|
||
: `<div style="color:var(--text-tertiary); font-size:13px;">Kein Lauf vorhanden.</div>`;
|
||
|
||
// files
|
||
document.getElementById("filesBody").innerHTML = s.files.length
|
||
? s.files.map(f => `<div class="file"><span class="pic">${f[2]}</span><div><div class="nm">${f[0]}</div><div class="mt">${f[1]}</div></div><button class="btn btn-ghost btn-sm" onclick="toast('Download (Mockup)')">Öffnen</button></div>`).join("")
|
||
: `<div style="color:var(--text-tertiary); font-size:13px;">Noch keine Dokumente erzeugt.</div>`;
|
||
|
||
document.getElementById("viewList").style.display = "none";
|
||
document.getElementById("viewDetail").style.display = "block";
|
||
// reset to first tab
|
||
document.querySelectorAll("#viewDetail .tabs button").forEach((b,i)=>b.classList.toggle("on",i===0));
|
||
document.querySelectorAll("#viewDetail .tabpane").forEach((p,i)=>p.classList.toggle("on",p.id==="tEinst"));
|
||
document.getElementById("testTrace").innerHTML = "";
|
||
document.querySelector(".content").scrollTop = 0;
|
||
}
|
||
|
||
function showList(){
|
||
current = null;
|
||
document.getElementById("crumb").textContent = "Lösungen";
|
||
document.getElementById("viewDetail").style.display = "none";
|
||
document.getElementById("viewList").style.display = "block";
|
||
}
|
||
|
||
function switchTab(e, id){
|
||
document.querySelectorAll("#viewDetail .tabs button").forEach(b=>b.classList.remove("on"));
|
||
e.currentTarget.classList.add("on");
|
||
document.querySelectorAll("#viewDetail .tabpane").forEach(p=>p.classList.toggle("on",p.id===id));
|
||
}
|
||
|
||
function startTest(){
|
||
const s = solutions[current];
|
||
const box = document.getElementById("testTrace");
|
||
box.innerHTML = `<div class="note note-info"><span>✦</span><span>Testlauf gestartet — Dry-Run, keine Mails werden versendet.</span></div><div class="trace" id="liveTrace" style="margin-top:12px;"></div>`;
|
||
const lt = document.getElementById("liveTrace");
|
||
const steps = s.trace.length ? s.trace : [["ok","workflow.run","Beispiel-Schritt","1s"]];
|
||
let i = 0;
|
||
(function next(){
|
||
if (i >= steps.length){ toast("Testlauf erfolgreich · 6 Dokumente erzeugt (nicht versendet)"); return; }
|
||
const t = steps[i];
|
||
const div = document.createElement("div");
|
||
div.className = "step";
|
||
div.innerHTML = `<span class="dotc ${t[0]==='ok'?'ok':'warnc'}">${t[0]==='ok'?'✓':'!'}</span><span class="nm">${t[1]} <small>· ${t[2]}</small></span><span class="dur">${t[3]}</span>`;
|
||
div.style.opacity = 0;
|
||
lt.appendChild(div);
|
||
requestAnimationFrame(()=>{ div.style.transition="opacity .25s"; div.style.opacity=1; });
|
||
i++; setTimeout(next, 420);
|
||
})();
|
||
}
|
||
|
||
function runNow(){
|
||
const s = solutions[current];
|
||
if (s.restricted) toast("Lauf gestartet (Rolle trustee-admin bestätigt) ▶");
|
||
else toast("Lauf gestartet ▶");
|
||
}
|
||
|
||
// catalog modal
|
||
function openCatalog(){ document.getElementById("catalog").classList.add("on"); }
|
||
function closeCatalog(){ document.getElementById("catalog").classList.remove("on"); document.querySelectorAll(".tmpl").forEach(t=>t.classList.remove("sel")); }
|
||
function selTmpl(el){ document.querySelectorAll(".tmpl").forEach(t=>t.classList.remove("sel")); el.classList.add("sel"); }
|
||
function generateSolution(){ closeCatalog(); toast("✦ Workflow erstellt — jetzt Einstellungen konfigurieren"); showDetail("pling"); }
|
||
|
||
// toast
|
||
let toastTimer;
|
||
function toast(msg){
|
||
const t = document.getElementById("toast");
|
||
document.getElementById("toastMsg").textContent = msg;
|
||
t.classList.add("show");
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(()=>t.classList.remove("show"), 2600);
|
||
}
|
||
|
||
document.getElementById("catalog").addEventListener("click", e => { if (e.target.id === "catalog") closeCatalog(); });
|
||
|
||
renderList();
|
||
</script>
|
||
</body>
|
||
</html>
|