614 lines
31 KiB
HTML
614 lines
31 KiB
HTML
<!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>
|