Files

614 lines
31 KiB
HTML
Raw Permalink 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="tr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Numex İşletme — Operasyon Paneli</title>
<style>
:root{
/* NEON PALET — camgöbeği (cyan) ana, mor (magenta) ikincil */
--bg:#05060f; --bg2:#0a0c1b; --panel:rgba(14,18,38,.72); --panel2:#111631;
--line:rgba(0,229,255,.22); --line2:rgba(0,229,255,.45);
--txt:#eaf6ff; --muted:#7f90b0;
--ok:#22e6a6; --warn:#ffc94a; --bad:#ff4d7d; --accent:#00e5ff; --accent2:#b14aff;
--glow:0 0 12px rgba(0,229,255,.45), 0 0 34px rgba(0,229,255,.18);
--glow2:0 0 12px rgba(177,74,255,.5), 0 0 34px rgba(177,74,255,.2);
}
*{box-sizing:border-box}
body{
margin:0;color:var(--txt);font:14px/1.55 "Segoe UI",system-ui,sans-serif;overflow-y:auto;
background:
radial-gradient(900px 520px at 12% -8%, rgba(0,229,255,.16), transparent 62%),
radial-gradient(820px 520px at 92% 4%, rgba(177,74,255,.16), transparent 60%),
radial-gradient(700px 600px at 50% 118%, rgba(0,229,255,.10), transparent 62%),
linear-gradient(180deg, var(--bg2), var(--bg) 55%);
/* NUMEX-DÜZELTME (2026-10-27): Alt kisimda acik gri serit goruluyordu: background-attachment:fixed ile coklu gradient arka plan, body'nin icerikten kisa kalan kutusuna bagli kaliyor ve sayfa sonunda tarayici varsayilan zeminine dusuyordu. Sabitleme kaldirildi, html'e de taban rengi verildi. */
background-repeat:no-repeat;
background-size:cover;
background-color:var(--bg);
min-height:100vh;
}
html{background:var(--bg);min-height:100%}
/* ince tarama çizgileri — neon tüp dokusu */
body::before{
content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
background:repeating-linear-gradient(0deg, rgba(255,255,255,.028) 0 1px, transparent 1px 3px);
mix-blend-mode:screen;
}
header{
display:flex;flex-wrap:wrap;gap:12px;align-items:center;padding:14px 18px;position:sticky;top:0;z-index:5;
background:linear-gradient(180deg, rgba(10,12,27,.94), rgba(10,12,27,.72));
backdrop-filter:blur(10px);border-bottom:1px solid var(--line2);
box-shadow:0 1px 0 rgba(0,229,255,.15), 0 14px 40px -22px rgba(0,229,255,.65);
}
h1{
font-size:16px;margin:0;letter-spacing:.6px;font-weight:600;
color:#dffbff;text-shadow:0 0 10px rgba(0,229,255,.75), 0 0 26px rgba(0,229,255,.35);
}
.tag{
font-size:11px;color:var(--accent);border:1px solid var(--line2);padding:3px 10px;border-radius:20px;
background:rgba(0,229,255,.06);box-shadow:inset 0 0 12px rgba(0,229,255,.14);
letter-spacing:.3px;
}
main{padding:16px;max-width:1400px;margin:0 auto;position:relative;z-index:1}
.kpi{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:18px}
.kpi div{
position:relative;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px;
box-shadow:inset 0 1px 0 rgba(255,255,255,.05), 0 0 0 1px rgba(0,229,255,.04), 0 12px 30px -18px rgba(0,229,255,.6);
backdrop-filter:blur(6px);overflow:hidden;
}
.kpi div::after{
content:"";position:absolute;left:0;right:0;top:0;height:2px;
background:linear-gradient(90deg, transparent, var(--accent), var(--accent2), transparent);
opacity:.85;filter:blur(.3px);
}
.kpi b{
display:block;font-size:22px;letter-spacing:.5px;color:#eafcff;
text-shadow:0 0 12px rgba(0,229,255,.6), 0 0 30px rgba(0,229,255,.25);
}
.kpi span{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
nav{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
nav button{
background:rgba(17,22,49,.7);color:#cfe6f6;border:1px solid var(--line);padding:10px 15px;min-height:44px;
border-radius:9px;cursor:pointer;font-size:13px;letter-spacing:.2px;
transition:box-shadow .18s, border-color .18s, color .18s, background .18s;
}
nav button:hover{border-color:var(--line2);color:#eafdff;box-shadow:0 0 14px rgba(0,229,255,.35)}
nav button.aktif{
background:linear-gradient(180deg, rgba(0,229,255,.24), rgba(177,74,255,.18));
border-color:var(--accent);color:#fff;box-shadow:var(--glow);
text-shadow:0 0 10px rgba(255,255,255,.65);
}
section{display:none;animation:belir .22s ease-out}
section.gorunur{display:block}
@keyframes belir{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
.card{
position:relative;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;
margin-bottom:16px;overflow-x:auto;backdrop-filter:blur(6px);
box-shadow:inset 0 1px 0 rgba(255,255,255,.04), 0 18px 44px -30px rgba(0,229,255,.8);
}
.card::before{
content:"";position:absolute;inset:-1px;border-radius:12px;pointer-events:none;
background:linear-gradient(135deg, rgba(0,229,255,.35), transparent 38%, transparent 62%, rgba(177,74,255,.35));
-webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite:xor;mask-composite:exclude;padding:1px;
}
.card h2{
font-size:12px;margin:0 0 12px;color:var(--accent);text-transform:uppercase;letter-spacing:1.6px;
text-shadow:0 0 12px rgba(0,229,255,.6);
}
table{width:100%;border-collapse:collapse;font-size:13px;min-width:520px}
th,td{text-align:left;padding:9px 10px;border-bottom:1px solid rgba(0,229,255,.10);vertical-align:top}
th{color:var(--muted);font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:1px}
tbody tr{transition:background .15s}
tbody tr:hover{background:rgba(0,229,255,.06)}
input,select,textarea{
background:rgba(9,12,26,.9);border:1px solid var(--line);color:var(--txt);padding:9px 10px;
border-radius:8px;min-height:44px;font:inherit;width:100%;transition:box-shadow .18s, border-color .18s;
}
input:focus,select:focus,textarea:focus{
outline:none;border-color:var(--accent);box-shadow:0 0 0 2px rgba(0,229,255,.16), var(--glow);
}
button.eylem{
background:rgba(17,22,49,.85);border:1px solid var(--line);color:#d9eefb;padding:10px 15px;min-height:44px;
border-radius:8px;cursor:pointer;font-size:13px;transition:box-shadow .18s, border-color .18s, transform .1s;
}
button.eylem:hover{border-color:var(--accent);box-shadow:var(--glow);color:#fff}
button.eylem:active{transform:translateY(1px)}
button.eylem.birincil{
background:linear-gradient(180deg, rgba(0,229,255,.9), rgba(0,180,220,.95));
border-color:#5cf3ff;color:#04121a;font-weight:600;
box-shadow:0 0 18px rgba(0,229,255,.6), 0 0 46px rgba(0,229,255,.25);
}
button.eylem.birincil:hover{box-shadow:0 0 26px rgba(0,229,255,.85), 0 0 60px rgba(0,229,255,.35)}
button.eylem.tehlike{border-color:var(--bad);color:var(--bad);box-shadow:0 0 14px rgba(255,77,125,.28)}
.satir{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:10px}
label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
.durum{
font-size:11px;padding:3px 10px;border-radius:20px;border:1px solid;letter-spacing:.4px;
display:inline-block;background:rgba(255,255,255,.03);
}
.d-REZERVE{color:var(--warn);border-color:var(--warn);box-shadow:0 0 12px rgba(255,201,74,.35)}
.d-ODENDI,.d-HAZIRLANIYOR{color:var(--accent);border-color:var(--accent);box-shadow:0 0 12px rgba(0,229,255,.4)}
.d-YOLDA{color:var(--accent2);border-color:var(--accent2);box-shadow:0 0 12px rgba(177,74,255,.45)}
.d-TESLIM_EDILDI{color:var(--ok);border-color:var(--ok);box-shadow:0 0 12px rgba(34,230,166,.4)}
.d-IPTAL{color:var(--bad);border-color:var(--bad);box-shadow:0 0 12px rgba(255,77,125,.4)}
.risk-YUKSEK{color:var(--bad);font-weight:700;text-shadow:0 0 12px rgba(255,77,125,.8)}
.risk-ORTA{color:var(--warn);text-shadow:0 0 10px rgba(255,201,74,.55)}
.risk-DUSUK{color:var(--muted)}
.bildirim{
position:fixed;right:16px;bottom:16px;max-width:420px;background:rgba(12,16,34,.96);
border:1px solid var(--line2);border-left:4px solid var(--accent);border-radius:10px;padding:13px;
font-size:13px;z-index:50;display:none;white-space:pre-wrap;backdrop-filter:blur(10px);
box-shadow:var(--glow), 0 20px 50px -20px #000;
}
.bildirim.hata{border-left-color:var(--bad);box-shadow:0 0 16px rgba(255,77,125,.5), 0 20px 50px -20px #000}
.bildirim.basari{border-left-color:var(--ok);box-shadow:0 0 16px rgba(34,230,166,.5), 0 20px 50px -20px #000}
.bos{color:var(--muted);font-size:13px;padding:10px 0;font-style:italic}
/* ---------- GRAFİKLER ---------- */
.grafik-izgara{display:grid;grid-template-columns:1.6fr 1fr;gap:16px;margin-bottom:16px}
.grafik-izgara2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
@media (max-width:900px){ .grafik-izgara,.grafik-izgara2{grid-template-columns:1fr} }
.grafik-baslik{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-bottom:8px}
.grafik-deger{font-size:12px;color:var(--muted);letter-spacing:.4px}
.grafik-deger b{color:var(--accent);font-weight:600;text-shadow:0 0 10px rgba(0,229,255,.55)}
svg.grafik{display:block;width:100%;height:auto;overflow:visible}
.grafik-cizgi{fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;
filter:drop-shadow(0 0 6px rgba(0,229,255,.85))}
.grafik-alan{opacity:.9}
.grafik-nokta{fill:#04121a;stroke-width:2}
.grafik-eksen{stroke:rgba(0,229,255,.16);stroke-width:1}
.grafik-yazi{fill:var(--muted);font-size:9px;letter-spacing:.3px}
.halka-dilim{transition:filter .2s}
.halka-dilim:hover{filter:brightness(1.35)}
.halka-merkez-deger{fill:#eafcff;font-size:20px;font-weight:700;text-anchor:middle;
filter:drop-shadow(0 0 10px rgba(0,229,255,.8))}
.halka-merkez-yazi{fill:var(--muted);font-size:9px;text-anchor:middle;letter-spacing:1.4px}
.halka-duzen{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.halka-duzen svg.halka{width:132px;flex:none;max-width:40%}
.gosterge{display:flex;flex-direction:column;gap:8px;flex:1;min-width:130px}
.gosterge-satir{display:grid;grid-template-columns:12px 1fr auto;gap:9px;align-items:center;font-size:12px}
.gosterge-nokta{width:10px;height:10px;border-radius:3px;box-shadow:0 0 10px currentColor;background:currentColor}
.gosterge-etiket{color:#cfe2f2}
.gosterge-sayi{color:var(--muted);font-variant-numeric:tabular-nums}
.bar-satir{margin-bottom:11px}
.bar-ust{display:flex;justify-content:space-between;font-size:12px;margin-bottom:5px;gap:8px}
.bar-ust span:first-child{color:#cfe2f2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bar-ust span:last-child{color:var(--muted);font-variant-numeric:tabular-nums;flex:none}
.bar-ray{height:9px;border-radius:6px;background:rgba(255,255,255,.06);overflow:hidden;
box-shadow:inset 0 0 0 1px rgba(0,229,255,.10)}
.bar-dolu{height:100%;border-radius:6px;transition:width .5s cubic-bezier(.3,.9,.3,1);
background:linear-gradient(90deg,#00e5ff,#b14aff);
box-shadow:0 0 12px rgba(0,229,255,.75),0 0 26px rgba(177,74,255,.35)}
.bar-dolu.uyari{background:linear-gradient(90deg,#ffc94a,#ff8a3d);box-shadow:0 0 12px rgba(255,201,74,.7)}
.bar-dolu.kritik{background:linear-gradient(90deg,#ff4d7d,#b14aff);box-shadow:0 0 14px rgba(255,77,125,.8)}
@media (max-width:640px){
main{padding:10px} .kpi b{font-size:18px} h1{font-size:14px}
.kpi{grid-template-columns:repeat(auto-fit,minmax(120px,1fr))}
}
@media (prefers-reduced-motion:reduce){
*{animation:none !important;transition:none !important}
}
</style>
</head>
<body>
<header>
<h1>Numex İşletme — Operasyon Paneli</h1>
<span class="tag" id="saglik">bağlantı kontrol ediliyor…</span>
<span class="tag" id="pid"></span>
</header>
<main>
<div class="kpi" id="kpi"></div>
<nav>
<button data-tab="panel" class="aktif">Panel</button>
<button data-tab="siparis">Siparişler</button>
<button data-tab="yeni">Yeni Sipariş</button>
<button data-tab="stok">Stok</button>
<button data-tab="musteri">Müşteriler</button>
<button data-tab="teslimat">Teslimat</button>
<button data-tab="risk">Risk İzleme</button>
<button data-tab="kayit">Operasyon Kayıtları</button>
</nav>
<section id="panel" class="gorunur">
<div class="grafik-izgara">
<div class="card">
<div class="grafik-baslik">
<h2 style="margin:0">Tahsilat Trendi</h2>
<div class="grafik-deger">Son 7 gün · toplam <b id="trendToplam">₺0</b></div>
</div>
<div id="grafikTrend"></div>
</div>
<div class="card">
<div class="grafik-baslik">
<h2 style="margin:0">Sipariş Durumları</h2>
<div class="grafik-deger" id="durumToplam">0 sipariş</div>
</div>
<div id="grafikDurum"></div>
</div>
</div>
<div class="grafik-izgara2">
<div class="card">
<div class="grafik-baslik">
<h2 style="margin:0">Stok Doluluk</h2>
<div class="grafik-deger">kullanılabilir / toplam</div>
</div>
<div id="grafikStok"></div>
</div>
<div class="card">
<div class="grafik-baslik">
<h2 style="margin:0">Risk Dağılımı</h2>
<div class="grafik-deger" id="riskToplam">0 açık sipariş</div>
</div>
<div id="grafikRisk"></div>
</div>
</div>
<div class="card"><h2>Kritik Stok</h2><div id="kritik"></div></div>
<div class="card"><h2>Son Siparişler</h2><div id="sonSiparis"></div></div>
</section>
<section id="siparis">
<div class="card"><h2>Siparişler</h2><div id="siparisListe"></div></div>
</section>
<section id="yeni">
<div class="card">
<h2>Yeni Sipariş</h2>
<div class="satir">
<div><label>Müşteri</label><select id="yMusteri"></select></div>
<div><label>Ürün</label><select id="yUrun"></select></div>
<div><label>Adet</label><input id="yAdet" type="number" min="1" value="1" /></div>
<div><label>Hedef teslim (opsiyonel)</label><input id="yHedef" type="datetime-local" /></div>
</div>
<button class="eylem birincil" id="yGonder">Sipariş oluştur (stok rezerve)</button>
</div>
<div class="card"><h2>Yeni Müşteri</h2>
<div class="satir">
<div><label>Ad</label><input id="mAd" /></div>
<div><label>Telefon</label><input id="mTel" /></div>
<div><label>E-posta</label><input id="mMail" /></div>
<div><label>Adres</label><input id="mAdres" /></div>
</div>
<button class="eylem birincil" id="mGonder">Müşteri ekle</button>
</div>
</section>
<section id="stok">
<div class="card"><h2>Stok Durumu</h2><div id="stokListe"></div></div>
<div class="card"><h2>Stok Hareketleri</h2><div id="stokHareket"></div></div>
</section>
<section id="musteri">
<div class="card"><h2>Müşteriler</h2><div id="musteriListe"></div></div>
</section>
<section id="teslimat">
<div class="card"><h2>Teslimatlar</h2><div id="teslimatListe"></div></div>
</section>
<section id="risk">
<div class="card"><h2>Risk İzleme (izleme kapsamı önceden yetkilendirilmiş)</h2>
<div id="riskListe"></div>
</div>
</section>
<section id="kayit">
<div class="card"><h2>Operasyon Kayıtları</h2><div id="opsListe"></div></div>
<div class="card"><h2>Karar Defteri</h2><div id="kararListe"></div></div>
</section>
</main>
<div class="bildirim" id="bildirim"></div>
<script>
const $ = (s) => document.querySelector(s);
const para = (k) => (k / 100).toLocaleString('tr-TR', { style: 'currency', currency: 'TRY' });
const tarih = (s) => s ? new Date(s).toLocaleString('tr-TR') : '—';
function bildir(mesaj, tur = '') {
const b = $('#bildirim');
b.textContent = mesaj; b.className = 'bildirim ' + tur; b.style.display = 'block';
clearTimeout(b._t); b._t = setTimeout(() => { b.style.display = 'none'; }, 6000);
}
async function api(yol, secenek = {}) {
const r = await fetch(yol, {
headers: { 'Content-Type': 'application/json' },
...secenek,
body: secenek.body ? JSON.stringify(secenek.body) : undefined
});
const veri = await r.json().catch(() => ({}));
if (!r.ok) throw Object.assign(new Error(veri.mesaj || veri.hata || 'İstek başarısız'), { veri, status: r.status });
return veri;
}
/* ==================== GRAFİKLER (saf SVG, bağımlılık yok) ==================== */
// 1) Alan + çizgi grafik — tahsilat trendi
function alanGrafik(hedef, seri) {
// NUMEX-DÜZELTME (2026-10-27): Trend grafigi 560x168 viewBox ile ciziliyordu; yanindaki halka karti daha kisa oldugu icin grid satiri trend kartinin yuksekligine oturuyor ve kartin altinda buyuk bos alan kaliyordu. Yükseklik artirilip kart icerigine oturtuldu.
const W = 620, H = 232, padL = 34, padR = 14, padT = 18, padB = 26;
const enBuyuk = Math.max(1, ...seri.map((d) => d.deger));
const icW = W - padL - padR, icH = H - padT - padB;
const adim = seri.length > 1 ? icW / (seri.length - 1) : 0;
const nokta = seri.map((d, i) => ({
x: padL + i * adim,
y: padT + icH - (d.deger / enBuyuk) * icH,
...d
}));
const cizgi = nokta.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
const alan = `${cizgi} L${nokta[nokta.length - 1].x.toFixed(1)},${padT + icH} L${padL},${padT + icH} Z`;
const izgara = [0, .5, 1].map((o) => {
const y = padT + icH * o;
return `<line class="grafik-eksen" x1="${padL}" y1="${y}" x2="${W - padR}" y2="${y}"/>`;
}).join('');
hedef.innerHTML = `<svg class="grafik" viewBox="0 0 ${W} ${H}" role="img" aria-label="Tahsilat trendi">
<defs>
<linearGradient id="alanDolgu" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#00e5ff" stop-opacity=".42"/>
<stop offset="100%" stop-color="#b14aff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="cizgiRenk" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#00e5ff"/>
<stop offset="100%" stop-color="#b14aff"/>
</linearGradient>
</defs>
${izgara}
<path class="grafik-alan" d="${alan}" fill="url(#alanDolgu)"/>
<path class="grafik-cizgi" d="${cizgi}" stroke="url(#cizgiRenk)"/>
${nokta.map((p) => `<circle class="grafik-nokta" cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3.2" stroke="#00e5ff"><title>${p.etiket}: ${para(p.deger)}</title></circle>`).join('')}
${nokta.map((p, i) => (i % Math.ceil(seri.length / 7) === 0 || i === seri.length - 1)
? `<text class="grafik-yazi" x="${p.x.toFixed(1)}" y="${H - 6}" text-anchor="middle">${p.kisa}</text>` : '').join('')}
</svg>`;
}
// 2) Halka grafik — sipariş durum dağılımı
function halkaGrafik(hedef, dilimler) {
// NUMEX-DÜZELTME (2026-10-27): Halka 180px idi ve gosterge listesi altina indigi icin kart icerigi 300px'i asip kartla hizasiz buyuk bosluk birakiyordu. Halka kucultulup gosterge listesi halkanin SAGINA alindi (yatay yerlesim) — kart yuksekligi grafiklerle dengelendi.
const boy = 150, r = 56, kalinlik = 15, merkez = boy / 2;
const cevre = 2 * Math.PI * r;
const toplam = dilimler.reduce((t, d) => t + d.deger, 0);
let ofis = 0;
const yaylar = dilimler.filter((d) => d.deger > 0).map((d) => {
const oran = d.deger / toplam;
const uzunluk = oran * cevre;
const parca = `<circle class="halka-dilim" cx="${merkez}" cy="${merkez}" r="${r}"
fill="none" stroke="${d.renk}" stroke-width="${kalinlik}"
stroke-dasharray="${(uzunluk - 3).toFixed(2)} ${(cevre - uzunluk + 3).toFixed(2)}"
stroke-dashoffset="${(-ofis).toFixed(2)}" stroke-linecap="round"
transform="rotate(-90 ${merkez} ${merkez})"
style="filter:drop-shadow(0 0 6px ${d.renk})">
<title>${d.etiket}: ${d.deger} (%${Math.round(oran * 100)})</title>
</circle>`;
ofis += uzunluk;
return parca;
}).join('');
hedef.innerHTML = `<div class="halka-duzen">
<svg class="grafik halka" viewBox="0 0 ${boy} ${boy}" role="img" aria-label="Dağılım">
<circle cx="${merkez}" cy="${merkez}" r="${r}" fill="none" stroke="rgba(255,255,255,.06)" stroke-width="${kalinlik}"/>
${toplam ? yaylar : ''}
<text class="halka-merkez-deger" x="${merkez}" y="${merkez + 2}">${toplam}</text>
<text class="halka-merkez-yazi" x="${merkez}" y="${merkez + 18}">TOPLAM</text>
</svg>
<div class="gosterge">${dilimler.filter((d) => d.deger > 0).map((d) => `
<div class="gosterge-satir">
<span class="gosterge-nokta" style="color:${d.renk}"></span>
<span class="gosterge-etiket">${d.etiket}</span>
<span class="gosterge-sayi">${d.deger} · %${Math.round((d.deger / Math.max(toplam, 1)) * 100)}</span>
</div>`).join('') || '<div class="gosterge-satir"><span class="gosterge-sayi">Kayıt yok</span></div>'}</div>
</div>`;
}
// 3) Yatay bar grafik — stok doluluk
function barGrafik(hedef, satirlar) {
hedef.innerHTML = satirlar.map((s) => {
const oran = s.toplam ? Math.round((s.deger / s.toplam) * 100) : 0;
const sinif = oran <= 20 ? ' kritik' : oran <= 50 ? ' uyari' : '';
return `<div class="bar-satir">
<div class="bar-ust"><span>${s.etiket}</span><span>${s.deger} / ${s.toplam}</span></div>
<div class="bar-ray"><div class="bar-dolu${sinif}" style="width:${Math.max(oran, 2)}%"></div></div>
</div>`;
}).join('') || '<div class="bos">Ürün yok</div>';
}
const tablo = (basliklar, satirlar, bosMesaj = 'Kayıt yok') => {
if (!satirlar.length) return `<div class="bos">${bosMesaj}</div>`;
return `<table><thead><tr>${basliklar.map((b) => `<th>${b}</th>`).join('')}</tr></thead>
<tbody>${satirlar.map((s) => `<tr>${s.join('')}</tr>`).join('')}</tbody></table>`;
};
const durumEtiket = (d) => `<span class="durum d-${d}">${d}</span>`;
async function yukle() {
try {
const h = await api('/api/health');
$('#saglik').textContent = 'bağlı';
$('#pid').textContent = 'pid ' + h.pid;
} catch { $('#saglik').textContent = 'bağlantı yok'; }
const [dash, urunler, musteriler, siparisler, teslimat, riskler, hareketler, ops, kararlar, odemeler] = await Promise.all([
api('/api/dashboard'), api('/api/products'), api('/api/customers'), api('/api/orders'),
api('/api/shipments'), api('/api/risk'), api('/api/stock-moves'), api('/api/ops'), api('/api/decisions'),
api('/api/payments')
]);
// Sipariş satırlarına başarılı ödemelerini iliştir (trend grafiği için)
for (const s of siparisler) s.odemeler = odemeler.filter((p) => p.order_id === s.id);
$('#kpi').innerHTML = `
<div><span>Müşteri</span><b>${dash.musteri}</b></div>
<div><span>Sipariş</span><b>${dash.siparis}</b></div>
<div><span>Tahsil edilen</span><b>${para(dash.ciro_kurus)}</b></div>
<div><span>Toplam stok</span><b>${dash.stok_toplam}</b></div>
<div><span>Riskli sipariş</span><b style="color:${dash.riskli ? 'var(--bad)' : 'inherit'}">${dash.riskli}</b></div>`;
// ---------- GRAFİKLER ----------
// Tahsilat trendi: son 7 gün, başarılı ödemeler günlük toplanır
// NUMEX-DÜZELTME (2026-10-27): Gun anahtari UTC (toISOString) ile uretiliyordu; Turkiye UTC+3 oldugu icin gece 00:00-03:00 arasi odemeler bir onceki gune dusuyor ve grafik gunleri kayiyordu. Yerel tarih anahtari kullanildi.
// Yerel takvim gunu anahtari (YYYY-MM-DD) — UTC kaymasini onler
const yerelGun = (d) => {
const t = new Date(d);
return `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`;
};
const gunler = [];
for (let i = 6; i >= 0; i--) {
const g = new Date(); g.setHours(0, 0, 0, 0); g.setDate(g.getDate() - i);
gunler.push({
anahtar: yerelGun(g),
etiket: g.toLocaleDateString('tr-TR'),
kisa: g.toLocaleDateString('tr-TR', { day: '2-digit', month: '2-digit' }),
deger: 0
});
}
for (const s of siparisler) {
for (const p of (s.odemeler || [])) {
if (p.durum !== 'BASARILI' || !p.olusturma) continue;
const k = yerelGun(p.olusturma);
const g = gunler.find((x) => x.anahtar === k);
if (g) g.deger += p.tutar_kurus;
}
}
alanGrafik($('#grafikTrend'), gunler);
$('#trendToplam').textContent = para(gunler.reduce((t, g) => t + g.deger, 0));
// Sipariş durum halkası
const DURUM_RENK = {
REZERVE: '#ffc94a', ODENDI: '#00e5ff', HAZIRLANIYOR: '#5cf3ff',
YOLDA: '#b14aff', TESLIM_EDILDI: '#22e6a6', IPTAL: '#ff4d7d'
};
const DURUM_AD = {
REZERVE: 'Rezerve', ODENDI: 'Ödendi', HAZIRLANIYOR: 'Hazırlanıyor',
YOLDA: 'Yolda', TESLIM_EDILDI: 'Teslim edildi', IPTAL: 'İptal'
};
const durumSay = {};
for (const s of siparisler) durumSay[s.durum] = (durumSay[s.durum] || 0) + 1;
const dilimler = Object.keys(DURUM_AD).map((k) => ({
etiket: DURUM_AD[k], deger: durumSay[k] || 0, renk: DURUM_RENK[k]
}));
halkaGrafik($('#grafikDurum'), dilimler);
$('#durumToplam').textContent = siparisler.length + ' sipariş';
// Stok doluluk barları (kullanılabilir / fiziksel stok)
barGrafik($('#grafikStok'), urunler.map((u) => ({
etiket: u.sku, deger: u.kullanilabilir, toplam: u.stok
})));
// Risk dağılımı (açık siparişlerin risk seviyesi)
const RISK_RENK = { DUSUK: '#5cf3ff', ORTA: '#ffc94a', YUKSEK: '#ff4d7d' };
const RISK_AD = { DUSUK: 'Düşük', ORTA: 'Orta', YUKSEK: 'Yüksek' };
const riskSay = { DUSUK: 0, ORTA: 0, YUKSEK: 0 };
for (const r of riskler) riskSay[r.seviye] = (riskSay[r.seviye] || 0) + 1;
const riskDilim = Object.keys(RISK_AD).map((k) => ({
etiket: RISK_AD[k], deger: riskSay[k], renk: RISK_RENK[k]
}));
halkaGrafik($('#grafikRisk'), riskDilim);
$('#riskToplam').textContent = riskler.length + ' açık sipariş';
$('#kritik').innerHTML = tablo(['SKU', 'Stok', 'Rezerve', 'Kullanılabilir'],
dash.kritik_stok.map((k) => [`<td>${k.sku}</td>`, `<td>${k.stok}</td>`, `<td>${k.rezerve}</td>`, `<td>${k.stok - k.rezerve}</td>`]),
'Kritik stok yok');
$('#sonSiparis').innerHTML = tablo(['#', 'Müşteri', 'Tutar', 'Durum', 'Risk', 'Tarih'],
siparisler.slice(0, 6).map((s) => [`<td>#${s.id}</td>`, `<td>${s.musteri_ad}</td>`, `<td>${para(s.toplam_kurus)}</td>`,
`<td>${durumEtiket(s.durum)}</td>`, `<td class="risk-${s.risk_seviyesi}">${s.risk_seviyesi}</td>`, `<td>${tarih(s.olusturma)}</td>`]));
$('#siparisListe').innerHTML = tablo(['#', 'Müşteri', 'Tutar', 'Durum', 'Risk', 'İşlem'],
siparisler.map((s) => [`<td>#${s.id}</td>`, `<td>${s.musteri_ad}</td>`, `<td>${para(s.toplam_kurus)}</td>`,
`<td>${durumEtiket(s.durum)}</td>`, `<td class="risk-${s.risk_seviyesi}">${s.risk_seviyesi}</td>`,
`<td>${s.durum === 'REZERVE' ? `<button class="eylem" onclick="ode(${s.id})">Ödeme al</button>` : '—'}</td>`]),
'Henüz sipariş yok — "Yeni Sipariş" sekmesinden oluştur.');
$('#stokListe').innerHTML = tablo(['SKU', 'Ürün', 'Fiyat', 'Stok', 'Rezerve', 'Kullanılabilir', 'Tedarik (gün)'],
urunler.map((u) => [`<td>${u.sku}</td>`, `<td>${u.ad}</td>`, `<td>${para(u.birim_fiyat)}</td>`, `<td>${u.stok}</td>`,
`<td>${u.rezerve}</td>`, `<td><b>${u.kullanilabilir}</b></td>`, `<td>${u.tedarik_gun}</td>`]));
$('#stokHareket').innerHTML = tablo(['Ürün', 'Değişim', 'Sebep', 'Sipariş', 'Zaman'],
hareketler.map((h) => [`<td>${h.sku}</td>`, `<td>${h.delta}</td>`, `<td>${h.sebep}</td>`, `<td>#${h.order_id ?? '—'}</td>`, `<td>${tarih(h.olusturma)}</td>`]));
$('#musteriListe').innerHTML = tablo(['#', 'Ad', 'Telefon', 'E-posta', 'Adres'],
musteriler.map((m) => [`<td>${m.id}</td>`, `<td>${m.ad}</td>`, `<td>${m.telefon}</td>`, `<td>${m.email || '—'}</td>`, `<td>${m.adres || '—'}</td>`]));
$('#teslimatListe').innerHTML = tablo(['Sipariş', 'Durum', 'Takip', 'Çıkış', 'Teslim', 'İşlem'],
teslimat.map((t) => [`<td>#${t.order_id}</td>`, `<td>${durumEtiket(t.durum)}</td>`, `<td>${t.takip_no || '—'}</td>`,
`<td>${tarih(t.cikis)}</td>`, `<td>${tarih(t.teslim)}</td>`,
`<td>${t.durum !== 'TESLIM_EDILDI' ? `<button class="eylem" onclick="teslim(${t.order_id},'${t.durum === 'HAZIRLANIYOR' ? 'YOLDA' : 'TESLIM_EDILDI'}')">${t.durum === 'HAZIRLANIYOR' ? 'Yola çıkar' : 'Teslim et'}</button>` : '—'}</td>`]),
'Teslimat kaydı yok');
$('#riskListe').innerHTML = riskler.length ? riskler.map((r) => `
<div class="card" style="margin:8px 0">
<b>Sipariş #${r.order_id}</b> — <span class="risk-${r.seviye}">${r.seviye} RİSK</span> — durum ${durumEtiket(r.durum)}
<ul style="margin:8px 0 8px 18px;color:var(--muted)">${r.kanit.map((k) => `<li>${k}</li>`).join('')}</ul>
<div><b>Öneri:</b><ul style="margin:6px 0 0 18px">${r.oneriler.map((o) => `<li>${o}</li>`).join('')}</ul></div>
<div style="margin-top:8px;font-size:12px;color:var(--warn)">${r.veri_degisikligi}</div>
</div>`).join('') : '<div class="bos">Riskli sipariş yok.</div>';
$('#opsListe').innerHTML = tablo(['Zaman', 'Tür', 'Başlık', 'Detay'],
ops.map((o) => [`<td>${tarih(o.zaman)}</td>`, `<td>${o.tur}</td>`, `<td>${o.baslik}</td>`, `<td style="font-size:12px;color:var(--muted)">${o.detay || ''}</td>`]));
$('#kararListe').innerHTML = tablo(['Zaman', 'Gözlem', 'Risk', 'Karar', 'Yetki', 'Yapılmayan', 'Doğrulama'],
kararlar.map((k) => [`<td>${tarih(k.zaman)}</td>`, `<td>${k.gozlem}</td>`, `<td>${k.risk}</td>`, `<td>${k.karar}</td>`,
`<td>${k.yetki}</td>`, `<td>${k.yapilmayan}</td>`, `<td>${k.dogrulama}</td>`]));
// Form seçenekleri
$('#yMusteri').innerHTML = musteriler.map((m) => `<option value="${m.id}">${m.ad}</option>`).join('');
$('#yUrun').innerHTML = urunler.map((u) => `<option value="${u.id}">${u.sku} — ${u.kullanilabilir} adet</option>`).join('');
}
async function ode(id) {
try {
const r = await api(`/api/orders/${id}/pay`, { method: 'POST', body: { idempotency_key: 'pay-' + id + '-' + Date.now() } });
bildir(r.tekrar ? r.mesaj : `Sipariş #${id} ödemesi alındı.`, 'basari');
} catch (e) { bildir('Hata: ' + e.message, 'hata'); }
yukle();
}
async function teslim(id, durum) {
try {
await api(`/api/orders/${id}/shipment`, { method: 'POST', body: { durum, takip_no: 'TRK' + id } });
bildir(`Sipariş #${id} → ${durum}`, 'basari');
} catch (e) { bildir('Hata: ' + e.message, 'hata'); }
yukle();
}
$('#yGonder').onclick = async () => {
const hedef = $('#yHedef').value;
try {
const r = await api('/api/orders', {
method: 'POST',
body: {
musteri_id: Number($('#yMusteri').value),
kalemler: [{ product_id: Number($('#yUrun').value), adet: Number($('#yAdet').value) }],
idempotency_key: 'ord-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7),
hedef_teslim: hedef ? new Date(hedef).toISOString() : undefined
}
});
bildir(`Sipariş #${r.siparis.id} oluşturuldu (stok rezerve edildi).`, 'basari');
yukle();
} catch (e) { bildir('Reddedildi: ' + e.message, 'hata'); }
};
$('#mGonder').onclick = async () => {
try {
await api('/api/customers', { method: 'POST', body: { ad: $('#mAd').value, telefon: $('#mTel').value, email: $('#mMail').value, adres: $('#mAdres').value } });
bildir('Müşteri eklendi.', 'basari');
$('#mAd').value = $('#mTel').value = $('#mMail').value = $('#mAdres').value = '';
yukle();
} catch (e) { bildir('Hata: ' + e.message, 'hata'); }
};
document.querySelectorAll('nav button').forEach((b) => {
b.onclick = () => {
document.querySelectorAll('nav button').forEach((x) => x.classList.remove('aktif'));
document.querySelectorAll('section').forEach((x) => x.classList.remove('gorunur'));
b.classList.add('aktif');
document.getElementById(b.dataset.tab).classList.add('gorunur');
};
});
yukle();
setInterval(yukle, 10000);
</script>
</body>
</html>