N8N API Testing
This commit is contained in:
parent
3def426cf9
commit
26d8efd021
22
.htaccess
22
.htaccess
@ -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
263
ai-call-logs.php
Normal 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
200
api-keys.php
Normal 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
64
api/ai-call-logs.php
Normal 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
68
api/bookings.php
Normal 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
64
api/call-tracking.php
Normal 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
48
api/config.php
Normal 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
84
api/keys.php
Normal 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
55
api/reviews.php
Normal 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
47
assets/css/custom.css
Normal 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
1
assets/js/main.js
Normal file
@ -0,0 +1 @@
|
||||
// Future javascript for the HVAC Command Center
|
||||
189
bookings.php
Normal file
189
bookings.php
Normal 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
121
customers.php
Normal 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
46
db/schema.sql
Normal 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
54
db/schema_api.sql
Normal 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
17
db/seed.sql
Normal 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;
|
||||
357
index.php
357
index.php
@ -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; ?>
|
||||
<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=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>
|
||||
<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>
|
||||
|
||||
<header class="header text-white">
|
||||
<div class="container-fluid">
|
||||
<h1 class="display-6 m-0"><?= htmlspecialchars($project_name) ?></h1>
|
||||
</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>
|
||||
</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>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
|
||||
<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>
|
||||
Loading…
x
Reference in New Issue
Block a user