- 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>
341 lines
25 KiB
HTML
341 lines
25 KiB
HTML
<!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>
|