aether-framework/resources/[aether]/aether-jobs/html/index.html
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

341 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
</head>
<body>
<title>Aether — Jobs</title>
<style>
:root{
--bg:#080a0f;--fg:#e9ecf4;--muted:#828a9e;--surface:rgba(22,26,36,0.72);--surface2:rgba(34,39,52,0.85);
--slot:#111520;--border:rgba(255,255,255,0.09);--accent:#4aa8ff;--ink:#04162f;--accent-soft:rgba(74,168,255,0.16);
--green:#34d39a;--amber:#f5b23d;--red:#f0506e;
}
*{box-sizing:border-box;} html,body{margin:0;height:100%;}
body{min-height:100%;background:radial-gradient(1100px 700px at 15% 0%,var(--accent-soft),transparent 55%),radial-gradient(900px 700px at 90% 100%,rgba(52,211,153,0.05),transparent 55%),var(--bg);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);
display:flex;align-items:center;justify-content:center;padding:22px 14px;overflow-x:hidden;transition:background .4s;}
.app{position:relative;width:min(1200px,98vw);aspect-ratio:16/10;border-radius:20px;overflow:hidden;
background:linear-gradient(160deg,rgba(18,22,30,0.92),rgba(9,11,16,0.96));backdrop-filter:blur(20px);border:1px solid var(--border);
box-shadow:0 50px 100px -30px rgba(0,0,0,0.85);display:grid;grid-template-columns:210px 1fr;grid-template-rows:auto 1fr;grid-template-areas:"brand top" "nav main";}
.brand{grid-area:brand;padding:14px 16px;border-right:1px solid var(--border);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;}
.brand .bl{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;font-size:19px;background:linear-gradient(150deg,var(--accent),#1f5fb0);}
.brand .bn{font-size:16px;font-weight:800;letter-spacing:-0.02em;} .brand .bn small{display:block;font-size:10px;color:var(--muted);font-weight:500;}
.top{grid-area:top;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:14px;padding:0 18px;}
.top .status{display:flex;align-items:center;gap:8px;font-size:12.5px;font-weight:700;} .status .dot{width:9px;height:9px;border-radius:50%;background:var(--muted);} .status .dot.on{background:var(--green);box-shadow:0 0 8px var(--green);}
.top .money{margin-left:auto;display:flex;gap:10px;} .money-chip{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:7px 13px;text-align:right;} .money-chip .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;font-weight:800;} .money-chip .v{font-size:14px;font-weight:800;font-variant-numeric:tabular-nums;}
.nav{grid-area:nav;border-right:1px solid var(--border);padding:12px 9px;display:flex;flex-direction:column;gap:2px;overflow-y:auto;}
.nav button{display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:9px;border:none;background:none;color:var(--muted);font-family:inherit;font-size:13.5px;font-weight:700;cursor:pointer;text-align:left;transition:.12s;}
.nav button .ni{font-size:15px;width:18px;text-align:center;} .nav button:hover{background:var(--surface);color:var(--fg);} .nav button.active{background:var(--accent-soft);color:var(--accent);} .nav button:disabled{opacity:.4;cursor:not-allowed;}
.main{grid-area:main;overflow-y:auto;padding:20px 24px;} .main::-webkit-scrollbar,.nav::-webkit-scrollbar{width:7px;} .main::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.09);border-radius:4px;}
.phead{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:16px;} .phead h2{font-size:21px;margin:0;font-weight:800;letter-spacing:-0.02em;} .phead p{margin:2px 0 0;color:var(--muted);font-size:12.5px;}
.jobgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:14px;}
.jobcard{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:16px;transition:.12s;position:relative;overflow:hidden;}
.jobcard.aktiv{border-color:var(--green);box-shadow:0 0 0 1px var(--green) inset;}
.jobcard .ico{width:48px;height:48px;border-radius:12px;display:grid;place-items:center;font-size:26px;margin-bottom:10px;}
.jobcard .jn{font-size:16px;font-weight:800;} .jobcard .jk{font-size:11.5px;color:var(--muted);} .jobcard .jd{font-size:12.5px;color:#b9c0cf;margin:8px 0 10px;line-height:1.4;min-height:34px;}
.jobcard .lvl{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted);margin-bottom:10px;}
.lvlbar{flex:1;height:6px;border-radius:4px;background:var(--slot);overflow:hidden;} .lvlbar>i{display:block;height:100%;border-radius:4px;background:var(--accent);}
.btn{border:none;border-radius:11px;padding:11px 16px;font-size:14px;font-weight:800;cursor:pointer;font-family:inherit;transition:.12s;width:100%;} .btn.primary{background:var(--accent);color:#fff;} .btn.primary:hover{filter:brightness(1.08);} .btn.ghost{background:var(--surface2);color:var(--fg);border:1px solid var(--border);} .btn.green{background:var(--green);color:#06231a;} .btn.red{background:var(--red);color:#2a0910;} .btn:disabled{opacity:.4;cursor:not-allowed;} .btn:active{transform:scale(0.99);} .btn.wide{padding:15px;font-size:15px;}
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:18px;}
.list-h{font-size:10.5px;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);font-weight:800;margin:0 0 10px;}
.kpi .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em;font-weight:800;} .kpi .v{font-size:24px;font-weight:800;font-variant-numeric:tabular-nums;margin-top:3px;}
.pill{font-size:10px;font-weight:800;padding:3px 9px;border-radius:999px;display:inline-block;} .pill.g{background:rgba(52,211,153,0.16);color:var(--green);} .pill.a{background:rgba(245,178,61,0.16);color:var(--amber);} .pill.ac{background:var(--accent-soft);color:var(--accent);}
/* Arbeits-Ansicht */
.work-head{display:flex;align-items:center;gap:16px;margin-bottom:16px;}
.work-head .ico{width:64px;height:64px;border-radius:16px;display:grid;place-items:center;font-size:34px;flex-shrink:0;}
.work-head .wt{font-size:22px;font-weight:800;} .work-head .wl{font-size:12.5px;color:var(--muted);}
.xpbar{height:8px;border-radius:5px;background:var(--slot);overflow:hidden;margin-top:6px;width:220px;} .xpbar>i{display:block;height:100%;border-radius:5px;background:var(--accent);transition:width .4s;}
.task{background:linear-gradient(150deg,var(--accent-soft),transparent);border:1px solid var(--border);border-radius:16px;padding:22px;text-align:center;}
.task .auf{font-size:12px;color:var(--muted);font-weight:700;letter-spacing:0.04em;text-transform:uppercase;}
.task .ziel{font-size:20px;font-weight:800;margin:6px 0 3px;} .task .schritt{font-size:14px;color:#c2cbd8;margin-bottom:16px;}
.task .lohn{display:inline-block;background:var(--slot);border:1px solid var(--border);border-radius:999px;padding:5px 14px;font-weight:800;color:var(--green);margin-bottom:16px;}
.workprog{height:12px;border-radius:7px;background:var(--slot);overflow:hidden;margin:0 auto 14px;max-width:420px;} .workprog>i{display:block;height:100%;width:0;border-radius:7px;background:var(--accent);} .workprog>i.fill{animation:fill 1s linear forwards;} @keyframes fill{to{width:100%;}}
.stepbar{display:flex;gap:6px;justify-content:center;margin-bottom:16px;} .stepbar .sd{width:26px;height:6px;border-radius:3px;background:var(--slot);} .stepbar .sd.done{background:var(--green);} .stepbar .sd.cur{background:var(--accent);}
.toast{position:absolute;bottom:16px;left:50%;transform:translateX(-50%) translateY(16px);background:rgba(12,15,22,0.97);border:1px solid var(--border);color:var(--fg);padding:11px 20px;border-radius:12px;font-size:13.5px;font-weight:600;z-index:50;opacity:0;pointer-events:none;transition:.3s;} .toast.show{opacity:1;transform:translateX(-50%) translateY(0);} .toast.good{border-color:var(--green);} .toast.warn{border-color:var(--red);} .toast.level{border-color:var(--amber);}
.empty{text-align:center;color:var(--muted);padding:44px 10px;} .empty .big{font-size:40px;margin-bottom:8px;}
/* Hinweis, dass die Arbeit in der Welt stattfindet */
.ortshinweis{background:rgba(255,255,255,0.05);border:1px dashed var(--border);border-radius:12px;
padding:14px 16px;font-size:13.5px;line-height:1.6;color:var(--muted);margin-top:4px;}
.ortshinweis b{color:var(--fg);}
.ortshinweis .ortname{margin-top:6px;font-size:12.5px;color:var(--accent);font-weight:700;}
</style>
<div class="app">
<div class="brand"><span class="bl">💼</span><div class="bn">Aether Jobs<small>Arbeitsvermittlung</small></div></div>
<div class="top" id="top"></div>
<div class="nav" id="nav"></div>
<div class="main" id="main"></div>
<div class="toast" id="toast"></div>
</div>
<script>
"use strict";
const $=id=>document.getElementById(id);
function sicher(t){const d=document.createElement("div");d.textContent=t==null?"":t;return d.innerHTML;}
const euro=n=>Math.round(n).toLocaleString("de-DE")+" €";
const rnd=(a,b)=>a+Math.floor(Math.random()*(b-a+1));
const pick=a=>a[Math.floor(Math.random()*a.length)];
let toastTimer=null; function toast(t,typ){const e=$("toast");e.textContent=t;e.className="toast show"+(typ?" "+typ:"");clearTimeout(toastTimer);toastTimer=setTimeout(()=>e.classList.remove("show"),2600);}
/* =====================================================================
SPIELER & FORTSCHRITT
===================================================================== */
let spieler={bargeld:0,konto:0}; // vom Server befuellt
let XP_PRO_LEVEL=120;
const orte=["Legion Square","Vinewood Blvd","Vespucci Beach","Grove Street","Sandy Shores","Del Perro","Mirror Park","Downtown","Rockford Hills","Paleto Bay"];
const fische=[{n:"alter Stiefel",w:5},{n:"Hering",w:55},{n:"Makrele",w:85},{n:"Wolfsbarsch",w:130},{n:"Lachs",w:190},{n:"Thunfisch",w:340}];
const erze=[{n:"Kohle",w:45},{n:"Eisenerz",w:80},{n:"Kupfererz",w:120},{n:"Silbererz",w:220},{n:"Goldnugget",w:480}];
/* =====================================================================
JOBS — jeder mit Schritten und Auftrags-Generator
===================================================================== */
let jobs=[
{id:"liefer",name:"Lieferdienst",icon:"📦",kat:"Service",farbe:"#4aa8ff",beschreibung:"Pakete abholen und im Stadtgebiet ausliefern.",
schritte:[{text:"Fahre zum Depot und lade das Paket ein.",btn:"📦 Paket abholen"},{text:"Liefere das Paket beim Kunden aus.",btn:"✅ Zustellen"}],
auftrag:l=>({ziel:"Zustellung: "+pick(orte),geld:Math.round(rnd(120,180)*(1+l*0.05)),xp:30})},
{id:"muell",name:"Müllabfuhr",icon:"🗑️",kat:"Service",farbe:"#7c8b52",beschreibung:"Mülltonnen entlang der Route leeren.",
schritte:[{text:"Sammle die Mülltonnen entlang der Straße ein.",btn:"🗑️ Tonnen leeren"},{text:"Entlade am Wertstoffhof.",btn:"♻️ Entladen"}],
auftrag:l=>({ziel:"Route: "+pick(orte),geld:Math.round(rnd(90,140)*(1+l*0.05)),xp:26})},
{id:"taxi",name:"Taxi",icon:"🚕",kat:"Service",farbe:"#f5b23d",beschreibung:"Fahrgäste abholen und ans Ziel bringen.",
schritte:[{text:"Hole den Fahrgast am Standort ab.",btn:"🧍 Fahrgast abholen"},{text:"Bringe den Fahrgast sicher ans Ziel.",btn:"🏁 Zum Ziel fahren"}],
auftrag:l=>{const d=rnd(2,12);return {ziel:pick(orte)+" → "+pick(orte),geld:Math.round((80+d*14)*(1+l*0.05)),xp:22,info:d+" km Fahrt"};}},
{id:"trucker",name:"Trucker",icon:"🚛",kat:"Fracht",farbe:"#e0503f",beschreibung:"Große Frachten über Land ausliefern — Top-Bezahlung.",
schritte:[{text:"Beladung am Lagerhaus vorbereiten.",btn:"📦 Anhänger beladen"},{text:"Fahre die Fracht zum Zielort.",btn:"🛣️ Ausliefern"}],
auftrag:l=>({ziel:"Fracht nach "+pick(orte),geld:Math.round(rnd(420,680)*(1+l*0.05)),xp:45})},
{id:"tankwart",name:"Tankwart",icon:"⛽",kat:"Service",farbe:"#34d39a",beschreibung:"Zapfsäulen bedienen und kassieren.",
schritte:[{text:"Bediene die Zapfsäule für den Kunden.",btn:"⛽ Betanken"},{text:"Kassiere an der Kasse.",btn:"💳 Kassieren"}],
auftrag:l=>({ziel:"Tankstelle "+pick(orte),geld:Math.round(rnd(100,160)*(1+l*0.05)),xp:24})},
{id:"angeln",name:"Angeln",icon:"🎣",kat:"Freizeit",farbe:"#4ac1d4",beschreibung:"Angel auswerfen und den Fang verkaufen.",
schritte:[{text:"Wirf die Angel aus und warte auf einen Biss.",btn:"🎣 Auswerfen"}],
auftrag:l=>{const f=pick(fische);return {ziel:"Am Pier",geld:Math.round(f.w*(1+l*0.06)),xp:18,fang:f.n};}},
{id:"holz",name:"Holzfäller",icon:"🪓",kat:"Rohstoff",farbe:"#8a6a34",beschreibung:"Bäume fällen und Holz zum Sägewerk bringen.",
schritte:[{text:"Fälle den markierten Baum.",btn:"🪓 Baum fällen"},{text:"Bringe die Stämme zum Sägewerk.",btn:"🪵 Verkaufen"}],
auftrag:l=>{const n=rnd(3,8);return {ziel:"Wald bei Paleto",geld:Math.round(n*35*(1+l*0.05)),xp:28,fang:n+" Stämme"};}},
{id:"bergbau",name:"Bergbau",icon:"⛏️",kat:"Rohstoff",farbe:"#9aa0b5",beschreibung:"Erz abbauen und einschmelzen.",
schritte:[{text:"Baue das Erz in der Mine ab.",btn:"⛏️ Erz abbauen"},{text:"Schmelze das Erz ein und verkaufe.",btn:"🔥 Einschmelzen"}],
auftrag:l=>{const e=pick(erze),n=rnd(2,6);return {ziel:"Mine Sandy Shores",geld:Math.round(e.w*n*0.5*(1+l*0.05)),xp:30,fang:n+"× "+e.n};}}
];
const jobById=id=>jobs.find(j=>j.id===id);
// Fortschritt je Job
let fortschritt={};
jobs.forEach(j=>fortschritt[j.id]={level:1,xp:0});
fortschritt.liefer.level=4; fortschritt.liefer.xp=60; fortschritt.angeln.level=2; fortschritt.taxi.level=3;
let sektion="jobcenter";
let aktiv=null; // {jobId, schrittIdx, aktuell:{...}, verdient, auftraege, startTs}
let arbeitet=false; // gerade ein Schritt in Bearbeitung
let schrittTimer=null, schichtTimer=null;
const aktJob=()=>aktiv?jobById(aktiv.jobId):null;
/* =====================================================================
SHELL
===================================================================== */
function geldAnzeige(){$("m-cash").textContent=euro(spieler.bargeld);$("m-bank").textContent=euro(spieler.konto);}
function renderTop(){
const j=aktJob();
$("top").innerHTML=`<div class="status"><span class="dot ${aktiv?'on':''}"></span>${aktiv?'Im Dienst · '+j.name:'Außer Dienst'}</div>
<div class="money"><div class="money-chip"><div class="l">Bargeld</div><div class="v" id="m-cash"></div></div><div class="money-chip"><div class="l">Konto</div><div class="v" id="m-bank"></div></div></div>`;
geldAnzeige();
}
function renderNav(){
const items=[{id:"jobcenter",label:"Jobcenter",icon:"🏢"},{id:"arbeit",label:"Meine Arbeit",icon:"🛠️",dis:!aktiv},{id:"statistik",label:"Statistik",icon:"📊"}];
$("nav").innerHTML=items.map(n=>`<button data-s="${n.id}" class="${n.id===sektion?'active':''}" ${n.dis?'disabled':''}><span class="ni">${n.icon}</span>${n.label}</button>`).join('');
$("nav").querySelectorAll("[data-s]").forEach(b=>b.addEventListener("click",()=>{sektion=b.dataset.s;renderNav();renderSektion();}));
}
function setAccent(farbe){document.documentElement.style.setProperty("--accent",farbe);document.documentElement.style.setProperty("--accent-soft","color-mix(in srgb, "+farbe+" 16%, transparent)");}
function renderSektion(){clearInterval(schichtTimer);(sektionen[sektion]||sektionen.jobcenter)();}
function refresh(){renderTop();renderNav();renderSektion();}
const sektionen={};
/* =====================================================================
JOBCENTER
===================================================================== */
sektionen.jobcenter=()=>{
setAccent("#4aa8ff");
$("main").innerHTML=`<div class="phead"><div><h2>Jobcenter</h2><p>Wähle einen Job und tritt den Dienst an</p></div></div>
<div class="jobgrid">${jobs.map(j=>{const f=fortschritt[j.id],istAktiv=aktiv&&aktiv.jobId===j.id;
return `<div class="jobcard ${istAktiv?'aktiv':''}"><div class="ico" style="background:${j.farbe}22;color:${j.farbe}">${j.icon}</div>
<div class="jn">${sicher(j.name)} ${istAktiv?'<span class="pill g">im Dienst</span>':''}</div><div class="jk">${sicher(j.kat)}</div>
<div class="jd">${sicher(j.beschreibung)}</div>
<div class="lvl">Level ${f.level}<div class="lvlbar"><i style="width:${Math.round(f.xp/XP_PRO_LEVEL*100)}%;background:${j.farbe}"></i></div></div>
${istAktiv?`<button class="btn ghost" data-open="${j.id}">Zur Arbeit ›</button>`:`<button class="btn primary" data-start="${j.id}" ${aktiv?'disabled':''} style="background:${j.farbe}">${aktiv?'Erst abmelden':'Job antreten'}</button>`}</div>`;}).join('')}</div>
${aktiv?'<div class="card" style="margin-top:14px;color:var(--muted);font-size:12.5px">Du bist aktuell bei <b style="color:var(--fg)">'+sicher(aktJob().name)+'</b> im Dienst. Melde dich unter „Meine Arbeit" ab, um den Job zu wechseln.</div>':''}`;
$("main").querySelectorAll("[data-start]").forEach(b=>b.addEventListener("click",()=>antreten(b.dataset.start)));
$("main").querySelectorAll("[data-open]").forEach(b=>b.addEventListener("click",()=>{sektion="arbeit";renderNav();renderSektion();}));
};
function antreten(id){
if(IST_FIVEM){ nui("antreten",{jobId:id}); return; }
const j=jobById(id);
aktiv={jobId:id,schrittIdx:0,aktuell:j.auftrag(fortschritt[id].level),verdient:0,auftraege:0,startTs:Date.now()};
sektion="arbeit"; toast("Dienst angetreten: "+j.name,"good"); refresh();
}
/* =====================================================================
ARBEIT (Auftrags-Loop)
===================================================================== */
function dienstzeit(){const s=Math.floor((Date.now()-aktiv.startTs)/1000),m=Math.floor(s/60);return m+" min "+String(s%60).padStart(2,"0")+" s";}
sektionen.arbeit=()=>{
if(!aktiv){sektion="jobcenter";return sektionen.jobcenter();}
const j=aktJob(),f=fortschritt[j.id],a=aktiv,s=j.schritte[a.schrittIdx];
setAccent(j.farbe);
$("main").innerHTML=`<div class="work-head"><div class="ico" style="background:${j.farbe}22;color:${j.farbe}">${j.icon}</div>
<div style="flex:1"><div class="wt">${sicher(j.name)}</div><div class="wl">Level ${f.level} · ${f.xp}/${XP_PRO_LEVEL} XP</div><div class="xpbar"><i style="width:${Math.round(f.xp/XP_PRO_LEVEL*100)}%;background:${j.farbe}"></i></div></div>
<button class="btn red" id="feierabend" style="width:auto">Feierabend</button></div>
<div class="grid" style="display:grid;grid-template-columns:1.5fr 1fr;gap:14px;align-items:start">
<div class="task"><div class="auf">Auftrag #${a.auftraege+1}</div><div class="ziel">${sicher(a.aktuell.ziel)}</div>
<div class="schritt">${a.aktuell.info?'<span style="color:var(--muted)">'+sicher(a.aktuell.info)+'</span> · ':''}${sicher(s.text)}</div>
<div class="lohn">Lohn: ${euro(a.aktuell.geld)}${a.aktuell.fang?' · '+sicher(a.aktuell.fang):''}</div>
<div class="stepbar">${j.schritte.map((_,i)=>`<div class="sd ${i<a.schrittIdx?'done':i===a.schrittIdx?'cur':''}"></div>`).join('')}</div>
${arbeitet?'<div class="workprog"><i class="fill"></i></div>':''}
${IST_FIVEM
? '<div class="ortshinweis">📍 Fahre zum markierten Ort auf der Karte und drücke dort <b>E</b>:<br><b>'+sicher(s.btn)+'</b>'+(aktuellerOrtName()?'<div class="ortname">Ziel: '+sicher(aktuellerOrtName())+'</div>':'')+'</div>'
: '<button class="btn wide primary" id="do-step" '+(arbeitet?'disabled':'')+' style="background:'+j.farbe+'">'+(arbeitet?'⏳ Arbeite…':sicher(s.btn))+'</button>'}</div>
<div class="card"><div class="list-h">Schicht</div>
<div style="display:flex;flex-direction:column;gap:2px">
<div class="kpi"><div class="l">Verdient (Schicht)</div><div class="v" style="color:var(--green)">${euro(a.verdient)}</div></div>
<div class="kpi" style="margin-top:10px"><div class="l">Aufträge erledigt</div><div class="v">${a.auftraege}</div></div>
<div class="kpi" style="margin-top:10px"><div class="l">Dienstzeit</div><div class="v" id="dz" style="font-size:18px">${dienstzeit()}</div></div></div></div></div>`;
$("feierabend").addEventListener("click",feierabend);
if(!IST_FIVEM) $("do-step").addEventListener("click",macheSchritt);
clearInterval(schichtTimer); schichtTimer=setInterval(()=>{const el=$("dz");if(el&&aktiv)el.textContent=dienstzeit();else clearInterval(schichtTimer);},1000);
};
/* Nur für die eigenständige Browser-Ansicht.
In FiveM wird ausschließlich am echten Arbeitsort per E gearbeitet —
das Menü kann keinen Schritt auslösen. */
function macheSchritt(){
if(arbeitet||!aktiv||IST_FIVEM)return;
const j=aktJob(),a=aktiv;
arbeitet=true; sektionen.arbeit(); // rendert Fortschrittsbalken
clearTimeout(schrittTimer);
schrittTimer=setTimeout(()=>{
arbeitet=false;
if(a.schrittIdx<j.schritte.length-1){ a.schrittIdx++; sektionen.arbeit(); return; }
// Auftrag abgeschlossen -> Belohnung
const r=a.aktuell; spieler.bargeld+=r.geld; a.verdient+=r.geld; a.auftraege++;
const f=fortschritt[j.id]; f.xp+=r.xp; let up=false;
while(f.xp>=XP_PRO_LEVEL){ f.xp-=XP_PRO_LEVEL; f.level++; up=true; }
a.schrittIdx=0; a.aktuell=j.auftrag(f.level);
toast((r.fang?"🎉 "+r.fang+" · ":"✅ Auftrag erledigt · ")+euro(r.geld)+" verdient","good");
if(up)setTimeout(()=>toast("⭐ Level "+f.level+" bei "+j.name+"!","level"),700);
renderTop(); sektionen.arbeit();
},1000);
}
function feierabend(){
if(IST_FIVEM){ nui("feierabend",{}); return; }
const j=aktJob(),a=aktiv;
clearTimeout(schrittTimer); clearInterval(schichtTimer); arbeitet=false;
toast("Feierabend bei "+j.name+" · "+a.auftraege+" Aufträge · "+euro(a.verdient),"good");
aktiv=null; sektion="jobcenter"; refresh();
}
/* =====================================================================
STATISTIK
===================================================================== */
sektionen.statistik=()=>{
setAccent("#4aa8ff");
const gesamtLevel=jobs.reduce((s,j)=>s+fortschritt[j.id].level,0);
$("main").innerHTML=`<div class="phead"><div><h2>Statistik</h2><p>Dein Fortschritt in allen Jobs</p></div></div>
<div class="grid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:14px">
<div class="card kpi"><div class="l">Aktueller Job</div><div class="v" style="font-size:18px">${aktiv?sicher(aktJob().name):'—'}</div></div>
<div class="card kpi"><div class="l">Level gesamt</div><div class="v">${gesamtLevel}</div></div>
<div class="card kpi"><div class="l">Bargeld</div><div class="v">${euro(spieler.bargeld)}</div></div></div>
<div class="card"><div class="list-h">Level je Job</div>${jobs.map(j=>{const f=fortschritt[j.id];return `<div style="display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--border)"><span style="font-size:22px;width:30px;text-align:center">${j.icon}</span><div style="flex:1"><div style="font-weight:700;font-size:13.5px">${sicher(j.name)} <span class="pill ac">Lvl ${f.level}</span></div><div class="lvlbar" style="margin-top:5px"><i style="width:${Math.round(f.xp/XP_PRO_LEVEL*100)}%;background:${j.farbe}"></i></div></div><span style="color:var(--muted);font-size:12px">${f.xp}/${XP_PRO_LEVEL} XP</span></div>`;}).join('')}</div>`;
};
/* =====================================================================
NUI-BRÜCKE — Verbindung zum FiveM-Client
===================================================================== */
const IST_FIVEM = (typeof GetParentResourceName === "function");
const RESSOURCE = IST_FIVEM ? GetParentResourceName() : "aether-jobs";
let SCHRITT_DAUER = 4000; // vom Server vorgegeben
function nui(aktion, daten){
if(!IST_FIVEM) return Promise.resolve();
return fetch(`https://${RESSOURCE}/${aktion}`, {
method: "POST",
headers: { "Content-Type": "application/json; charset=UTF-8" },
body: JSON.stringify(daten || {}),
}).catch(() => {});
}
/* Name des Orts, an dem der aktuelle Arbeitsschritt stattfindet */
function aktuellerOrtName(){
if(!aktiv || !aktiv.aktuell || !aktiv.aktuell.orte) return null;
const ort = aktiv.aktuell.orte[aktiv.schrittIdx];
return ort ? ort.name : null;
}
/* Übernimmt Jobs, Fortschritt und laufende Schicht vom Server */
function setzeZustand(z){
if(z.jobs) jobs = z.jobs;
if(z.xpProLevel) XP_PRO_LEVEL = z.xpProLevel;
if(z.schrittDauer) SCHRITT_DAUER = z.schrittDauer;
if(z.fortschritt) fortschritt = z.fortschritt;
if(z.geld){ spieler.bargeld = z.geld.bargeld || 0; spieler.konto = z.geld.konto || 0; }
if(z.aktiv){
/* startTs aus der vom Server gemeldeten Dienstzeit zurückrechnen,
damit der Timer in der Oberfläche weiterläuft. */
aktiv = {
jobId: z.aktiv.jobId,
schrittIdx: z.aktiv.schrittIdx,
aktuell: z.aktiv.aktuell,
verdient: z.aktiv.verdient,
auftraege: z.aktiv.auftraege,
startTs: Date.now() - (z.aktiv.dienstzeit || 0) * 1000,
};
if(sektion !== "arbeit" && sektion !== "statistik") sektion = "arbeit";
} else {
aktiv = null;
if(sektion === "arbeit") sektion = "jobcenter";
}
arbeitet = false;
refresh();
if(z.hinweis) toast(z.hinweis, "good");
}
window.addEventListener("message", (ereignis) => {
const n = ereignis.data || {};
if(n.aktion === "oeffnen"){
document.body.style.display = "";
setzeZustand(n);
}
else if(n.aktion === "zustand"){
setzeZustand(n);
}
else if(n.aktion === "fehler"){
arbeitet = false;
toast(n.text || "Nicht möglich", "warn");
refresh();
}
else if(n.aktion === "schliessen"){
document.body.style.display = "none";
}
});
/* ESC schließt das Jobcenter */
document.addEventListener("keydown", (e) => {
if(e.key === "Escape" && IST_FIVEM) nui("schliessen", {});
});
/* In FiveM zunächst verstecken — die Seite wartet auf "oeffnen" */
if(IST_FIVEM) document.body.style.display = "none";
/* START */
if(!IST_FIVEM) refresh();
</script>
</body>
</html>