Add customers CRUD section to admin panel

This commit is contained in:
2026-05-22 21:52:56 +00:00
parent c3882fd23b
commit 3b0daa72e4
+236 -2
View File
@@ -241,6 +241,36 @@ if ($isAjax) {
echo json_encode(['ok'=>true]);
exit;
}
if ($action === 'customer_save') {
$cid = (int)($_POST['id'] ?? 0);
$name = substr(trim($_POST['name'] ?? ''), 0, 150);
$email = trim($_POST['email'] ?? '');
$phone = substr(trim($_POST['phone'] ?? ''), 0, 30);
$dob = preg_match('/^\d{4}-\d{2}-\d{2}$/', $_POST['dob'] ?? '') ? $_POST['dob'] : null;
$addr = substr(trim($_POST['address'] ?? ''), 0, 500);
$notes = substr(trim($_POST['notes'] ?? ''), 0, 1000);
$active = (int)($_POST['is_active'] ?? 1);
if (!$name || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['error' => 'Name and valid email are required']); exit;
}
if ($cid) {
db()->prepare("UPDATE pcs_customers SET name=?,email=?,phone=?,dob=?,address=?,notes=?,is_active=? WHERE id=?")
->execute([$name,$email,$phone,$dob,$addr,$notes,$active,$cid]);
} else {
db()->prepare("INSERT INTO pcs_customers (name,email,phone,dob,address,notes,is_active) VALUES (?,?,?,?,?,?,?)")
->execute([$name,$email,$phone,$dob,$addr,$notes,1]);
$cid = (int)db()->lastInsertId();
}
echo json_encode(['ok'=>true,'id'=>$cid]); exit;
}
if ($action === 'customer_delete') {
$cid = (int)($_POST['id'] ?? 0);
if ($cid) db()->prepare("DELETE FROM pcs_customers WHERE id=?")->execute([$cid]);
echo json_encode(['ok'=>true]); exit;
}
exit;
}
@@ -285,8 +315,13 @@ button:hover{background:#ea580c}
<?php exit; }
// ── Dashboard data ─────────────────────────────────────────────────────────────
$bookings = db()->query("SELECT * FROM bookings ORDER BY rental_date ASC, created_at DESC")->fetchAll();
$blocked = db()->query("SELECT * FROM blocked_dates ORDER BY block_date ASC")->fetchAll();
$bookings = db()->query("SELECT * FROM bookings ORDER BY rental_date ASC, created_at DESC")->fetchAll();
$blocked = db()->query("SELECT * FROM blocked_dates ORDER BY block_date ASC")->fetchAll();
$customers = db()->query("
SELECT c.*,
(SELECT COUNT(*) FROM bookings WHERE email=c.email) AS booking_count
FROM pcs_customers c ORDER BY c.created_at DESC
")->fetchAll();
$stats = db()->query("
SELECT
@@ -413,6 +448,21 @@ textarea.notes-ta:focus{border-color:#f97316}
.del-btn{background:none;border:none;color:#dc2626;cursor:pointer;font-size:1rem;padding:0;line-height:1}
@media(max-width:700px){.main{padding:1rem}.stat-val{font-size:1.5rem}}
/* Customer card */
.cust-search{border:1px solid #e5e7eb;border-radius:6px;padding:.45rem .75rem;font-size:.875rem;width:100%;max-width:280px}
.cust-form{background:#f9fafb;border-top:1px solid #f3f4f6;padding:1.25rem 1.5rem;display:none}
.cust-form.open{display:block}
.cust-form .fg{display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin-bottom:.75rem}
@media(max-width:600px){.cust-form .fg{grid-template-columns:1fr}}
.cust-form label{font-size:.78rem;color:#6b7280;display:block;margin-bottom:.2rem}
.cust-form input,.cust-form textarea{width:100%;border:1px solid #e5e7eb;border-radius:6px;padding:.45rem .65rem;font-size:.875rem;font-family:inherit;outline:none}
.cust-form input:focus,.cust-form textarea:focus{border-color:#f97316}
.cust-form textarea{resize:vertical;min-height:60px}
.cust-form .form-actions{display:flex;gap:.5rem;margin-top:.75rem}
.cust-badge{display:inline-block;padding:.15rem .5rem;border-radius:999px;font-size:.7rem;font-weight:700}
.cust-active{background:#dcfce7;color:#15803d}
.cust-inactive{background:#f3f4f6;color:#9ca3af}
</style>
</head>
<body>
@@ -784,6 +834,93 @@ textarea.notes-ta:focus{border-color:#f97316}
</div>
</div>
<!-- Customers -->
<div class="card">
<div class="card-header">
<h2>Customers</h2>
<div style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap">
<input class="cust-search" type="search" id="custSearch" placeholder="Search name, email, phone…" oninput="filterCustomers(this.value)">
<button class="save-btn" onclick="toggleCustForm(0)" id="addCustBtn">+ Add Customer</button>
</div>
</div>
<!-- Add/Edit form -->
<div class="cust-form" id="custForm">
<input type="hidden" id="custId" value="0">
<div class="fg">
<div>
<label>Full Name *</label>
<input type="text" id="custName" placeholder="Jane Doe" maxlength="150">
</div>
<div>
<label>Email *</label>
<input type="email" id="custEmail" placeholder="jane@example.com">
</div>
<div>
<label>Phone</label>
<input type="tel" id="custPhone" placeholder="(817) 555-0100" maxlength="30">
</div>
<div>
<label>Date of Birth</label>
<input type="date" id="custDob">
</div>
<div style="grid-column:1/-1">
<label>Address</label>
<input type="text" id="custAddr" placeholder="123 Main St, Weatherford TX 76086" maxlength="500">
</div>
<div style="grid-column:1/-1">
<label>Notes</label>
<textarea id="custNotes" maxlength="1000" placeholder="e.g. Preferred customer, allergies, anything relevant…"></textarea>
</div>
</div>
<div class="form-actions">
<button class="save-btn" onclick="saveCustomer()">Save Customer</button>
<button class="save-btn" onclick="toggleCustForm(0)" style="background:#6b7280">Cancel</button>
</div>
<div id="custFormMsg" style="margin-top:.5rem;font-size:.8rem;display:none"></div>
</div>
<?php if (empty($customers)): ?>
<div style="padding:3rem;text-align:center;color:#9ca3af">No customers yet.</div>
<?php else: ?>
<div style="overflow-x:auto">
<table id="custTable">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>DOB</th>
<th>Bookings</th>
<th>Status</th>
<th>Added</th>
<th style="width:80px"></th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $c): ?>
<tr class="cust-row" data-search="<?= strtolower(htmlspecialchars($c['name'].' '.$c['email'].' '.($c['phone']??''))) ?>">
<td><strong><?= htmlspecialchars($c['name']) ?></strong></td>
<td style="font-size:.82rem"><a href="mailto:<?= htmlspecialchars($c['email']) ?>" style="color:#f97316;text-decoration:none"><?= htmlspecialchars($c['email']) ?></a></td>
<td style="font-size:.82rem"><?= htmlspecialchars($c['phone'] ?? '—') ?></td>
<td style="font-size:.82rem"><?= $c['dob'] ? date('M j, Y', strtotime($c['dob'])) : '—' ?></td>
<td style="text-align:center"><?= (int)$c['booking_count'] ?></td>
<td><span class="cust-badge <?= $c['is_active']?'cust-active':'cust-inactive' ?>"><?= $c['is_active']?'Active':'Inactive' ?></span></td>
<td style="font-size:.78rem;color:#9ca3af;white-space:nowrap"><?= date('M j, Y', strtotime($c['created_at'])) ?></td>
<td style="white-space:nowrap">
<button class="flow-toggle" style="font-size:.72rem;margin-right:4px"
onclick="editCustomer(<?= htmlspecialchars(json_encode($c), ENT_QUOTES) ?>)">Edit</button>
<button class="flow-toggle" style="font-size:.72rem;border-color:#dc2626;color:#dc2626"
onclick="deleteCustomer(<?= $c['id'] ?>,this)">Del</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div><!-- /.main -->
<script>
@@ -982,6 +1119,103 @@ function squareAction(id, action, btn) {
});
}
// ── Customer CRUD ─────────────────────────────────────────────────────────────
function toggleCustForm(id) {
const form = document.getElementById('custForm');
const isOpen = form.classList.contains('open');
if (isOpen && id === 0) {
form.classList.remove('open');
return;
}
if (!isOpen || id === 0) {
// Reset form for new customer
document.getElementById('custId').value = 0;
document.getElementById('custName').value = '';
document.getElementById('custEmail').value = '';
document.getElementById('custPhone').value = '';
document.getElementById('custDob').value = '';
document.getElementById('custAddr').value = '';
document.getElementById('custNotes').value = '';
const msg = document.getElementById('custFormMsg');
msg.style.display = 'none';
document.getElementById('addCustBtn').textContent = '✕ Close';
}
form.classList.add('open');
document.getElementById('custName').focus();
}
function editCustomer(c) {
document.getElementById('custId').value = c.id;
document.getElementById('custName').value = c.name || '';
document.getElementById('custEmail').value = c.email || '';
document.getElementById('custPhone').value = c.phone || '';
document.getElementById('custDob').value = c.dob || '';
document.getElementById('custAddr').value = c.address || '';
document.getElementById('custNotes').value = c.notes || '';
const msg = document.getElementById('custFormMsg');
msg.style.display = 'none';
const form = document.getElementById('custForm');
form.classList.add('open');
document.getElementById('addCustBtn').textContent = '✕ Close';
form.scrollIntoView({behavior:'smooth', block:'nearest'});
}
function saveCustomer() {
const id = document.getElementById('custId').value;
const name = document.getElementById('custName').value.trim();
const email = document.getElementById('custEmail').value.trim();
const msg = document.getElementById('custFormMsg');
if (!name || !email) { showCustMsg('Name and email are required.', 'red'); return; }
const body = 'action=customer_save'
+ '&id=' + encodeURIComponent(id)
+ '&name=' + encodeURIComponent(name)
+ '&email=' + encodeURIComponent(email)
+ '&phone=' + encodeURIComponent(document.getElementById('custPhone').value)
+ '&dob=' + encodeURIComponent(document.getElementById('custDob').value)
+ '&address=' + encodeURIComponent(document.getElementById('custAddr').value)
+ '&notes=' + encodeURIComponent(document.getElementById('custNotes').value)
+ '&_t=' + ADMIN_TOKEN;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body
}).then(r=>r.json()).then(d=>{
if (d.ok) {
showCustMsg(id === '0' ? 'Customer added.' : 'Saved.', '#16a34a');
setTimeout(()=>location.reload(), 900);
} else {
showCustMsg(d.error || 'Error saving.', 'red');
}
}).catch(()=>showCustMsg('Request failed.', 'red'));
}
function deleteCustomer(id, btn) {
if (!confirm('Delete this customer? This cannot be undone.')) return;
btn.disabled = true;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=customer_delete&id='+id+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
if (d.ok) btn.closest('tr').remove();
else { btn.disabled=false; alert(d.error||'Error deleting.'); }
});
}
function filterCustomers(q) {
const term = q.toLowerCase().trim();
document.querySelectorAll('.cust-row').forEach(row=>{
row.style.display = (!term || row.dataset.search.includes(term)) ? '' : 'none';
});
}
function showCustMsg(text, color) {
const msg = document.getElementById('custFormMsg');
msg.textContent = text;
msg.style.color = color;
msg.style.display = 'block';
}
// ── Block / unblock dates ─────────────────────────────────────────────────────
function blockDate(e) {
e.preventDefault();