N8N API Testing

This commit is contained in:
Flatlogic Bot 2026-01-02 22:45:22 +00:00
parent 3def426cf9
commit 26d8efd021
17 changed files with 1558 additions and 156 deletions

View File

@ -1,18 +1,12 @@
DirectoryIndex index.php index.html
Options -Indexes
Options -MultiViews
RewriteEngine On
# 0) Serve existing files/directories as-is
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Route API calls to the correct script
RewriteRule ^api/bookings$ api/bookings.php [L]
RewriteRule ^api/ai-call-logs$ api/ai-call-logs.php [L]
RewriteRule ^api/call-tracking$ api/call-tracking.php [L]
RewriteRule ^api/reviews$ api/reviews.php [L]
# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists)
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.+?)/?$ $1.php [L]
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
# Standard rule to pass requests to index.php if they are not files or directories
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ $1 [R=301,L]
RewriteRule ^.*$ index.php [L]

263
ai-call-logs.php Normal file
View File

@ -0,0 +1,263 @@
<?php
require_once 'db/config.php';
try {
$pdo = db();
// Fetch all AI call logs
$stmt_call_logs = $pdo->query('SELECT * FROM ai_call_logs ORDER BY call_start_time DESC');
$call_logs = $stmt_call_logs->fetchAll();
// Calculate performance metrics
$total_calls = count($call_logs);
$total_duration_seconds = 0;
foreach ($call_logs as $log) {
$start = new DateTime($log['call_start_time']);
$end = new DateTime($log['call_end_time']);
$duration = $end->getTimestamp() - $start->getTimestamp();
$total_duration_seconds += $duration;
}
$avg_call_duration = $total_calls > 0 ? $total_duration_seconds / $total_calls : 0;
// Data for charts
$intent_distribution = [];
$outcome_distribution = [];
foreach ($call_logs as $log) {
$intent_distribution[$log['call_intent']] = ($intent_distribution[$log['call_intent']] ?? 0) + 1;
$outcome_distribution[$log['call_outcome']] = ($outcome_distribution[$log['call_outcome']] ?? 0) + 1;
}
} catch (PDOException $e) {
$error = "Database error: " . $e->getMessage();
}
$project_name = "HVAC Command Center";
$page_title = "AI Call Logs";
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($page_title) ?> | <?= htmlspecialchars($project_name) ?></title>
<!-- Styles -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<header class="header text-white">
<div class="container-fluid">
<h1 class="display-6 m-0"><?= htmlspecialchars($project_name) ?></h1>
</div>
</header>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="customers.php">Customers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="bookings.php">Bookings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="ai-call-logs.php">AI Call Logs</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container-fluid mt-4">
<?php if (isset($error)): ?>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle-fill"></i> <?= htmlspecialchars($error) ?>
</div>
<?php else: ?>
<!-- Performance Metrics -->
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100 shadow-sm">
<div class="card-body d-flex align-items-center">
<i class="bi bi-telephone-fill display-4 text-primary me-3"></i>
<div>
<h5 class="card-title">Total Calls</h5>
<p class="card-text fs-2 fw-bold"><?= htmlspecialchars($total_calls) ?></p>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 shadow-sm">
<div class="card-body d-flex align-items-center">
<i class="bi bi-clock-history display-4 text-info me-3"></i>
<div>
<h5 class="card-title">Avg. Call Duration</h5>
<p class="card-text fs-2 fw-bold"><?= htmlspecialchars(round($avg_call_duration)) ?>s</p>
</div>
</div>
</div>
</div>
</div>
<!-- Charts -->
<div class="row g-4 mt-2">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header">
<h5 class="m-0">Call Intent Distribution</h5>
</div>
<div class="card-body">
<canvas id="intentChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header">
<h5 class="m-0">Call Outcome Distribution</h5>
</div>
<div class="card-body">
<canvas id="outcomeChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Call Logs Table -->
<div class="card shadow-sm mt-4">
<div class="card-header">
<h5 class="m-0"><i class="bi bi-list-ul me-2"></i>All AI Call Logs</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Call ID</th>
<th>Start Time</th>
<th>End Time</th>
<th>Intent</th>
<th>Outcome</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<?php foreach ($call_logs as $log): ?>
<tr>
<td><?= htmlspecialchars($log['call_id']) ?></td>
<td><?= htmlspecialchars(date("M d, Y H:i:s", strtotime($log['call_start_time']))) ?></td>
<td><?= htmlspecialchars(date("M d, Y H:i:s", strtotime($log['call_end_time']))) ?></td>
<td><?= htmlspecialchars($log['call_intent']) ?></td>
<td><?= htmlspecialchars($log['call_outcome']) ?></td>
<td><?= htmlspecialchars($log['call_summary']) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($call_logs)): ?>
<tr>
<td colspan="6" class="text-center text-muted">No call logs found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</main>
<footer class="container-fluid text-center text-muted py-3 mt-4">
<small>Powered by Flatlogic</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Intent Chart
const intentCtx = document.getElementById('intentChart').getContext('2d');
new Chart(intentCtx, {
type: 'doughnut',
data: {
labels: <?= json_encode(array_keys($intent_distribution)) ?>,
datasets: [{
label: 'Call Intents',
data: <?= json_encode(array_values($intent_distribution)) ?>,
backgroundColor: [
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)',
'rgba(75, 192, 192, 0.8)',
'rgba(153, 102, 255, 0.8)',
'rgba(255, 159, 64, 0.8)'
],
borderColor: '#fff',
borderWidth: 2
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
}
}
}
});
// Outcome Chart
const outcomeCtx = document.getElementById('outcomeChart').getContext('2d');
new Chart(outcomeCtx, {
type: 'pie',
data: {
labels: <?= json_encode(array_keys($outcome_distribution)) ?>,
datasets: [{
label: 'Call Outcomes',
data: <?= json_encode(array_values($outcome_distribution)) ?>,
backgroundColor: [
'rgba(75, 192, 192, 0.8)',
'rgba(255, 99, 132, 0.8)',
'rgba(255, 205, 86, 0.8)',
'rgba(201, 203, 207, 0.8)',
'rgba(54, 162, 235, 0.8)'
],
borderColor: '#fff',
borderWidth: 2
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
}
}
}
});
});
</script>
</body>
</html>

200
api-keys.php Normal file
View File

@ -0,0 +1,200 @@
<?php
require_once __DIR__ . '/api/keys.php';
$generated_key = null;
$error = null;
$success = null;
// Handle POST requests for key management
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['generate_api_key'])) {
$keyName = trim($_POST['key_name']);
$integrationType = trim($_POST['integration_type']);
$result = generateApiKey($keyName, $integrationType);
if ($result['success']) {
$generated_key = $result;
$success = "Successfully generated new API key. Please copy it now, it won't be shown again.";
} else {
$error = $result['message'];
}
} elseif (isset($_POST['deactivate_api_key'])) {
$keyId = $_POST['key_id'];
$result = deactivateApiKey($keyId);
if ($result['success']) {
$success = $result['message'];
} else {
$error = $result['message'];
}
} elseif (isset($_POST['delete_api_key'])) {
$keyId = $_POST['key_id'];
$result = deleteApiKey($keyId);
if ($result['success']) {
$success = $result['message'];
} else {
$error = $result['message'];
}
}
}
// Fetch all existing API keys to display
$api_keys = listApiKeys();
$project_name = "HVAC Command Center";
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Key Management - <?= htmlspecialchars($project_name) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<header class="header text-white">
<div class="container-fluid">
<h1 class="display-6 m-0"><?= htmlspecialchars($project_name) ?></h1>
</div>
</header>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="customers.php">Customers</a></li>
<li class="nav-item"><a class="nav-link" href="bookings.php">Bookings</a></li>
<li class="nav-item"><a class="nav-link" href="ai-call-logs.php">AI Call Logs</a></li>
<li class="nav-item"><a class="nav-link active" href="api-keys.php">API Keys</a></li>
</ul>
</div>
</div>
</nav>
<main class="container-fluid mt-4">
<h2 class="mb-4"><i class="bi bi-key-fill me-2"></i>API Key Management</h2>
<?php if ($error): ?>
<div class="alert alert-danger"><i class="bi bi-exclamation-triangle-fill me-2"></i><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><i class="bi bi-check-circle-fill me-2"></i><?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<?php if ($generated_key): ?>
<div class="alert alert-warning" id="generated-key-alert">
<h5 class="alert-heading"><i class="bi bi-shield-lock-fill me-2"></i>Your New API Key</h5>
<p>Please copy it now. It will not be shown again.</p>
<div class="input-group">
<input type="text" id="new-api-key" class="form-control form-control-lg bg-white" value="<?= htmlspecialchars($generated_key['api_key']) ?>" readonly style="font-family: monospace;">
<button class="btn btn-dark" type="button" onclick="copyToClipboard()">
<i class="bi bi-clipboard-check-fill me-2"></i>Copy
</button>
</div>
<hr>
<p class="mb-0">Use this key in the <code>Authorization</code> header as a Bearer token: <code>Bearer <?= htmlspecialchars($generated_key['api_key']) ?></code></p>
</div>
<?php endif; ?>
<div class="card shadow-sm mb-4">
<div class="card-header">
<h5 class="m-0">Create New API Key</h5>
</div>
<div class="card-body">
<form method="POST" action="api-keys.php">
<div class="row g-3 align-items-end">
<div class="col-md-5">
<label for="key_name" class="form-label">Key Name</label>
<input type="text" class="form-control" id="key_name" name="key_name" placeholder="e.g., N8N Production" required>
</div>
<div class="col-md-5">
<label for="integration_type" class="form-label">Integration Type</label>
<select class="form-select" id="integration_type" name="integration_type" required>
<option value="n8n">n8n</option>
<option value="callrail">CallRail</option>
<option value="twilio">Twilio</option>
<option value="giveme5">Giveme5.ai</option>
<option value="google">Google</option>
<option value="other">Other</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" name="generate_api_key" class="btn btn-primary w-100">Generate Key</button>
</div>
</div>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header">
<h5 class="m-0">Existing API Keys</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Integration</th>
<th>Status</th>
<th>Created</th>
<th>Last Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($api_keys)): ?>
<tr><td colspan="6" class="text-center text-muted">No API keys found.</td></tr>
<?php else: ?>
<?php foreach ($api_keys as $key): ?>
<tr>
<td><?= htmlspecialchars($key['key_name']) ?></td>
<td><span class="badge bg-secondary"><?= htmlspecialchars($key['integration_type']) ?></span></td>
<td><span class="badge bg-<?= $key['is_active'] ? 'success' : 'secondary' ?>"><?= $key['is_active'] ? 'Active' : 'Inactive' ?></span></td>
<td><?= htmlspecialchars(date("M d, Y", strtotime($key['created_at']))) ?></td>
<td><?= $key['last_used_at'] ? htmlspecialchars(date("M d, Y H:i", strtotime($key['last_used_at']))) : 'Never' ?></td>
<td>
<form method="POST" action="api-keys.php" onsubmit="return confirm('Are you sure?');" class="d-inline">
<input type="hidden" name="key_id" value="<?= $key['id'] ?>">
<?php if ($key['is_active']): ?>
<button type="submit" name="deactivate_api_key" class="btn btn-sm btn-warning" title="Deactivate"><i class="bi bi-pause-circle-fill"></i></button>
<?php endif; ?>
<button type="submit" name="delete_api_key" class="btn btn-sm btn-danger" title="Delete"><i class="bi bi-trash-fill"></i></button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</main>
<footer class="container-fluid text-center text-muted py-3 mt-4">
<small>Powered by Flatlogic</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function copyToClipboard() {
const keyInput = document.getElementById('new-api-key');
keyInput.select();
keyInput.setSelectionRange(0, 99999); // For mobile devices
document.execCommand('copy');
// Optional: Provide feedback to the user
const originalBtn = document.querySelector('#generated-key-alert button');
originalBtn.innerHTML = '<i class="bi bi-check-lg me-2"></i>Copied!';
setTimeout(() => {
originalBtn.innerHTML = '<i class="bi bi-clipboard-check-fill me-2"></i>Copy';
}, 2000);
}
</script>
</body>
</html>

64
api/ai-call-logs.php Normal file
View File

@ -0,0 +1,64 @@
<?php
require_once __DIR__ . '/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJsonResponse(['error' => 'Invalid request method'], 405);
exit;
}
if (!validateApiKey()) {
logWebhook('ai-call-logs', file_get_contents('php://input'), 401);
sendJsonResponse(['error' => 'Unauthorized'], 401);
exit;
}
$request_body = file_get_contents('php://input');
$data = json_decode($request_body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
logWebhook('ai-call-logs', $request_body, 400);
sendJsonResponse(['error' => 'Invalid JSON'], 400);
exit;
}
$errors = [];
if (empty($data['call_id'])) {
$errors[] = 'call_id is required';
}
if (empty($data['call_start_time'])) {
$errors[] = 'call_start_time is required';
}
if (empty($data['call_intent'])) {
$errors[] = 'call_intent is required';
}
if (empty($data['call_outcome'])) {
$errors[] = 'call_outcome is required';
}
if (!empty($errors)) {
logWebhook('ai-call-logs', $request_body, 422);
sendJsonResponse(['errors' => $errors], 422);
exit;
}
try {
$stmt = db()->prepare("INSERT INTO ai_call_logs (call_id, conversation_id, call_start_time, call_end_time, call_duration_seconds, call_intent, call_outcome, ai_summary) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$data['call_id'],
$data['conversation_id'] ?? null,
$data['call_start_time'],
$data['call_end_time'] ?? null,
$data['call_duration_seconds'] ?? null,
$data['call_intent'],
$data['call_outcome'],
$data['ai_summary'] ?? null
]);
$new_id = db()->lastInsertId();
logWebhook('ai-call-logs', $request_body, 201);
sendJsonResponse(['success' => true, 'id' => $new_id, 'message' => 'Call log created'], 201);
} catch (PDOException $e) {
error_log($e->getMessage());
logWebhook('ai-call-logs', $request_body, 500);
sendJsonResponse(['error' => 'Database error'], 500);
}

68
api/bookings.php Normal file
View File

@ -0,0 +1,68 @@
<?php
require_once __DIR__ . '/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJsonResponse(['error' => 'Invalid request method'], 405);
exit;
}
if (!validateApiKey()) {
logWebhook('bookings', file_get_contents('php://input'), 401);
sendJsonResponse(['error' => 'Unauthorized'], 401);
exit;
}
$request_body = file_get_contents('php://input');
$data = json_decode($request_body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
logWebhook('bookings', $request_body, 400);
sendJsonResponse(['error' => 'Invalid JSON'], 400);
exit;
}
$errors = [];
if (empty($data['record_type'])) {
$errors[] = 'record_type is required';
}
if (empty($data['customer_name'])) {
$errors[] = 'customer_name is required';
}
if (empty($data['customer_phone'])) {
$errors[] = 'customer_phone is required';
}
if (!empty($errors)) {
logWebhook('bookings', $request_body, 422);
sendJsonResponse(['errors' => $errors], 422);
exit;
}
try {
$stmt = db()->prepare("INSERT INTO bookings (record_type, customer_name, customer_phone, service_address, service_category, service_type, system_type, urgency_level, issue_description, appointment_date, appointment_time, status, estimated_revenue, actual_revenue, booked_by, customer_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$data['record_type'],
$data['customer_name'],
$data['customer_phone'],
$data['service_address'] ?? null,
$data['service_category'] ?? null,
$data['service_type'] ?? null,
$data['system_type'] ?? null,
$data['urgency_level'] ?? null,
$data['issue_description'] ?? null,
$data['appointment_date'] ?? null,
$data['appointment_time'] ?? null,
$data['status'] ?? 'new',
$data['estimated_revenue'] ?? null,
$data['actual_revenue'] ?? null,
$data['booked_by'] ?? 'online',
$data['customer_id'] ?? null
]);
$new_id = db()->lastInsertId();
logWebhook('bookings', $request_body, 201);
sendJsonResponse(['success' => true, 'id' => $new_id, 'message' => 'Booking created'], 201);
} catch (PDOException $e) {
error_log($e->getMessage());
logWebhook('bookings', $request_body, 500);
sendJsonResponse(['error' => 'Database error'], 500);
}

64
api/call-tracking.php Normal file
View File

@ -0,0 +1,64 @@
<?php
require_once __DIR__ . '/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJsonResponse(['error' => 'Invalid request method'], 405);
exit;
}
if (!validateApiKey()) {
logWebhook('call-tracking', file_get_contents('php://input'), 401);
sendJsonResponse(['error' => 'Unauthorized'], 401);
exit;
}
$request_body = file_get_contents('php://input');
$data = json_decode($request_body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
logWebhook('call-tracking', $request_body, 400);
sendJsonResponse(['error' => 'Invalid JSON'], 400);
exit;
}
$errors = [];
if (empty($data['external_call_id'])) {
$errors[] = 'external_call_id is required';
}
if (empty($data['tracking_platform'])) {
$errors[] = 'tracking_platform is required';
}
if (empty($data['call_start_time'])) {
$errors[] = 'call_start_time is required';
}
if (empty($data['call_status'])) {
$errors[] = 'call_status is required';
}
if (empty($data['traffic_source'])) {
$errors[] = 'traffic_source is required';
}
if (!empty($errors)) {
logWebhook('call-tracking', $request_body, 422);
sendJsonResponse(['errors' => $errors], 422);
exit;
}
try {
$stmt = db()->prepare("INSERT INTO call_tracking (external_call_id, tracking_platform, call_start_time, call_status, traffic_source) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([
$data['external_call_id'],
$data['tracking_platform'],
$data['call_start_time'],
$data['call_status'],
$data['traffic_source']
]);
$new_id = db()->lastInsertId();
logWebhook('call-tracking', $request_body, 201);
sendJsonResponse(['success' => true, 'id' => $new_id, 'message' => 'Call tracking created'], 201);
} catch (PDOException $e) {
error_log($e->getMessage());
logWebhook('call-tracking', $request_body, 500);
sendJsonResponse(['error' => 'Database error'], 500);
}

48
api/config.php Normal file
View File

@ -0,0 +1,48 @@
<?php
require_once __DIR__ . '/../db/config.php';
function validateApiKey() {
$headers = getallheaders();
if (!isset($headers['Authorization'])) {
return false;
}
$auth_header = $headers['Authorization'];
if (preg_match('/Bearer\s(\S+)/', $auth_header, $matches)) {
$api_key = $matches[1];
$stmt = db()->prepare("SELECT * FROM api_keys WHERE api_key = ? AND is_active = TRUE");
$stmt->execute([$api_key]);
$key_data = $stmt->fetch();
if ($key_data) {
return true;
}
}
return false;
}
function logWebhook($endpoint, $body, $status_code) {
$headers = getallheaders();
$ip_address = $_SERVER['REMOTE_ADDR'] ?? null;
try {
$stmt = db()->prepare("INSERT INTO webhook_logs (endpoint, request_headers, request_body, response_status, ip_address) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([
$endpoint,
json_encode($headers),
$body,
$status_code,
$ip_address
]);
} catch (PDOException $e) {
error_log("Failed to log webhook request: " . $e->getMessage());
}
}
function sendJsonResponse($data, $statusCode = 200) {
header('Content-Type: application/json');
http_response_code($statusCode);
echo json_encode($data);
exit;
}

84
api/keys.php Normal file
View File

@ -0,0 +1,84 @@
<?php
require_once __DIR__ . '/config.php';
/**
* Generate a new API key, hash it, store it, and return the plain text key.
*
* @param string $keyName User-defined name for the key.
* @param string $integrationType The type of integration (e.g., 'n8n', 'callrail').
* @return array Result array with success status, plain text API key, and key name.
*/
function generateApiKey($keyName, $integrationType) {
if (empty($keyName) || empty($integrationType)) {
return ["success" => false, "message" => "Key name and integration type are required."];
}
$plainKey = bin2hex(random_bytes(16));
$hashedKey = password_hash($plainKey, PASSWORD_DEFAULT);
$pdo = db();
$stmt = $pdo->prepare(
"INSERT INTO api_keys (key_name, api_key_hash, integration_type, is_active, rate_limit_per_minute, created_at)
VALUES (:key_name, :api_key_hash, :integration_type, true, 60, NOW())"
);
$stmt->bindParam(':key_name', $keyName);
$stmt->bindParam(':api_key_hash', $hashedKey);
$stmt->bindParam(':integration_type', $integrationType);
if ($stmt->execute()) {
return ["success" => true, "api_key" => $plainKey, "key_name" => $keyName, "id" => $pdo->lastInsertId()];
} else {
return ["success" => false, "message" => "Failed to generate API key."];
}
}
/**
* List all API keys from the database (excluding the hash).
*
* @return array An array of API key records.
*/
function listApiKeys() {
$pdo = db();
$stmt = $pdo->query(
"SELECT id, key_name, integration_type, is_active, last_used_at, created_at, expires_at
FROM api_keys ORDER BY created_at DESC"
);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
/**
* Deactivate an API key.
*
* @param int $keyId The ID of the key to deactivate.
* @return array Result array with success status and a message.
*/
function deactivateApiKey($keyId) {
$pdo = db();
$stmt = $pdo->prepare("UPDATE api_keys SET is_active = false WHERE id = :id");
$stmt->bindParam(':id', $keyId, PDO::PARAM_INT);
if ($stmt->execute()) {
return ["success" => true, "message" => "API key deactivated."];
} else {
return ["success" => false, "message" => "Failed to deactivate API key."];
}
}
/**
* Delete an API key.
*
* @param int $keyId The ID of the key to delete.
* @return array Result array with success status and a message.
*/
function deleteApiKey($keyId) {
$pdo = db();
$stmt = $pdo->prepare("DELETE FROM api_keys WHERE id = :id");
$stmt->bindParam(':id', $keyId, PDO::PARAM_INT);
if ($stmt->execute()) {
return ["success" => true, "message" => "API key deleted."];
} else {
return ["success" => false, "message" => "Failed to delete API key."];
}
}

55
api/reviews.php Normal file
View File

@ -0,0 +1,55 @@
<?php
require_once __DIR__ . '/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJsonResponse(['error' => 'Invalid request method'], 405);
exit;
}
if (!validateApiKey()) {
logWebhook('reviews', file_get_contents('php://input'), 401);
sendJsonResponse(['error' => 'Unauthorized'], 401);
exit;
}
$request_body = file_get_contents('php://input');
$data = json_decode($request_body, true);
if (json_last_error() !== JSON_ERROR_NONE) {
logWebhook('reviews', $request_body, 400);
sendJsonResponse(['error' => 'Invalid JSON'], 400);
exit;
}
$errors = [];
if (empty($data['platform_source'])) {
$errors[] = 'platform_source is required';
}
if (empty($data['star_rating'])) {
$errors[] = 'star_rating is required';
}
if (!empty($errors)) {
logWebhook('reviews', $request_body, 422);
sendJsonResponse(['errors' => $errors], 422);
exit;
}
try {
$stmt = db()->prepare("INSERT INTO reviews (platform_source, star_rating, review_text, reviewer_name, review_date) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([
$data['platform_source'],
$data['star_rating'],
$data['review_text'] ?? null,
$data['reviewer_name'] ?? null,
$data['review_date'] ?? null
]);
$new_id = db()->lastInsertId();
logWebhook('reviews', $request_body, 201);
sendJsonResponse(['success' => true, 'id' => $new_id, 'message' => 'Review created'], 201);
} catch (PDOException $e) {
error_log($e->getMessage());
logWebhook('reviews', $request_body, 500);
sendJsonResponse(['error' => 'Database error'], 500);
}

47
assets/css/custom.css Normal file
View File

@ -0,0 +1,47 @@
/* HVAC Command Center Custom Styles */
body {
background-color: #f8f9fa;
font-family: 'Roboto', sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Montserrat', sans-serif;
}
.header {
background: linear-gradient(90deg, #0d6efd, #17a2b8);
padding: 1.5rem 2rem;
}
.card {
border: none;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.card-header {
background-color: #fff;
border-bottom: 1px solid #e9ecef;
}
.table-hover tbody tr:hover {
background-color: #f1f3f5;
}
.badge.bg-emergency {
background-color: #dc3545 !important;
}
.badge.bg-urgent {
background-color: #ffc107 !important;
color: #000 !important;
}
.badge.bg-routine {
background-color: #6c757d !important;
}

1
assets/js/main.js Normal file
View File

@ -0,0 +1 @@
// Future javascript for the HVAC Command Center

189
bookings.php Normal file
View File

@ -0,0 +1,189 @@
<?php
require_once 'db/config.php';
try {
$pdo = db();
// Filtering logic
$where_clauses = [];
$params = [];
if (!empty($_GET['service_type'])) {
$where_clauses[] = 'service_type = :service_type';
$params[':service_type'] = $_GET['service_type'];
}
if (!empty($_GET['urgency_level'])) {
$where_clauses[] = 'urgency_level = :urgency_level';
$params[':urgency_level'] = $_GET['urgency_level'];
}
if (!empty($_GET['status'])) {
$where_clauses[] = 'status = :status';
$params[':status'] = $_GET['status'];
}
$sql = 'SELECT * FROM bookings';
if (!empty($where_clauses)) {
$sql .= ' WHERE ' . implode(' AND ', $where_clauses);
}
$sql .= ' ORDER BY appointment_date DESC';
$stmt_bookings = $pdo->prepare($sql);
$stmt_bookings->execute($params);
$bookings = $stmt_bookings->fetchAll();
// Fetch distinct values for filters
$stmt_service_types = $pdo->query('SELECT DISTINCT service_type FROM bookings');
$service_types = $stmt_service_types->fetchAll(PDO::FETCH_COLUMN);
$stmt_urgency_levels = $pdo->query('SELECT DISTINCT urgency_level FROM bookings');
$urgency_levels = $stmt_urgency_levels->fetchAll(PDO::FETCH_COLUMN);
$stmt_statuses = $pdo->query('SELECT DISTINCT status FROM bookings');
$statuses = $stmt_statuses->fetchAll(PDO::FETCH_COLUMN);
} catch (PDOException $e) {
$error = "Database error: " . $e->getMessage();
}
$project_name = "HVAC Command Center";
$page_title = "Bookings";
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($page_title) ?> | <?= htmlspecialchars($project_name) ?></title>
<!-- Styles -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<header class="header text-white">
<div class="container-fluid">
<h1 class="display-6 m-0"><?= htmlspecialchars($project_name) ?></h1>
</div>
</header>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="customers.php">Customers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="bookings.php">Bookings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="ai-call-logs.php">AI Call Logs</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container-fluid mt-4">
<?php if (isset($error)): ?>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle-fill"></i> <?= htmlspecialchars($error) ?>
</div>
<?php else: ?>
<div class="card shadow-sm">
<div class="card-header">
<h5 class="m-0"><i class="bi bi-calendar-check-fill me-2"></i>All Bookings</h5>
</div>
<div class="card-body">
<form method="GET" action="" class="row g-3 mb-4">
<div class="col-md-3">
<select name="service_type" class="form-select">
<option value="">All Services</option>
<?php foreach ($service_types as $type): ?>
<option value="<?= htmlspecialchars($type) ?>" <?= (isset($_GET['service_type']) && $_GET['service_type'] == $type) ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst($type)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<select name="urgency_level" class="form-select">
<option value="">All Urgencies</option>
<?php foreach ($urgency_levels as $level): ?>
<option value="<?= htmlspecialchars($level) ?>" <?= (isset($_GET['urgency_level']) && $_GET['urgency_level'] == $level) ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst($level)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<select name="status" class="form-select">
<option value="">All Statuses</option>
<?php foreach ($statuses as $status): ?>
<option value="<?= htmlspecialchars($status) ?>" <?= (isset($_GET['status']) && $_GET['status'] == $status) ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst($status)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary">Filter</button>
<a href="bookings.php" class="btn btn-secondary">Reset</a>
</div>
</form>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Customer</th>
<th>Service</th>
<th>Urgency</th>
<th>Status</th>
<th class="text-end">Est. Revenue</th>
<th class="text-end">Actual Revenue</th>
</tr>
</thead>
<tbody>
<?php foreach ($bookings as $booking): ?>
<tr>
<td><?= htmlspecialchars(date("M d, Y", strtotime($booking['appointment_date']))) ?></td>
<td><?= htmlspecialchars($booking['customer_name']) ?></td>
<td><?= htmlspecialchars($booking['service_type']) ?></td>
<td><span class="badge bg-<?= strtolower(htmlspecialchars($booking['urgency_level'])) == 'emergency' ? 'danger' : (strtolower(htmlspecialchars($booking['urgency_level'])) == 'urgent' ? 'warning' : 'secondary') ?>"><?= htmlspecialchars(ucfirst($booking['urgency_level'])) ?></span></td>
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars(ucfirst($booking['status'])) ?></span></td>
<td class="text-end fw-bold">$<?= number_format($booking['estimated_revenue'], 2) ?></td>
<td class="text-end fw-bold">$<?= number_format($booking['actual_revenue'], 2) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($bookings)): ?>
<tr>
<td colspan="7" class="text-center text-muted">No bookings found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</main>
<footer class="container-fluid text-center text-muted py-3 mt-4">
<small>Powered by Flatlogic</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body>
</html>

121
customers.php Normal file
View File

@ -0,0 +1,121 @@
<?php
require_once 'db/config.php';
try {
$pdo = db();
// Fetch all customers
$stmt_customers = $pdo->query('SELECT * FROM customers ORDER BY created_at DESC');
$customers = $stmt_customers->fetchAll();
} catch (PDOException $e) {
// For production, you would log this error and show a user-friendly message.
$error = "Database error: " . $e->getMessage();
}
$project_name = "HVAC Command Center";
$page_title = "Customers";
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($page_title) ?> | <?= htmlspecialchars($project_name) ?></title>
<!-- Styles -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<header class="header text-white">
<div class="container-fluid">
<h1 class="display-6 m-0"><?= htmlspecialchars($project_name) ?></h1>
</div>
</header>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="customers.php">Customers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="bookings.php">Bookings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="ai-call-logs.php">AI Call Logs</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container-fluid mt-4">
<?php if (isset($error)): ?>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle-fill"></i> <?= htmlspecialchars($error) ?>
</div>
<?php else: ?>
<div class="card shadow-sm">
<div class="card-header">
<h5 class="m-0"><i class="bi bi-people-fill me-2"></i>All Customers</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Phone</th>
<th>Email</th>
<th>Address</th>
<th>Member Since</th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $customer): ?>
<tr>
<td><?= htmlspecialchars($customer['name']) ?></td>
<td><?= htmlspecialchars($customer['phone']) ?></td>
<td><?= htmlspecialchars($customer['email']) ?></td>
<td><?= htmlspecialchars($customer['address']) ?></td>
<td><?= htmlspecialchars(date("M d, Y", strtotime($customer['created_at']))) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($customers)): ?>
<tr>
<td colspan="5" class="text-center text-muted">No customers found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</main>
<footer class="container-fluid text-center text-muted py-3 mt-4">
<small>Powered by Flatlogic</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body>
</html>

46
db/schema.sql Normal file
View File

@ -0,0 +1,46 @@
-- Adapted from user requirements for MySQL/MariaDB
CREATE TABLE IF NOT EXISTS `customers` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`first_name` VARCHAR(100),
`last_name` VARCHAR(100),
`email` VARCHAR(255) UNIQUE,
`phone` VARCHAR(20),
`phone_normalized` VARCHAR(15),
`service_address` TEXT,
`city` VARCHAR(100),
`state` VARCHAR(50),
`zip_code` VARCHAR(20),
`lifetime_value` DECIMAL(10, 2) DEFAULT 0,
`total_bookings` INT DEFAULT 0,
`total_quotes` INT DEFAULT 0,
`acquisition_source` ENUM('google_ads', 'google_lsa', 'organic', 'referral', 'facebook', 'yelp', 'direct', 'other'),
`acquisition_campaign` VARCHAR(255),
`first_contact_date` DATETIME DEFAULT CURRENT_TIMESTAMP,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `bookings` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`record_type` ENUM('appointment', 'quote_request') NOT NULL,
`customer_name` VARCHAR(255),
`customer_phone` VARCHAR(20),
`customer_email` VARCHAR(255),
`service_address` TEXT,
`service_category` ENUM('repair', 'maintenance', 'installation', 'inspection', 'emergency'),
`service_type` VARCHAR(100),
`system_type` ENUM('central_air', 'heat_pump', 'furnace', 'mini_split', 'boiler', 'other'),
`urgency_level` ENUM('routine', 'urgent', 'emergency'),
`issue_description` TEXT,
`appointment_date` DATE,
`appointment_time` VARCHAR(20),
`status` ENUM('new', 'confirmed', 'dispatched', 'in_progress', 'completed', 'cancelled', 'no_show') NOT NULL DEFAULT 'new',
`estimated_revenue` DECIMAL(10, 2),
`actual_revenue` DECIMAL(10, 2),
`booked_by` ENUM('ai_agent', 'human_agent', 'online', 'walk_in'),
`customer_id` INT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`customer_id`) REFERENCES `customers`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

54
db/schema_api.sql Normal file
View File

@ -0,0 +1,54 @@
-- API Keys for authentication
CREATE TABLE IF NOT EXISTS `api_keys` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`api_key` VARCHAR(255) NOT NULL UNIQUE,
`is_active` BOOLEAN NOT NULL DEFAULT TRUE,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Webhook request logs
CREATE TABLE IF NOT EXISTS `webhook_logs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`endpoint` VARCHAR(255) NOT NULL,
`request_headers` TEXT,
`request_body` LONGTEXT,
`response_status` INT,
`ip_address` VARCHAR(45),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- AI Call Logs
CREATE TABLE IF NOT EXISTS `ai_call_logs` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`call_id` VARCHAR(255) NOT NULL UNIQUE,
`conversation_id` VARCHAR(255) DEFAULT NULL,
`call_start_time` DATETIME NOT NULL,
`call_end_time` DATETIME DEFAULT NULL,
`call_duration_seconds` INT DEFAULT NULL,
`call_intent` VARCHAR(100) DEFAULT NULL,
`call_outcome` VARCHAR(100) DEFAULT NULL,
`ai_summary` TEXT,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Call Tracking Logs
CREATE TABLE IF NOT EXISTS `call_tracking` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`external_call_id` VARCHAR(255) NOT NULL,
`tracking_platform` VARCHAR(100) NOT NULL,
`call_start_time` DATETIME NOT NULL,
`call_status` VARCHAR(50) NOT NULL,
`traffic_source` VARCHAR(100),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Reviews
CREATE TABLE IF NOT EXISTS `reviews` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`platform_source` VARCHAR(100) NOT NULL,
`star_rating` DECIMAL(3, 2) NOT NULL,
`review_text` TEXT,
`reviewer_name` VARCHAR(255),
`review_date` DATETIME,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

17
db/seed.sql Normal file
View File

@ -0,0 +1,17 @@
INSERT INTO `customers` (`first_name`, `last_name`, `email`, `phone`, `service_address`, `city`, `state`, `zip_code`, `acquisition_source`) VALUES
('John', 'Smith', 'john.smith@example.com', '555-1234', '123 Main St', 'Anytown', 'CA', '12345', 'organic'),
('Jane', 'Doe', 'jane.doe@example.com', '555-5678', '456 Oak Ave', 'Someville', 'NY', '54321', 'referral'),
('Peter', 'Jones', 'peter.jones@example.com', '555-9876', '789 Pine Ln', 'Metropolis', 'FL', '67890', 'google_ads');
INSERT INTO `bookings` (`record_type`, `customer_name`, `customer_phone`, `service_address`, `service_category`, `service_type`, `system_type`, `urgency_level`, `appointment_date`, `status`, `estimated_revenue`, `customer_id`) VALUES
('appointment', 'Bob Johnson', '555-1111', '101 Maple Dr', 'repair', 'AC Unit Repair', 'central_air', 'urgent', '2026-01-05', 'new', 350.00, NULL),
('appointment', 'Samantha Williams', '555-2222', '212 Birch Rd', 'maintenance', 'Furnace Tune-up', 'furnace', 'routine', '2026-01-06', 'confirmed', 150.00, NULL),
('quote_request', 'Michael Brown', '555-3333', '333 Cedar Ct', 'installation', 'New Heat Pump', 'heat_pump', 'routine', '2026-01-07', 'new', 8500.00, NULL),
('appointment', 'Jessica Davis', '555-4444', '444 Spruce Way', 'emergency', 'Boiler Emergency', 'boiler', 'emergency', '2026-01-08', 'dispatched', 700.00, NULL),
('appointment', 'David Miller', '555-5555', '555 Willow Bend', 'inspection', 'Pre-purchase Inspection', 'central_air', 'routine', '2026-01-09', 'in_progress', 200.00, NULL),
('appointment', 'Emily Wilson', '555-6666', '666 Aspen Pl', 'repair', 'Mini-split not cooling', 'mini_split', 'urgent', '2026-01-10', 'completed', 450.00, NULL);
-- Link some bookings to the customers we created
UPDATE `bookings` SET `customer_id` = 1 WHERE `id` IN (1, 2);
UPDATE `bookings` SET `customer_id` = 2 WHERE `id` = 3;
UPDATE `bookings` SET `customer_id` = 3 WHERE `id` = 4;

371
index.php
View File

@ -1,150 +1,237 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once 'db/config.php';
try {
$pdo = db();
// Fetch total customers
$stmt_customers = $pdo->query('SELECT COUNT(*) as total FROM customers');
$total_customers = $stmt_customers->fetch()['total'];
// Fetch total bookings
$stmt_bookings = $pdo->query('SELECT COUNT(*) as total FROM bookings');
$total_bookings = $stmt_bookings->fetch()['total'];
// Fetch total revenue
$stmt_revenue = $pdo->query("SELECT SUM(actual_revenue) as total FROM bookings WHERE status = 'completed'");
$total_revenue = $stmt_revenue->fetch()['total'] ?? 0;
// Fetch recent bookings
$stmt_recent_bookings = $pdo->query('SELECT * FROM bookings ORDER BY created_at DESC LIMIT 5');
$recent_bookings = $stmt_recent_bookings->fetchAll();
// --- API Key Management ---
// Handle API key generation
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['generate_api_key'])) {
$new_key = 'hvac_' . bin2hex(random_bytes(16));
$stmt_insert_key = $pdo->prepare("INSERT INTO api_keys (api_key) VALUES (?)");
$stmt_insert_key->execute([$new_key]);
// Redirect to avoid form resubmission
header("Location: " . $_SERVER['PHP_SELF']);
exit;
}
// Fetch all API keys
$stmt_api_keys = $pdo->query('SELECT * FROM api_keys ORDER BY created_at DESC');
$api_keys = $stmt_api_keys->fetchAll();
} catch (PDOException $e) {
// For production, you would log this error and show a user-friendly message.
$error = "Database error: " . $e->getMessage();
}
$project_name = "HVAC Command Center";
$project_description = "Central dashboard for managing your HVAC business operations.";
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($project_name) ?></title>
<meta name="description" content="<?= htmlspecialchars($project_description) ?>">
<!-- Open Graph / Twitter -->
<meta property="og:title" content="<?= htmlspecialchars($project_name) ?>">
<meta property="og:description" content="<?= htmlspecialchars($project_description) ?>">
<meta property="og:image" content="<?= htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? '') ?>">
<meta name="twitter:card" content="summary_large_image">
<!-- Styles -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
<header class="header text-white">
<div class="container-fluid">
<h1 class="display-6 m-0"><?= htmlspecialchars($project_name) ?></h1>
</div>
</header>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="index.php">Dashboard</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="customers.php">Customers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="bookings.php">Bookings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="ai-call-logs.php">AI Call Logs</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container-fluid mt-4">
<?php if (isset($error)): ?>
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle-fill"></i> <?= htmlspecialchars($error) ?>
</div>
<?php else: ?>
<!-- Stat Cards -->
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body d-flex align-items-center">
<i class="bi bi-people-fill display-4 text-primary me-3"></i>
<div>
<h5 class="card-title">Total Customers</h5>
<p class="card-text fs-2 fw-bold"><?= htmlspecialchars($total_customers) ?></p>
<a href="customers.php" class="btn btn-outline-primary btn-sm mt-2">View All</a>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body d-flex align-items-center">
<i class="bi bi-calendar-check-fill display-4 text-success me-3"></i>
<div>
<h5 class="card-title">Total Bookings</h5>
<p class="card-text fs-2 fw-bold"><?= htmlspecialchars($total_bookings) ?></p>
<a href="bookings.php" class="btn btn-outline-success btn-sm mt-2">View All</a>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 shadow-sm">
<div class="card-body d-flex align-items-center">
<i class="bi bi-cash-stack display-4 text-info me-3"></i>
<div>
<h5 class="card-title">Completed Revenue</h5>
<p class="card-text fs-2 fw-bold">$<?= number_format($total_revenue, 2) ?></p>
</div>
</div>
</div>
</div>
</div>
<!-- API Key Management -->
<div class="card mt-4 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="m-0"><i class="bi bi-key-fill me-2"></i>API Keys</h5>
<form method="POST" action="">
<button type="submit" name="generate_api_key" class="btn btn-primary btn-sm">Generate New Key</button>
</form>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>API Key</th>
<th>Status</th>
<th>Created On</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($api_keys as $key): ?>
<tr>
<td><input type="text" readonly class="form-control-plaintext" value="<?= htmlspecialchars($key['api_key']) ?>"></td>
<td><span class="badge bg-<?= $key['is_active'] ? 'success' : 'danger' ?>"><?= $key['is_active'] ? 'Active' : 'Inactive' ?></span></td>
<td><?= htmlspecialchars(date("M d, Y", strtotime($key['created_at']))) ?></td>
<td><!-- Action buttons here --></td>
</tr>
<?php endforeach; ?>
<?php if (empty($api_keys)): ?>
<tr>
<td colspan="4" class="text-center text-muted">No API keys found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Recent Bookings Table -->
<div class="card mt-4 shadow-sm">
<div class="card-header">
<h5 class="m-0"><i class="bi bi-clock-history me-2"></i>Recent Bookings</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Customer</th>
<th>Service</th>
<th>Urgency</th>
<th>Status</th>
<th class="text-end">Est. Revenue</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent_bookings as $booking): ?>
<tr>
<td><?= htmlspecialchars(date("M d, Y", strtotime($booking['appointment_date']))) ?></td>
<td><?= htmlspecialchars($booking['customer_name']) ?></td>
<td><?= htmlspecialchars($booking['service_type']) ?></td>
<td><span class="badge bg-<?= strtolower(htmlspecialchars($booking['urgency_level'])) == 'emergency' ? 'danger' : (strtolower(htmlspecialchars($booking['urgency_level'])) == 'urgent' ? 'warning' : 'secondary') ?>"><?= htmlspecialchars(ucfirst($booking['urgency_level'])) ?></span></td>
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars(ucfirst($booking['status'])) ?></span></td>
<td class="text-end fw-bold">$<?= number_format($booking['estimated_revenue'], 2) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($recent_bookings)): ?>
<tr>
<td colspan="6" class="text-center text-muted">No recent bookings found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
</main>
<footer class="container-fluid text-center text-muted py-3 mt-4">
<small>Powered by Flatlogic</small>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body>
</html>
</html>