Switch email to CyberMail API; integrations page manages API key via DB

This commit is contained in:
2026-05-29 18:49:15 +00:00
parent 6b71828199
commit 76bf967bd0
5 changed files with 479 additions and 304 deletions
+234
View File
@@ -0,0 +1,234 @@
<?php
ob_start();
/**
* Tom's Java Jive - Admin Email Log
*/
$pageTitle = 'Email Log';
require_once __DIR__ . '/includes/header.php';
// Refresh delivery status for a message
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'refresh_status') {
$logId = (int)($_POST['log_id'] ?? 0);
$messageId = trim($_POST['message_id'] ?? '');
if ($logId && $messageId) {
require_once dirname(__DIR__) . '/includes/email.php';
$data = emailService()->checkDeliveryStatus($messageId);
if (!empty($data)) {
$statusMap = ['queued'=>'sent','sent'=>'sent','delivered'=>'delivered','bounced'=>'bounced','failed'=>'failed'];
db()->update('email_log', [
'status' => $statusMap[$data['status']] ?? 'unknown',
'opened' => $data['opened'] ?? 0,
'opened_at' => !empty($data['opened_at']) ? date('Y-m-d H:i:s', strtotime($data['opened_at'])) : null,
'open_count' => $data['open_count'] ?? 0,
'clicked' => $data['clicked'] ?? 0,
'click_count' => $data['click_count'] ?? 0,
'status_checked_at'=> date('Y-m-d H:i:s'),
], 'id = :id', ['id' => $logId]);
}
}
header('Location: /admin/email-log.php' . (!empty($_POST['customer_filter']) ? '?customer=' . urlencode($_POST['customer_filter']) : ''));
exit;
}
// Filters
$customerFilter = trim($_GET['customer'] ?? '');
$statusFilter = trim($_GET['status'] ?? '');
$search = trim($_GET['search'] ?? '');
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 25;
$offset = ($page - 1) * $perPage;
$where = [];
$params = [];
if ($customerFilter) {
$where[] = 'l.customer_id = :customer_id';
$params['customer_id'] = $customerFilter;
}
if ($statusFilter) {
$where[] = 'l.status = :status';
$params['status'] = $statusFilter;
}
if ($search) {
$where[] = '(l.recipient_email LIKE :search OR l.subject LIKE :search)';
$params['search'] = '%' . $search . '%';
}
$whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$total = db()->fetch(
"SELECT COUNT(*) as cnt FROM email_log l $whereClause",
$params
)['cnt'] ?? 0;
$logs = db()->fetchAll(
"SELECT l.*, c.name as customer_name
FROM email_log l
LEFT JOIN customers c ON l.customer_id = c.customer_id
$whereClause
ORDER BY l.sent_at DESC
LIMIT $perPage OFFSET $offset",
$params
);
$totalPages = max(1, ceil($total / $perPage));
// Status badge helper
$statusBadge = [
'sent' => ['bg'=>'#3B82F6','label'=>'Sent'],
'delivered' => ['bg'=>'#10B981','label'=>'Delivered'],
'bounced' => ['bg'=>'#EF4444','label'=>'Bounced'],
'failed' => ['bg'=>'#EF4444','label'=>'Failed'],
'unknown' => ['bg'=>'#9CA3AF','label'=>'Unknown'],
];
?>
<div class="admin-content">
<div class="content-header">
<h1 class="content-title"><i class="fas fa-envelope-open-text"></i> Email Log</h1>
<div style="display:flex;gap:10px;align-items:center;">
<span style="color:#666;font-size:.9em;"><?= number_format($total) ?> emails total</span>
</div>
</div>
<!-- Filters -->
<form method="GET" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:20px;align-items:flex-end;">
<div>
<label style="display:block;font-size:.8em;color:#666;margin-bottom:4px;">Search</label>
<input type="text" name="search" value="<?= htmlspecialchars($search) ?>"
placeholder="Email or subject..." class="form-control" style="width:220px;">
</div>
<div>
<label style="display:block;font-size:.8em;color:#666;margin-bottom:4px;">Status</label>
<select name="status" class="form-control" style="width:140px;">
<option value="">All Statuses</option>
<?php foreach (array_keys($statusBadge) as $s): ?>
<option value="<?= $s ?>" <?= $statusFilter === $s ? 'selected' : '' ?>><?= ucfirst($s) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($customerFilter): ?>
<input type="hidden" name="customer" value="<?= htmlspecialchars($customerFilter) ?>">
<div style="align-self:flex-end;">
<span style="background:#FF5E1A;color:white;padding:4px 10px;border-radius:4px;font-size:.85em;">
Filtered by customer &nbsp;
<a href="/admin/email-log.php" style="color:white;">&times;</a>
</span>
</div>
<?php endif; ?>
<div style="align-self:flex-end;">
<button type="submit" class="btn btn-primary">Filter</button>
<a href="/admin/email-log.php" class="btn btn-secondary">Reset</a>
</div>
</form>
<!-- Table -->
<div class="table-container">
<table class="admin-table">
<thead>
<tr>
<th>Recipient</th>
<th>Subject</th>
<th>Preview</th>
<th>Status</th>
<th>Opened</th>
<th>Clicked</th>
<th>Sent At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($logs)): ?>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#999;">No emails found.</td></tr>
<?php else: foreach ($logs as $log):
$badge = $statusBadge[$log['status']] ?? $statusBadge['unknown'];
?>
<tr>
<td>
<div><?= htmlspecialchars($log['recipient_email']) ?></div>
<?php if ($log['customer_name']): ?>
<small style="color:#666;">
<a href="/admin/customers.php?highlight=<?= urlencode($log['customer_id']) ?>" style="color:#FF5E1A;">
<?= htmlspecialchars($log['customer_name']) ?>
</a>
</small>
<?php endif; ?>
</td>
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
<?= htmlspecialchars($log['subject']) ?>
<?php if ($log['tags']): ?>
<br><small style="color:#9CA3AF;"><?= htmlspecialchars($log['tags']) ?></small>
<?php endif; ?>
</td>
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#666;font-size:.85em;">
<?= htmlspecialchars($log['preview'] ?? '') ?>
</td>
<td>
<span style="background:<?= $badge['bg'] ?>;color:white;padding:3px 8px;border-radius:4px;font-size:.8em;font-weight:600;">
<?= $badge['label'] ?>
</span>
<?php if ($log['error_message']): ?>
<br><small style="color:#EF4444;font-size:.75em;"><?= htmlspecialchars(substr($log['error_message'],0,60)) ?></small>
<?php endif; ?>
</td>
<td style="text-align:center;">
<?php if ($log['opened']): ?>
<span style="color:#10B981;" title="<?= $log['open_count'] ?> opens<?= $log['opened_at'] ? ', first '.date('M j g:ia',strtotime($log['opened_at'])) : '' ?>">
✓ <?= $log['open_count'] > 1 ? '('.$log['open_count'].')' : '' ?>
</span>
<?php else: ?>
<span style="color:#9CA3AF;">—</span>
<?php endif; ?>
</td>
<td style="text-align:center;">
<?php if ($log['clicked']): ?>
<span style="color:#3B82F6;">✓ <?= $log['click_count'] > 1 ? '('.$log['click_count'].')' : '' ?></span>
<?php else: ?>
<span style="color:#9CA3AF;">—</span>
<?php endif; ?>
</td>
<td style="white-space:nowrap;font-size:.85em;">
<?= date('M j, Y', strtotime($log['sent_at'])) ?><br>
<span style="color:#666;"><?= date('g:i a', strtotime($log['sent_at'])) ?></span>
<?php if ($log['status_checked_at']): ?>
<br><small style="color:#9CA3AF;">checked <?= date('M j g:ia', strtotime($log['status_checked_at'])) ?></small>
<?php endif; ?>
</td>
<td>
<?php if ($log['message_id']): ?>
<form method="POST" style="display:inline;">
<input type="hidden" name="action" value="refresh_status">
<input type="hidden" name="log_id" value="<?= $log['id'] ?>">
<input type="hidden" name="message_id" value="<?= htmlspecialchars($log['message_id']) ?>">
<?php if ($customerFilter): ?>
<input type="hidden" name="customer_filter" value="<?= htmlspecialchars($customerFilter) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-sm btn-secondary" title="Refresh delivery status">
<i class="fas fa-sync-alt"></i>
</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div style="display:flex;justify-content:center;gap:8px;margin-top:20px;">
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a href="?page=<?= $i ?>&search=<?= urlencode($search) ?>&status=<?= urlencode($statusFilter) ?>&customer=<?= urlencode($customerFilter) ?>"
style="padding:6px 12px;border-radius:4px;text-decoration:none;
background:<?= $i === $page ? '#FF5E1A' : '#f3f4f6' ?>;
color:<?= $i === $page ? 'white' : '#374151' ?>;">
<?= $i ?>
</a>
<?php endfor; ?>
</div>
<?php endif; ?>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
+43 -45
View File
@@ -3,33 +3,31 @@
* Tom's Java Jive - Admin Integrations Settings
*/
// Auth only — no HTML yet
require_once __DIR__ . '/../includes/auth.php';
AdminAuth::require();
// Handle POST before any output
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$section = $_POST['section'] ?? '';
if ($section === 'cybermail') {
setSetting('cybermail_api_key', trim($_POST['cybermail_api_key'] ?? ''));
setSetting('cybermail_from_email', trim($_POST['cybermail_from_email'] ?? ''));
setSetting('cybermail_from_name', trim($_POST['cybermail_from_name'] ?? ''));
setSetting('email_notifications_enabled', isset($_POST['email_notifications_enabled']) ? '1' : '0');
setFlash('success', 'CyberMail settings saved.');
if ($section === 'email') {
setSetting('cybermail_api_key', trim($_POST['cybermail_api_key'] ?? ''));
setSetting('cybermail_from_email', trim($_POST['cybermail_from_email'] ?? ''));
setSetting('cybermail_from_name', trim($_POST['cybermail_from_name'] ?? ''));
setSetting('email_notifications_enabled', isset($_POST['email_notifications_enabled']) ? '1' : '0');
setFlash('success', 'Email settings saved.');
}
if ($section === 'twilio') {
setSetting('twilio_account_sid', trim($_POST['twilio_account_sid'] ?? ''));
setSetting('twilio_auth_token', trim($_POST['twilio_auth_token'] ?? ''));
setSetting('twilio_phone_number', trim($_POST['twilio_phone_number'] ?? ''));
setSetting('twilio_account_sid', trim($_POST['twilio_account_sid'] ?? ''));
setSetting('twilio_auth_token', trim($_POST['twilio_auth_token'] ?? ''));
setSetting('twilio_phone_number', trim($_POST['twilio_phone_number'] ?? ''));
setSetting('sms_notifications_enabled', isset($_POST['sms_notifications_enabled']) ? '1' : '0');
setFlash('success', 'Twilio settings saved.');
}
if ($section === 'push') {
setSetting('vapid_public_key', trim($_POST['vapid_public_key'] ?? ''));
setSetting('vapid_private_key', trim($_POST['vapid_private_key'] ?? ''));
setSetting('vapid_public_key', trim($_POST['vapid_public_key'] ?? ''));
setSetting('vapid_private_key', trim($_POST['vapid_private_key'] ?? ''));
setSetting('push_notifications_enabled', isset($_POST['push_notifications_enabled']) ? '1' : '0');
setFlash('success', 'Push notification settings saved.');
}
@@ -43,23 +41,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit;
}
// --- GET: render page ---
ob_start();
$pageTitle = 'Integrations';
$currentPage = 'integrations';
require_once __DIR__ . '/includes/header.php';
// Load current settings
$cm = [
'api_key' => getSetting('cybermail_api_key', ''),
'from_email' => getSetting('cybermail_from_email', 'noreply@tomsjavajive.com'),
'from_name' => getSetting('cybermail_from_name', "Tom's Java Jive"),
'enabled' => getSetting('email_notifications_enabled', '0'),
$email = [
'api_key' => getSetting('cybermail_api_key', ''),
'from' => getSetting('cybermail_from_email', 'noreply@tomsjavajive.com'),
'from_name' => getSetting('cybermail_from_name', "Tom's Java Jive"),
'enabled' => getSetting('email_notifications_enabled', '0'),
];
$tw = [
'sid' => getSetting('twilio_account_sid', ''),
'token' => getSetting('twilio_auth_token', ''),
'phone' => getSetting('twilio_phone_number', ''),
'sid' => getSetting('twilio_account_sid', ''),
'token' => getSetting('twilio_auth_token', ''),
'phone' => getSetting('twilio_phone_number', ''),
'enabled' => getSetting('sms_notifications_enabled', '0'),
];
$push = [
@@ -75,10 +71,10 @@ $loyaltyEnabled = getSetting('loyalty_enabled', '1') === '1';
.integration-header { display: flex; justify-content: space-between; align-items: center; padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--admin-border); }
.integration-title { display: flex; align-items: center; gap: 1rem; }
.integration-icon { width: 48px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; }
.integration-icon.cybermail { background: #0ea5e9; color: white; }
.integration-icon.twilio { background: #F22F46; color: white; }
.integration-icon.push { background: #8B5CF6; color: white; }
.integration-icon.loyalty { background: #F59E0B; color: white; }
.integration-icon.email { background: #0ea5e9; color: white; }
.integration-icon.twilio { background: #F22F46; color: white; }
.integration-icon.push { background: #8B5CF6; color: white; }
.integration-icon.loyalty { background: #F59E0B; color: white; }
.integration-body { padding: 1.5rem; }
.status-badge { padding: .375rem .75rem; border-radius: 20px; font-size: .75rem; font-weight: 600; }
.status-badge.configured { background: rgba(16,185,129,.1); color: var(--admin-success); }
@@ -99,49 +95,51 @@ $loyaltyEnabled = getSetting('loyalty_enabled', '1') === '1';
<div class="alert alert-success mb-2"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
<?php endif; ?>
<!-- CyberMail -->
<!-- Email — CyberMail -->
<div class="integration-card">
<div class="integration-header">
<div class="integration-title">
<div class="integration-icon cybermail"><i class="fas fa-envelope"></i></div>
<div class="integration-icon email"><i class="fas fa-envelope"></i></div>
<div>
<h3 style="margin:0;">CyberMail</h3>
<p style="margin:.25rem 0 0;color:var(--admin-text-muted);font-size:.875rem;">Transactional email — order confirmations, shipping updates</p>
<h3 style="margin:0;">Email — CyberMail</h3>
<p style="margin:.25rem 0 0;color:var(--admin-text-muted);font-size:.875rem;">Transactional email — order confirmations, shipping updates, password resets</p>
</div>
</div>
<?php $cmOk = !empty($cm['api_key']); $cmOn = $cm['enabled'] === '1'; ?>
<span class="status-badge <?= $cmOk && $cmOn ? 'enabled' : ($cmOk ? 'configured' : 'not-configured') ?>">
<?= $cmOk && $cmOn ? 'Enabled' : ($cmOk ? 'Configured' : 'Not Configured') ?>
<?php $emailOk = !empty($email['api_key']); $emailOn = $email['enabled'] === '1'; ?>
<span class="status-badge <?= $emailOk && $emailOn ? 'enabled' : ($emailOk ? 'configured' : 'not-configured') ?>">
<?= $emailOk && $emailOn ? 'Enabled' : ($emailOk ? 'Configured' : 'Not Configured') ?>
</span>
</div>
<div class="integration-body">
<form method="POST">
<input type="hidden" name="section" value="cybermail">
<input type="hidden" name="section" value="email">
<div class="form-group">
<label class="form-label">CyberMail API Key</label>
<input type="password" name="cybermail_api_key" class="form-input key-input"
value="<?= htmlspecialchars($cm['api_key']) ?>" placeholder="sk_live_...">
value="<?= htmlspecialchars($email['api_key']) ?>" placeholder="sk_live_...">
<p class="help-text">Manage at <a href="https://platform.cyberpersons.com/email/api-keys/" target="_blank" class="help-link">CyberMail Dashboard</a></p>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">From Email</label>
<input type="email" name="cybermail_from_email" class="form-input" value="<?= htmlspecialchars($cm['from_email']) ?>">
<input type="email" name="cybermail_from_email" class="form-input"
value="<?= htmlspecialchars($email['from']) ?>">
</div>
<div class="form-group">
<label class="form-label">From Name</label>
<input type="text" name="cybermail_from_name" class="form-input" value="<?= htmlspecialchars($cm['from_name']) ?>">
<input type="text" name="cybermail_from_name" class="form-input"
value="<?= htmlspecialchars($email['from_name']) ?>">
</div>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" name="email_notifications_enabled" value="1" <?= $cmOn ? 'checked' : '' ?>>
<input type="checkbox" name="email_notifications_enabled" value="1" <?= $emailOn ? 'checked' : '' ?>>
Enable email notifications
</label>
</div>
<div style="display:flex;gap:.5rem;">
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Save Settings</button>
<button type="button" class="btn btn-secondary" onclick="testCyberMail()"><i class="fas fa-paper-plane"></i> Send Test Email</button>
<button type="button" class="btn btn-secondary" onclick="testEmail()"><i class="fas fa-paper-plane"></i> Send Test Email</button>
</div>
</form>
</div>
@@ -296,7 +294,7 @@ $loyaltyEnabled = getSetting('loyalty_enabled', '1') === '1';
<script>
let testType = '';
function testCyberMail() {
function testEmail() {
testType = 'email';
document.getElementById('testModalTitle').textContent = 'Send Test Email';
document.getElementById('testInputLabel').textContent = 'Recipient Email';
@@ -322,19 +320,19 @@ document.getElementById('testForm').addEventListener('submit', async function(e)
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
try {
const response = await fetch('/api/test-notification.php', {
const r = await fetch('/api/test-notification.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type: testType, recipient})
});
const data = await response.json();
const data = await r.json();
resultDiv.style.display = 'block';
if (data.success) {
resultDiv.style.background = 'rgba(16,185,129,.1)';
resultDiv.innerHTML = '<i class="fas fa-check-circle" style="color:var(--admin-success);"></i> ' + (data.message || 'Sent successfully!');
resultDiv.innerHTML = '<i class="fas fa-check-circle" style="color:var(--admin-success);"></i> ' + (data.message || 'Sent!');
} else {
resultDiv.style.background = 'rgba(239,68,68,.1)';
resultDiv.innerHTML = '<i class="fas fa-times-circle" style="color:var(--admin-error);"></i> ' + (data.error || 'Failed to send');
resultDiv.innerHTML = '<i class="fas fa-times-circle" style="color:var(--admin-error);"></i> ' + (data.error || 'Failed');
}
} catch (err) {
resultDiv.style.display = 'block';