File: /home/muratemr/theotto.tr/admin/customers.php
<?php
/**
* admin/customers.php
*/
declare(strict_types=1);
$page_title = 'Müşteri CRM';
$active_menu = 'customers';
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('customers');
require_once __DIR__ . '/../includes/admin_layout.php';
$filter_name = get_str('name');
$filter_phone = get_str('phone');
$filter_tag = get_int('tag_id');
$page = max(1, get_int('page', 1));
$per_page = 25;
$where = ['1=1'];
$params = [];
if ($filter_name !== '') {
$where[] = 'c.full_name LIKE ?';
$params[] = '%' . $filter_name . '%';
}
if ($filter_phone !== '') {
// Ham arama + normalize edilmiş son 9 hane ile eşleştir
$norm_phone = normalize_phone($filter_phone);
$last9 = strlen($norm_phone) >= 9 ? substr($norm_phone, -9) : $filter_phone;
$where[] = "(c.phone LIKE ? OR RIGHT(REGEXP_REPLACE(c.phone,'[^0-9]',''), 9) = ?)";
$params[] = '%' . $filter_phone . '%';
$params[] = $last9;
}
if ($filter_tag > 0) {
$where[] = 'c.tag_id = ?';
$params[] = $filter_tag;
}
$where_str = implode(' AND ', $where);
$total = (int)db_value("SELECT COUNT(*) FROM customers c WHERE $where_str", $params);
$pg = paginate($total, $page, $per_page);
$customers = db_rows("
SELECT c.*,
ct.name AS tag_name,
ct.discount_rate,
COUNT(r.id) AS total_res,
SUM(CASE WHEN r.status='onaylandi' OR r.status='arsiv' THEN 1 ELSE 0 END) AS confirmed_res,
SUM(CASE WHEN r.visit_status='geldi' THEN 1 ELSE 0 END) AS came_count,
SUM(CASE WHEN r.visit_status='gelmedi' THEN 1 ELSE 0 END) AS no_show_count
FROM customers c
LEFT JOIN customer_tags ct ON ct.id = c.tag_id
LEFT JOIN reservations r ON r.customer_id = c.id
WHERE $where_str
GROUP BY c.id
ORDER BY c.created_at DESC
LIMIT {$pg['limit']} OFFSET {$pg['offset']}
", $params);
$tags = db_rows('SELECT id, name, discount_rate FROM customer_tags ORDER BY name ASC');
// WhatsApp mesaj şablonları
$wa_templates = [
['key'=>'discount5', 'label'=>'%5 İndirim'],
['key'=>'discount10', 'label'=>'%10 İndirim'],
['key'=>'discount15', 'label'=>'%15 İndirim'],
['key'=>'birthday', 'label'=>'Doğum Günü'],
['key'=>'tag', 'label'=>'Etikete Özel İndirim'],
['key'=>'free', 'label'=>'Serbest Mesaj'],
];
$restaurant_name = get_setting('restaurant_name', 'Restoran');
?>
<!-- Filtreler -->
<div class="card mb-3">
<div class="card-body py-2">
<form method="GET" class="row g-2 align-items-end">
<div class="col-12 col-md-3">
<label class="form-label">Ad Soyad</label>
<input type="text" name="name" class="form-control form-control-sm" value="<?= e($filter_name) ?>" placeholder="İsim ara...">
</div>
<div class="col-12 col-md-3">
<label class="form-label">Telefon</label>
<input type="text" name="phone" class="form-control form-control-sm" value="<?= e($filter_phone) ?>" placeholder="Telefon ara...">
</div>
<div class="col-12 col-md-3">
<label class="form-label">Etiket</label>
<select name="tag_id" class="form-select form-select-sm">
<option value="">Tümü</option>
<?php foreach ($tags as $t): ?>
<option value="<?= $t['id'] ?>" <?= $filter_tag === (int)$t['id'] ? 'selected' : '' ?>><?= e($t['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-sm btn-gold flex-fill">
<i class="bi bi-search me-1"></i>Filtrele
</button>
<a href="customers.php" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-x-lg"></i>
</a>
</div>
</form>
</div>
</div>
<!-- Tablo -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-people me-2"></i>Müşteriler (<?= $total ?>)</span>
<?php if (is_admin()): ?>
<button class="btn btn-warning btn-sm" onclick="openCustomerModal()">
<i class="bi bi-plus-lg me-1"></i>Yeni Müşteri
</button>
<?php endif; ?>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Ad Soyad</th>
<th>Telefon</th>
<th>Etiket</th>
<th>İndirim</th>
<th>Toplam Res.</th>
<th>Onaylı</th>
<th>Geldi</th>
<th>Gelmedi</th>
<th>Kayıt</th>
<th>İşlemler</th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $c): ?>
<tr>
<td>
<span><?= e($c['full_name']) ?></span>
<?php
$has_name_log = $c['admin_note'] && preg_match('/^\d{2}\.\d{2}\.\d{4}: /', $c['admin_note']);
if ($has_name_log): ?>
<span style="display:inline-block;width:9px;height:9px;border-radius:50%;
background:var(--gold);margin-left:5px;vertical-align:middle"
title="Farklı isimle rezervasyon geçmişi var"></span>
<?php endif; ?>
</td>
<td>
<?= e($c['phone']) ?>
</td>
<td>
<?php if ($c['tag_name']): ?>
<span class="badge bg-secondary"><?= e($c['tag_name']) ?></span>
<?php else: ?>
<span class="text-muted fs-12">—</span>
<?php endif; ?>
</td>
<td>
<?= $c['discount_rate'] > 0
? '<span class="badge bg-success">%' . $c['discount_rate'] . '</span>'
: '<span class="text-muted fs-12">—</span>' ?>
</td>
<td><?= $c['total_res'] ?></td>
<td><?= $c['confirmed_res'] ?></td>
<td><span class="text-success"><?= $c['came_count'] ?></span></td>
<td><span class="text-danger"><?= $c['no_show_count'] ?></span></td>
<td class="fs-12 text-muted"><?= format_date($c['created_at']) ?></td>
<td>
<div class="d-flex gap-1 flex-wrap">
<a href="customer_detail.php?id=<?= $c['id'] ?>"
class="btn btn-sm btn-outline-info btn-action" title="Detay Görüntüle">
<i class="bi bi-eye"></i>
</a>
<a href="customer_wa.php?id=<?= $c['id'] ?>"
class="btn btn-sm btn-outline-success btn-action" title="WhatsApp">
<i class="bi bi-whatsapp"></i>
</a>
<?php if (is_admin()): ?>
<a href="customer_edit.php?id=<?= $c['id'] ?>"
class="btn btn-sm btn-outline-warning btn-action" title="Düzenle">
<i class="bi bi-pencil"></i>
</a>
<button class="btn btn-sm btn-outline-danger btn-action" title="Sil"
onclick="deleteCustomer(<?= $c['id'] ?>, '<?= e(addslashes($c['full_name'])) ?>')">
<i class="bi bi-trash"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($customers)): ?>
<tr><td colspan="10" class="text-center text-muted py-4">Müşteri bulunamadı.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
$any_name_change = false;
foreach ($customers as $c2) {
if (!empty($c2['admin_note']) && preg_match('/^\d{2}\.\d{2}\.\d{4}: /', $c2['admin_note'])) {
$any_name_change = true; break;
}
}
if ($any_name_change): ?>
<div class="px-3 py-2" style="font-size:11px;color:rgba(255,255,255,0.45);border-top:1px solid var(--dark-border)">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--gold);margin-right:5px;vertical-align:middle"></span>
Farklı isimle rezervasyon geçmişi olan müşteri
</div>
<?php endif; ?>
</div>
<!-- Pagination -->
<?php if ($pg['total_pages'] > 1): ?>
<div class="card-body border-top py-2">
<nav>
<ul class="pagination pagination-sm mb-0 justify-content-center">
<?php for ($i = 1; $i <= $pg['total_pages']; $i++): ?>
<li class="page-item <?= $i === $pg['current_page'] ? 'active' : '' ?>">
<a class="page-link" href="?page=<?= $i ?>&name=<?= e($filter_name) ?>&phone=<?= e($filter_phone) ?>&tag_id=<?= $filter_tag ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
</div>
<?php endif; ?>
</div>
<!-- Etiket Atama Modalı -->
<div class="modal fade" id="tagModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title"><i class="bi bi-tag me-2"></i>Etiket Ata</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<select id="tagSelectInput" class="form-select">
<option value="">— Etiketsiz —</option>
<?php foreach ($tags as $t): ?>
<option value="<?= $t['id'] ?>"><?= e($t['name']) ?><?= $t['discount_rate'] ? ' (%'.$t['discount_rate'].')' : '' ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">İptal</button>
<button type="button" class="btn btn-gold btn-sm" id="tagSaveBtn">Kaydet</button>
</div>
</div>
</div>
</div>
<!-- Not Düzenleme Modalı -->
<!-- Müşteri Detay Modalı -->
<div class="modal fade" id="customerDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<div>
<h6 class="modal-title mb-0" id="detailModalName">—</h6>
<small class="text-muted" id="detailModalPhone"></small>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0" id="detailModalBody">
<div class="text-center py-5">
<div class="spinner-border spinner-border-sm text-warning"></div>
</div>
</div>
<div class="modal-footer" id="detailModalFooter" style="display:none">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Kapat</button>
<button type="button" class="btn btn-warning btn-sm" id="detailEditBtn">
<i class="bi bi-pencil me-1"></i>Düzenle
</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="noteModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<div>
<h6 class="modal-title mb-0" id="noteModalTitle">
<i class="bi bi-person-circle me-2 text-warning"></i>
<span id="noteModalCustomerName"></span>
</h6>
<small class="text-muted" id="noteModalPhone"></small>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<!-- İsim Değişim Geçmişi -->
<div id="nameHistorySection" style="display:none">
<div class="px-3 pt-3 pb-2"
style="border-bottom:1px solid rgba(255,193,7,0.2);background:rgba(255,193,7,0.05)">
<div class="d-flex align-items-center gap-2 mb-2">
<i class="bi bi-clock-history text-warning"></i>
<span class="fw-semibold" style="font-size:13px;color:var(--gold)">İsim Değişim Geçmişi</span>
</div>
<div id="nameHistoryCards"></div>
</div>
</div>
<!-- Admin Notu -->
<div class="p-3">
<label class="form-label d-flex align-items-center gap-2 mb-2">
<i class="bi bi-sticky text-warning"></i>
<span style="font-size:13px;font-weight:500">Admin Notu</span>
</label>
<textarea id="noteInput" class="form-control" rows="4"
placeholder="Müşteri hakkında not ekleyin..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">İptal</button>
<button type="button" class="btn btn-gold btn-sm" id="noteSaveBtn">
<i class="bi bi-check-lg me-1"></i>Kaydet
</button>
</div>
</div>
</div>
</div>
<!-- Müşteri Ekle/Düzenle Modalı (sadece admin) -->
<?php if (is_admin()): ?>
<div class="modal fade" id="customerModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content bg-dark">
<div class="modal-header border-secondary">
<h6 class="modal-title" id="customerModalTitle"><i class="bi bi-person me-2"></i>Yeni Müşteri</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="customerEditId">
<div class="mb-3">
<label class="form-label">Ad Soyad <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="customerFullName" placeholder="Ad Soyad">
</div>
<div class="mb-3">
<label class="form-label">Telefon <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="customerPhone" placeholder="5XXXXXXXXX">
</div>
<div class="mb-3">
<label class="form-label">Admin Notu</label>
<textarea class="form-control" id="customerNote" rows="3" placeholder="Müşteri hakkında not..."></textarea>
</div>
</div>
<div class="modal-footer border-secondary">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">İptal</button>
<button type="button" class="btn btn-warning btn-sm" id="customerSaveBtn">
<i class="bi bi-check-lg me-1"></i>Kaydet
</button>
</div>
</div>
</div>
</div>
<?php endif; ?>
<!-- WhatsApp Mesaj Modalı -->
<div class="modal fade" id="waModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title"><i class="bi bi-whatsapp text-success me-2"></i>WhatsApp Mesajı</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Şablon</label>
<select id="waTemplate" class="form-select form-select-sm" onchange="previewWAMessage()">
<?php foreach ($wa_templates as $tpl): ?>
<option value="<?= $tpl['key'] ?>"><?= $tpl['label'] ?></option>
<?php endforeach; ?>
</select>
</div>
<div id="waFreeWrap" class="mb-3 d-none">
<label class="form-label">Mesaj</label>
<textarea id="waFreeText" class="form-control" rows="4" placeholder="Mesajınızı yazın..."></textarea>
</div>
<div class="mb-2">
<label class="form-label">Önizleme</label>
<div id="waPreview" class="p-3 rounded" style="background:#0d1117;font-size:13px;white-space:pre-wrap;border:1px solid var(--dark-border);min-height:60px;"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">İptal</button>
<button type="button" class="btn btn-success btn-sm" id="waSendBtn">
<i class="bi bi-whatsapp me-1"></i>WhatsApp'ta Aç
</button>
</div>
</div>
</div>
</div>
<script>
let currentCustomerId = null;
let currentCustomerName = '';
let currentCustomerPhone = '';
let currentTagName = '';
let currentDiscount = 0;
// ============================================================
// Etiket Modalı
// ============================================================
function openTagModal(id, tagId) {
currentCustomerId = id;
document.getElementById('tagSelect').value = tagId || '';
openModal('tagModal');
}
document.getElementById('tagSaveBtn').addEventListener('click', async () => {
const res = await apiPost('/api/admin/customers.php', {
action: 'set_tag',
id: currentCustomerId,
tag_id: document.getElementById('tagSelect').value || 0,
});
if (res.success) { closeModal('tagModal'); showToast(res.message, 'success'); setTimeout(() => location.reload(), 600); }
else showToast(res.message, 'error');
});
// ============================================================
// Not Modalı
// ============================================================
function openNoteModal(id, note, customerName, customerPhone) {
currentCustomerId = id;
document.getElementById('noteModalCustomerName').textContent = customerName || '';
document.getElementById('noteModalPhone').textContent = customerPhone || '';
var nameLogPattern = /^\d{2}\.\d{2}\.\d{4}: /;
var lines = (note || '').split('
');
var nameLogs = lines.filter(function(l) { return nameLogPattern.test(l.trim()); });
var adminNote = lines.filter(function(l) { return !nameLogPattern.test(l.trim()); }).join('
').trim();
var histSection = document.getElementById('nameHistorySection');
var histCards = document.getElementById('nameHistoryCards');
if (nameLogs.length > 0) {
histCards.innerHTML = nameLogs.map(function(log) {
var dateM = log.match(/^(\d{2}\.\d{2}\.\d{4})/);
var newM = log.match(/"([^"]+)" adıyla rezervasyon/);
var oldM = log.match(/\(eski: "([^"]+)"\)/);
return '<div style="display:flex;align-items:center;gap:8px;padding:6px 8px;background:rgba(0,0,0,0.2);border-radius:5px;margin-bottom:4px;font-size:12px">'
+ '<span style="color:rgba(255,255,255,0.4);min-width:75px">' + (dateM ? dateM[1] : '') + '</span>'
+ '<span style="color:rgba(255,255,255,0.5)">' + (oldM ? oldM[1] : '') + '</span>'
+ '<i class="bi bi-arrow-right" style="color:var(--gold);font-size:11px"></i>'
+ '<span style="color:#fff;font-weight:500">' + (newM ? newM[1] : '') + '</span>'
+ '</div>';
}).join('');
histSection.style.display = '';
} else {
histSection.style.display = 'none';
}
var ta = document.getElementById('noteInput');
ta.value = adminNote;
ta.dataset.fullNote = note || '';
openModal('noteModal');
}
document.getElementById('noteSaveBtn').addEventListener('click', async function() {
var existing = document.getElementById('noteInput').dataset.fullNote || '';
var nameLogRx = /^\d{2}\.\d{2}\.\d{4}: /;
var nameLogs = existing.split('
').filter(function(l) { return nameLogRx.test(l.trim()); });
var newNote = document.getElementById('noteInput').value.trim();
var fullNote = nameLogs.length > 0
? nameLogs.join('
') + (newNote ? '
' + newNote : '')
: newNote;
var res = await apiPost('/api/admin/customers.php', {
action: 'set_note', id: currentCustomerId, admin_note: fullNote
});
if (res.success) {
closeModal('noteModal');
showToast('Not güncellendi.', 'success');
setTimeout(function() { location.reload(); }, 700);
} else {
showToast(res.message, 'error');
}
});
// ============================================================
// WhatsApp Modalı
// ============================================================
function openWAModal(id, name, phone, tagName, discount) {
currentCustomerId = id;
currentCustomerName = name;
currentCustomerPhone = phone;
currentTagName = tagName;
currentDiscount = discount;
document.getElementById('waTemplate').value = 'discount5';
document.getElementById('waFreeWrap').classList.add('d-none');
previewWAMessage();
openModal('waModal');
}
function previewWAMessage() {
var tpl = document.getElementById('waTemplate').value;
var freeWrap = document.getElementById('waFreeWrap');
var msg = '';
if (tpl === 'free') {
freeWrap.classList.remove('d-none');
msg = document.getElementById('waFreeText').value;
} else {
freeWrap.classList.add('d-none');
var tplMap = {
'discount5' : WA_CUST_MSGS['msg_wa_discount5'] || '',
'discount10': WA_CUST_MSGS['msg_wa_discount10'] || '',
'discount15': WA_CUST_MSGS['msg_wa_discount15'] || '',
'birthday' : WA_CUST_MSGS['msg_wa_birthday'] || '',
'tag' : WA_CUST_MSGS['msg_wa_tag'] || '',
};
var custVars = { '{isim}': currentCustomerName, '{restoran}': RESTAURANT_NAME, '{etiket}': currentTagName, '{indirim}': String(currentDiscount) };
msg = buildCustMsg(tplMap[tpl] || '', custVars);
}
document.getElementById('waPreview').textContent = msg;
}
document.getElementById('waFreeText').addEventListener('input', previewWAMessage);
document.getElementById('waSendBtn').addEventListener('click', function() {
var msg = document.getElementById('waPreview').textContent;
openWhatsApp(currentCustomerPhone, msg);
closeModal('waModal');
});
// ============================================================
// Müşteri Ekle/Düzenle (admin)
// ============================================================
function openCustomerModal(id, name, phone, note) {
id = id || null;
name = name || '';
phone = phone || '';
note = note || '';
document.getElementById('customerEditId').value = id || '';
document.getElementById('customerFullName').value = name;
document.getElementById('customerPhone').value = phone;
document.getElementById('customerNote').value = note;
document.getElementById('customerModalTitle').innerHTML =
'<i class="bi bi-person me-2"></i>' + (id ? 'Müşteri Düzenle' : 'Yeni Müşteri');
openModal('customerModal');
}
document.getElementById('customerSaveBtn').addEventListener('click', async function() {
var id = document.getElementById('customerEditId').value;
var fullName = document.getElementById('customerFullName').value.trim();
var phone = document.getElementById('customerPhone').value.trim();
var note = document.getElementById('customerNote').value.trim();
if (!fullName) { showToast('Ad Soyad zorunlu.', 'error'); return; }
if (!phone) { showToast('Telefon zorunlu.', 'error'); return; }
var btn = document.getElementById('customerSaveBtn');
btn.disabled = true;
var res = await apiPost('/api/admin/customers.php', {
action: id ? 'update' : 'create', id: id, full_name: fullName, phone: phone, admin_note: note
});
btn.disabled = false;
if (res.success) { closeModal('customerModal'); showToast(res.message, 'success'); setTimeout(function() { location.reload(); }, 600); }
else showToast(res.message, 'error');
});
</script>
<?php require_once __DIR__ . '/../includes/admin_layout_end.php'; ?>
<script>
function deleteCustomer(id, name) {
confirmAction('"' + name + '" silinsin mi? Rezervasyon geçmişi etkilenebilir.', async function() {
var res = await apiPost('/api/admin/customers.php', { action: 'delete', id: id });
if (res.success) {
showToast(res.message, 'success');
setTimeout(function() { location.reload(); }, 700);
} else {
showToast(res.message, 'error');
}
});
}
</script>