diff --git a/.htaccess b/.htaccess index e2bbc23..8facbde 100644 --- a/.htaccess +++ b/.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] \ No newline at end of file diff --git a/ai-call-logs.php b/ai-call-logs.php new file mode 100644 index 0000000..7e22b08 --- /dev/null +++ b/ai-call-logs.php @@ -0,0 +1,263 @@ +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"; + +?> + + + + + + + <?= htmlspecialchars($page_title) ?> | <?= htmlspecialchars($project_name) ?> + + + + + + + + + + + + + + + + +
+
+

+
+
+ + + +
+ + +
+ +
+ + + +
+
+
+
+ +
+
Total Calls
+

+
+
+
+
+
+
+
+ +
+
Avg. Call Duration
+

s

+
+
+
+
+
+ + +
+
+
+
+
Call Intent Distribution
+
+
+ +
+
+
+
+
+
+
Call Outcome Distribution
+
+
+ +
+
+
+
+ + + +
+
+
All AI Call Logs
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Call IDStart TimeEnd TimeIntentOutcomeSummary
No call logs found.
+
+
+
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/api-keys.php b/api-keys.php new file mode 100644 index 0000000..7905496 --- /dev/null +++ b/api-keys.php @@ -0,0 +1,200 @@ + + + + + + + API Key Management - <?= htmlspecialchars($project_name) ?> + + + + + +
+
+

+
+
+ + + +
+

API Key Management

+ + +
+ + + +
+ + + +
+
Your New API Key
+

Please copy it now. It will not be shown again.

+
+ + +
+
+

Use this key in the Authorization header as a Bearer token: Bearer

+
+ + +
+
+
Create New API Key
+
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ +
+
+
Existing API Keys
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameIntegrationStatusCreatedLast UsedActions
No API keys found.
+
+ + + + + +
+
+
+
+
+
+ + + + + + + diff --git a/api/ai-call-logs.php b/api/ai-call-logs.php new file mode 100644 index 0000000..24f9b48 --- /dev/null +++ b/api/ai-call-logs.php @@ -0,0 +1,64 @@ + '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); +} \ No newline at end of file diff --git a/api/bookings.php b/api/bookings.php new file mode 100644 index 0000000..e0ee1f4 --- /dev/null +++ b/api/bookings.php @@ -0,0 +1,68 @@ + '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); +} \ No newline at end of file diff --git a/api/call-tracking.php b/api/call-tracking.php new file mode 100644 index 0000000..3b1eff4 --- /dev/null +++ b/api/call-tracking.php @@ -0,0 +1,64 @@ + '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); +} \ No newline at end of file diff --git a/api/config.php b/api/config.php new file mode 100644 index 0000000..cf0a1c1 --- /dev/null +++ b/api/config.php @@ -0,0 +1,48 @@ +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; +} diff --git a/api/keys.php b/api/keys.php new file mode 100644 index 0000000..a515748 --- /dev/null +++ b/api/keys.php @@ -0,0 +1,84 @@ + 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."]; + } +} diff --git a/api/reviews.php b/api/reviews.php new file mode 100644 index 0000000..6947d5d --- /dev/null +++ b/api/reviews.php @@ -0,0 +1,55 @@ + '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); +} \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..0ff610b --- /dev/null +++ b/assets/css/custom.css @@ -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; +} diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..4bfa124 --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1 @@ +// Future javascript for the HVAC Command Center \ No newline at end of file diff --git a/bookings.php b/bookings.php new file mode 100644 index 0000000..6078fbf --- /dev/null +++ b/bookings.php @@ -0,0 +1,189 @@ +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"; + +?> + + + + + + + <?= htmlspecialchars($page_title) ?> | <?= htmlspecialchars($project_name) ?> + + + + + + + + + + + + + +
+
+

+
+
+ + + +
+ + +
+ +
+ +
+
+
All Bookings
+
+
+
+
+ +
+
+ +
+
+ +
+
+ + Reset +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateCustomerServiceUrgencyStatusEst. RevenueActual Revenue
$$
No bookings found.
+
+
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/customers.php b/customers.php new file mode 100644 index 0000000..15dbabf --- /dev/null +++ b/customers.php @@ -0,0 +1,121 @@ +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"; + +?> + + + + + + + <?= htmlspecialchars($page_title) ?> | <?= htmlspecialchars($project_name) ?> + + + + + + + + + + + + + +
+
+

+
+
+ + + +
+ + +
+ +
+ +
+
+
All Customers
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NamePhoneEmailAddressMember Since
No customers found.
+
+
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..3859b17 --- /dev/null +++ b/db/schema.sql @@ -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; diff --git a/db/schema_api.sql b/db/schema_api.sql new file mode 100644 index 0000000..06905f7 --- /dev/null +++ b/db/schema_api.sql @@ -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; diff --git a/db/seed.sql b/db/seed.sql new file mode 100644 index 0000000..33cba0a --- /dev/null +++ b/db/seed.sql @@ -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; diff --git a/index.php b/index.php index 7205f3d..3e76913 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,237 @@ 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'); ?> - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + + <?= htmlspecialchars($project_name) ?> + + + + + + + + + + + + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
-
- + +
+
+

+
+
+ + + +
+ + +
+ +
+ + +
+
+
+
+ +
+
Total Customers
+

+ View All +
+
+
+
+
+
+
+ +
+
Total Bookings
+

+ View All +
+
+
+
+
+
+
+ +
+
Completed Revenue
+

$

+
+
+
+
+
+ + +
+
+
API Keys
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
API KeyStatusCreated OnActions
No API keys found.
+
+
+
+ + +
+
+
Recent Bookings
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateCustomerServiceUrgencyStatusEst. Revenue
$
No recent bookings found.
+
+
+
+ +
+ + + + + - + \ No newline at end of file