File: /home/muratemr/theotto.tr/admin/dashboard.php
<?php
/**
* admin/dashboard.php
*/
declare(strict_types=1);
$page_title = 'Dashboard';
$active_menu = 'dashboard';
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/csrf.php';
require_permission('dashboard');
require_once __DIR__ . '/../includes/admin_layout.php';
// ---- İstatistikler ----
function get_dashboard_stats(): array
{
$today = date('Y-m-d');
$month = date('Y-m');
return [
'res_pending' => (int)db_value("SELECT COUNT(*) FROM reservations WHERE status='bekliyor'"),
'res_confirmed'=> (int)db_value("SELECT COUNT(*) FROM reservations WHERE status='onaylandi'"),
'res_archive' => (int)db_value("SELECT COUNT(*) FROM reservations WHERE status='arsiv'"),
'res_total' => (int)db_value("SELECT COUNT(*) FROM reservations"),
'customers' => (int)db_value("SELECT COUNT(*) FROM customers"),
'suppliers' => (int)db_value("SELECT COUNT(*) FROM suppliers WHERE is_active=1"),
'orders_today' => (int)db_value("SELECT COUNT(*) FROM daily_orders WHERE order_date=?", [$today]),
'orders_month' => (int)db_value("SELECT COUNT(*) FROM daily_orders WHERE DATE_FORMAT(order_date,'%Y-%m')=?", [$month]),
'orders_today_amt' => (float)db_value("
SELECT COALESCE(SUM(doi.quantity * COALESCE(doi.approx_price,0)),0)
FROM daily_orders do2
JOIN daily_order_items doi ON doi.order_id=do2.id
WHERE do2.order_date=?", [$today]),
'orders_month_amt' => (float)db_value("
SELECT COALESCE(SUM(doi.quantity * COALESCE(doi.approx_price,0)),0)
FROM daily_orders do2
JOIN daily_order_items doi ON doi.order_id=do2.id
WHERE DATE_FORMAT(do2.order_date,'%Y-%m')=?", [$month]),
'staff_working' => (int)db_value("SELECT COUNT(*) FROM staff_shifts WHERE date=? AND is_work_day=1", [$today]),
'staff_leave' => (int)db_value("SELECT COUNT(*) FROM staff_shifts WHERE date=? AND is_work_day=0", [$today]),
'late_today' => (int)db_value("SELECT COUNT(*) FROM staff_attendance WHERE date=? AND status='late'", [$today]),
'late_month' => (int)db_value("SELECT COUNT(*) FROM staff_attendance WHERE DATE_FORMAT(date,'%Y-%m')=? AND status='late'", [$month]),
];
}
$stats = get_dashboard_stats();
// Son bekleyen rezervasyonlar
$pending_res = db_rows("
SELECT id, full_name, phone, guest_count, date, time, created_at
FROM reservations
WHERE status='bekliyor'
ORDER BY created_at DESC
LIMIT 8
");
// Son 7 günlük rezervasyon grafiği verisi
$last7 = db_rows("
SELECT DATE_FORMAT(date,'%d.%m') AS d, COUNT(*) AS cnt
FROM reservations
WHERE date BETWEEN DATE_SUB(CURDATE(),INTERVAL 6 DAY) AND CURDATE()
GROUP BY date
ORDER BY date ASC
");
?>
<!-- Refresh seçimi -->
<div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="mb-0 text-gold"><i class="bi bi-speedometer2 me-2"></i>Genel Bakış</h5>
<div class="d-flex align-items-center gap-2">
<select id="refreshSelect" class="form-select form-select-sm" style="width:auto;background:#1a1a1a;color:var(--text-primary);border-color:var(--dark-border)">
<option value="0">Otomatik yenileme kapalı</option>
<option value="15">15 saniye</option>
<option value="30" selected>30 saniye</option>
<option value="60">1 dakika</option>
<option value="120">2 dakika</option>
</select>
<span class="refresh-badge" id="refreshBadge" onclick="loadStats()" title="Şimdi yenile">
<i class="bi bi-arrow-clockwise me-1"></i><span id="refreshCountdown">—</span>
</span>
</div>
</div>
<!-- Ana Layout: Sol=Stat Kartlar, Sağ=Hızlı İşlemler+Grafik -->
<div class="row g-3 mb-3">
<!-- SOL: 8 stat kartı 2 sütun grid -->
<div class="col-12 col-lg-8">
<div class="row g-3">
<div class="col-6">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(255,193,7,0.15)">
<i class="bi bi-hourglass-split" style="color:#ffc107"></i>
</div>
<div>
<div class="stat-val" id="s-res-pending"><?= $stats['res_pending'] ?></div>
<div class="stat-lbl">Bekleyen Rezervasyon</div>
</div>
</div>
</div>
<div class="col-6">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(40,167,69,0.15)">
<i class="bi bi-calendar-check" style="color:#28a745"></i>
</div>
<div>
<div class="stat-val" id="s-res-confirmed"><?= $stats['res_confirmed'] ?></div>
<div class="stat-lbl">Onaylı Rezervasyon</div>
</div>
</div>
</div>
<div class="col-6">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(111,66,193,0.15)">
<i class="bi bi-truck" style="color:#6f42c1"></i>
</div>
<div>
<div class="stat-val" id="s-suppliers"><?= $stats['suppliers'] ?></div>
<div class="stat-lbl">Aktif Tedarikçi</div>
</div>
</div>
</div>
<div class="col-6">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(253,126,20,0.15)">
<i class="bi bi-cart3" style="color:#fd7e14"></i>
</div>
<div>
<div class="stat-val" id="s-orders-today"><?= $stats['orders_today'] ?></div>
<div class="stat-lbl">Bugün Sipariş</div>
</div>
</div>
</div>
<div class="col-6">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(23,162,184,0.15)">
<i class="bi bi-archive" style="color:#17a2b8"></i>
</div>
<div>
<div class="stat-val" id="s-res-archive"><?= $stats['res_archive'] ?></div>
<div class="stat-lbl">Arşiv</div>
</div>
</div>
</div>
<div class="col-6">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(201,168,76,0.15)">
<i class="bi bi-people" style="color:var(--gold)"></i>
</div>
<div>
<div class="stat-val" id="s-customers"><?= $stats['customers'] ?></div>
<div class="stat-lbl">Kayıtlı Müşteri</div>
</div>
</div>
</div>
<div class="col-6">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(32,201,151,0.15)">
<i class="bi bi-person-badge" style="color:#20c997"></i>
</div>
<div>
<div class="stat-val" id="s-staff-working"><?= $stats['staff_working'] ?></div>
<div class="stat-lbl">Bugün Vardiyada</div>
</div>
</div>
</div>
<div class="col-6">
<div class="stat-card">
<div class="stat-icon" style="background:rgba(220,53,69,0.15)">
<i class="bi bi-clock-history" style="color:#dc3545"></i>
</div>
<div>
<div class="stat-val" id="s-late-today"><?= $stats['late_today'] ?></div>
<div class="stat-lbl">Bugün Geç Kalan</div>
</div>
</div>
</div>
</div>
</div>
<!-- SAĞ: Hızlı İşlemler + Son 7 Gün -->
<div class="col-12 col-lg-4 d-flex flex-column gap-3">
<!-- Hızlı İşlemler -->
<div class="card">
<div class="card-header">
<i class="bi bi-lightning me-2 text-gold"></i>Hızlı İşlemler
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-6">
<a href="reservations.php" class="btn btn-outline-secondary w-100 text-start fs-12">
<i class="bi bi-calendar-plus me-1"></i>Rezervasyon Yönet
</a>
</div>
<div class="col-6">
<a href="daily_orders.php" class="btn btn-outline-secondary w-100 text-start fs-12">
<i class="bi bi-plus-circle me-1"></i>Yeni Sipariş
</a>
</div>
<div class="col-6">
<a href="customers.php" class="btn btn-outline-secondary w-100 text-start fs-12">
<i class="bi bi-person-plus me-1"></i>Müşteri CRM
</a>
</div>
<div class="col-6">
<a href="staff.php" class="btn btn-outline-secondary w-100 text-start fs-12">
<i class="bi bi-person-check me-1"></i>Günlük Devam
</a>
</div>
</div>
</div>
</div>
<!-- Son 7 Gün Rezervasyon -->
<div class="card flex-grow-1">
<div class="card-header">
<i class="bi bi-bar-chart me-2 text-gold"></i>Son 7 Gün Rezervasyon
</div>
<div class="card-body d-flex align-items-center">
<canvas id="resChart" style="width:100%;max-height:180px"></canvas>
</div>
</div>
</div>
</div>
<!-- Alt: Bekleyen Rezervasyonlar - tam genişlik -->
<div class="row g-3">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-hourglass-split me-2 text-warning"></i>Bekleyen Rezervasyonlar</span>
<a href="reservations.php" class="btn btn-sm btn-outline-secondary fs-12">Tümü</a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Tarih/Saat</th>
<th>Ad Soyad</th>
<th>Kişi</th>
<th>İşlem</th>
</tr>
</thead>
<tbody id="pendingResList">
<?php foreach ($pending_res as $r): ?>
<tr>
<td>
<div><?= format_date($r['date']) ?></div>
<div class="text-muted fs-12"><?= format_time($r['time']) ?></div>
</td>
<td>
<div><?= e($r['full_name']) ?></div>
<div class="text-muted fs-12"><?= e($r['phone']) ?></div>
</td>
<td><span class="badge bg-secondary"><?= $r['guest_count'] ?> kişi</span></td>
<td>
<a href="reservations.php?id=<?= $r['id'] ?>" class="btn btn-sm btn-outline-warning btn-action">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($pending_res)): ?>
<tr><td colspan="4" class="text-center text-muted py-4">Bekleyen rezervasyon yok.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Yeni rezervasyon bildirim modalı -->
<div class="notification-modal-overlay d-none" id="newResModal">
<div class="notification-modal-box alarm-box">
<div class="alarm-bell-wrap mb-3">
<i class="bi bi-bell-fill alarm-bell-icon"></i>
</div>
<div class="alarm-title mb-1">YENİ REZERVASYON!</div>
<div class="alarm-subtitle mb-3">Onay bekliyor</div>
<div id="newResContent" class="mb-4 text-start alarm-content"></div>
<button class="btn btn-gold px-5 py-2 fw-bold" id="newResOkBtn" style="font-size:15px;min-width:200px">
<i class="bi bi-check-lg me-2"></i>TAMAM, GÖRDÜM
</button>
<div class="mt-3" style="font-size:11px;color:var(--text-muted)">
Bu butona basana kadar alarm çalmaya devam eder
</div>
</div>
</div>
<?php
$chartLabels = array_column($last7, 'd');
$chartData = array_column($last7, 'cnt');
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
<script>
// ---- Chart ----
(function() {
const ctx = document.getElementById('resChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: <?= json_encode($chartLabels) ?>,
datasets: [{
label: 'Rezervasyon',
data: <?= json_encode($chartData) ?>,
backgroundColor: 'rgba(201,168,76,0.5)',
borderColor: '#c9a84c',
borderWidth: 1,
borderRadius: 4,
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#666' }, grid: { color: 'rgba(255,255,255,0.05)' } },
y: { beginAtZero: true, ticks: { color: '#666', stepSize: 1 }, grid: { color: 'rgba(255,255,255,0.05)' } }
}
}
});
})();
// ---- Stats JSON yenileme ----
async function loadStats() {
try {
const data = await apiGet('/api/admin/dashboard_stats.php');
if (!data.success) return;
const s = data.stats;
document.getElementById('s-res-pending').textContent = s.res_pending;
document.getElementById('s-res-confirmed').textContent = s.res_confirmed;
document.getElementById('s-res-archive').textContent = s.res_archive;
document.getElementById('s-customers').textContent = s.customers;
document.getElementById('s-suppliers').textContent = s.suppliers;
document.getElementById('s-orders-today').textContent = s.orders_today;
document.getElementById('s-staff-working').textContent = s.staff_working;
document.getElementById('s-late-today').textContent = s.late_today;
} catch(e) {}
}
// ================================================================
// YENİ REZERVASYON ALARM SİSTEMİ
// ================================================================
// Son görülen rezervasyon ID'si - DB'den al, sessionStorage ile say. yenilemede koru
const _DB_MAX_ID = <?= (int)(db_value('SELECT COALESCE(MAX(id),0) FROM reservations') ?: 0) ?>;
(function() {
const stored = parseInt(sessionStorage.getItem('otto_last_res_id') || '0');
// stored varsa ve dbMaxId'den büyükse stored'u kullan (bir önceki oturumda yeni res gördük)
// stored dbMaxId'den büyükse: büyük olasılıkla eski session, dbMaxId kullan
if (stored > 0 && stored <= _DB_MAX_ID) {
window._lastKnownResId = stored;
} else {
window._lastKnownResId = _DB_MAX_ID;
sessionStorage.setItem('otto_last_res_id', _DB_MAX_ID);
}
})();
let lastKnownResId = window._lastKnownResId;
let audioInterval = null;
let audioCtx = null;
// AudioContext - kullanıcı etkileşiminde unlock
function getAudioCtx() {
if (!audioCtx) {
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
catch(e) { console.warn('AudioContext oluşturulamadı:', e); return null; }
}
return audioCtx;
}
// Her etkileşimde unlock dene
['click','keydown','touchend','touchstart'].forEach(ev => {
document.addEventListener(ev, function _unlock() {
const ctx = getAudioCtx();
if (ctx && ctx.state === 'suspended') ctx.resume();
}, { once: false, passive: true });
});
function _playRing(ctx, t) {
[440, 480].forEach(freq => {
try {
const osc = ctx.createOscillator(), g = ctx.createGain();
osc.connect(g); g.connect(ctx.destination);
osc.type = 'sine'; osc.frequency.value = freq;
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(0.6, t + 0.03);
g.gain.setValueAtTime(0.6, t + 0.37);
g.gain.linearRampToValueAtTime(0, t + 0.42);
osc.start(t); osc.stop(t + 0.45);
} catch(e) {}
});
}
function playAlarm() {
const ctx = getAudioCtx();
if (!ctx) return;
const go = () => {
const t = ctx.currentTime;
_playRing(ctx, t + 0.05); _playRing(ctx, t + 0.55);
_playRing(ctx, t + 1.40); _playRing(ctx, t + 1.90);
};
if (ctx.state === 'suspended') { ctx.resume().then(go); } else { go(); }
}
function showAlarmModal(reservations) {
const r = reservations[0];
document.getElementById('newResContent').innerHTML =
`<b>Ad Soyad:</b> ${r.full_name}<br>
<b>Telefon:</b> ${r.phone}<br>
<b>Tarih:</b> ${r.date}<br>
<b>Saat:</b> ${r.time}<br>
<b>Kişi Sayısı:</b> ${r.guest_count}` +
(reservations.length > 1 ? `<br><br><span class="text-warning">+${reservations.length-1} rezervasyon daha var!</span>` : '');
document.getElementById('newResModal').classList.remove('d-none');
if (!audioInterval) { audioInterval = setInterval(playAlarm, 2800); }
playAlarm();
}
async function checkNewReservations() {
try {
const url = '/api/admin/check_new_reservations.php?last_id=' + lastKnownResId + '&_=' + Date.now();
const res = await fetch(url, {
headers: { 'X-CSRF-Token': CSRF_TOKEN, 'X-Requested-With': 'XMLHttpRequest' },
cache: 'no-store'
});
if (!res.ok) {
console.warn('Rezervasyon kontrol hatası:', res.status);
return;
}
let data;
try { data = await res.json(); }
catch(e) { console.warn('JSON parse hatası - session süresi dolmuş olabilir'); return; }
if (!data.success) { console.warn('API hatası:', data.message); return; }
// max_id'yi her zaman güncelle (yeni res yoksa da)
if (data.max_id > lastKnownResId) {
lastKnownResId = data.max_id;
sessionStorage.setItem('otto_last_res_id', lastKnownResId);
}
if (data.new_count > 0) {
showAlarmModal(data.reservations);
}
} catch(e) { console.warn('checkNewReservations hata:', e.message); }
}
document.getElementById('newResOkBtn').addEventListener('click', () => {
document.getElementById('newResModal').classList.add('d-none');
if (audioInterval) { clearInterval(audioInterval); audioInterval = null; }
// Rezervasyonlar sayfasına git
// location.href = '/admin/reservations.php'; // İstersen açabilirsiniz
});
</script>
<?php require_once __DIR__ . '/../includes/admin_layout_end.php'; ?>
<script>
// ---- Auto refresh ----
let refreshSeconds = 30;
const refreshSelect = document.getElementById('refreshSelect');
refreshSelect.addEventListener('change', () => {
refreshSeconds = parseInt(refreshSelect.value);
stopAutoRefresh();
if (refreshSeconds > 0) {
document.getElementById('refreshCountdown').textContent = refreshSeconds + 's';
startAutoRefresh(refreshSeconds, loadStats);
} else {
document.getElementById('refreshCountdown').textContent = '—';
}
});
// Sayfa açılır açılmaz hemen geri sayımı başlat
document.getElementById('refreshCountdown').textContent = refreshSeconds + 's';
startAutoRefresh(refreshSeconds, loadStats);
// Yeni rezervasyon kontrolü her 15 sn + sayfa açılınca hemen bir kez
setInterval(checkNewReservations, 15000);
checkNewReservations();
</script>