diff --git a/admin/customer_delete.php b/admin/customer_delete.php
index 3b32155..0519e84 100644
--- a/admin/customer_delete.php
+++ b/admin/customer_delete.php
@@ -8,10 +8,7 @@ if (!isset($_GET['id'])) {
$pdo = db();
-// We should also delete related orders to maintain data integrity
-$stmt = $pdo->prepare('DELETE FROM orders WHERE customer_id = ?');
-$stmt->execute([$_GET['id']]);
-
+// The ON DELETE CASCADE constraint on the orders table will automatically delete related orders.
$stmt = $pdo->prepare('DELETE FROM customers WHERE id = ?');
$stmt->execute([$_GET['id']]);
diff --git a/admin/customer_edit.php b/admin/customer_edit.php
index 47d77e3..8c29db1 100644
--- a/admin/customer_edit.php
+++ b/admin/customer_edit.php
@@ -7,7 +7,7 @@ $customer = [
'id' => '',
'name' => '',
'email' => '',
- 'address' => ''
+ 'service_address' => ''
];
$page_title = 'Edit Customer';
@@ -30,12 +30,12 @@ if (!$customer) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
- $address = $_POST['address'] ?? '';
+ $service_address = $_POST['service_address'] ?? '';
$customer_id = $_POST['id'] ?? null;
if ($customer_id) {
- $stmt = $pdo->prepare('UPDATE customers SET name = ?, email = ?, address = ? WHERE id = ?');
- $stmt->execute([$name, $email, $address, $customer_id]);
+ $stmt = $pdo->prepare('UPDATE customers SET name = ?, email = ?, service_address = ? WHERE id = ?');
+ $stmt->execute([$name, $email, $service_address, $customer_id]);
header('Location: customers.php');
exit;
}
@@ -55,8 +55,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- Address
-
+ Service Address
+
Save Customer
Cancel
diff --git a/admin/customers.php b/admin/customers.php
index 4c96ee9..5a0f5d6 100644
--- a/admin/customers.php
+++ b/admin/customers.php
@@ -17,7 +17,7 @@ $customers = $stmt->fetchAll();
ID
Name
Email
- Address
+ Service Address
Joined On
Actions
@@ -33,7 +33,7 @@ $customers = $stmt->fetchAll();
-
+
Edit
diff --git a/admin/header.php b/admin/header.php
index 684aa5b..fa187f6 100644
--- a/admin/header.php
+++ b/admin/header.php
@@ -43,6 +43,7 @@
Customers
Orders
Plans
+ Pages
diff --git a/admin/orders.php b/admin/orders.php
index d95491e..7d06edb 100644
--- a/admin/orders.php
+++ b/admin/orders.php
@@ -3,8 +3,23 @@ include __DIR__ . '/../db/config.php';
include 'header.php';
$pdo = db();
-$stmt = $pdo->query('SELECT o.id, c.name as customer_name, p.name as plan_name, o.status, o.created_at FROM orders o JOIN customers c ON o.customer_id = c.id JOIN plans p ON o.plan_id = p.id ORDER BY o.created_at DESC');
+$stmt = $pdo->query('SELECT o.id, c.name as customer_name, p.name as plan_name, o.order_status, o.amount, o.created_at FROM orders o JOIN customers c ON o.customer_id = c.id JOIN plans p ON o.plan_id = p.id ORDER BY o.created_at DESC');
$orders = $stmt->fetchAll();
+
+function get_status_badge($status) {
+ switch (strtolower($status)) {
+ case 'completed':
+ case 'active':
+ return 'bg-success';
+ case 'pending':
+ return 'bg-warning';
+ case 'failed':
+ case 'cancelled':
+ return 'bg-danger';
+ default:
+ return 'bg-secondary';
+ }
+}
?>
@@ -17,6 +32,7 @@ $orders = $stmt->fetchAll();
Order ID
Customer
Plan
+
Amount
Status
Order Date
@@ -24,7 +40,7 @@ $orders = $stmt->fetchAll();
- No orders found.
+ No orders found.
@@ -32,8 +48,9 @@ $orders = $stmt->fetchAll();
-
-
+ $
+
+
diff --git a/admin/page_delete.php b/admin/page_delete.php
new file mode 100644
index 0000000..f38b611
--- /dev/null
+++ b/admin/page_delete.php
@@ -0,0 +1,12 @@
+prepare('DELETE FROM pages WHERE id = ?');
+ $stmt->execute([$id]);
+}
+
+header('Location: pages.php');
+exit;
diff --git a/admin/page_edit.php b/admin/page_edit.php
new file mode 100644
index 0000000..0eefdd5
--- /dev/null
+++ b/admin/page_edit.php
@@ -0,0 +1,55 @@
+prepare('SELECT * FROM pages WHERE id = ?');
+ $stmt->execute([$id]);
+ $page = $stmt->fetch();
+ $title = 'Edit Page';
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $slug = $_POST['slug'];
+ $page_title = $_POST['title'];
+ $content = $_POST['content'];
+
+ if ($id) {
+ $stmt = db()->prepare('UPDATE pages SET slug = ?, title = ?, content = ? WHERE id = ?');
+ $stmt->execute([$slug, $page_title, $content, $id]);
+ } else {
+ $stmt = db()->prepare('INSERT INTO pages (slug, title, content) VALUES (?, ?, ?)');
+ $stmt->execute([$slug, $page_title, $content]);
+ }
+
+ header('Location: pages.php');
+ exit;
+}
+
+include 'header.php';
+?>
+
+
+
+
+
+
diff --git a/admin/pages.php b/admin/pages.php
new file mode 100644
index 0000000..641ba9a
--- /dev/null
+++ b/admin/pages.php
@@ -0,0 +1,37 @@
+query('SELECT * FROM pages ORDER BY title ASC')->fetchAll();
+
+include 'header.php';
+?>
+
+
+
+
+
+
+ Title
+ Slug
+ Actions
+
+
+
+
+
+
+
+
+ Edit
+ Delete
+ View
+
+
+
+
+
+
+
diff --git a/api/ai_support.php b/api/ai_support.php
new file mode 100644
index 0000000..e53bc31
--- /dev/null
+++ b/api/ai_support.php
@@ -0,0 +1,75 @@
+query("SELECT name, speed_mbps, price_monthly, contract_length_months FROM plans ORDER BY price_monthly ASC");
+ $plans = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ if ($plans) {
+ $knowledge_base .= "Here is a list of our current internet plans:\n\n";
+ foreach ($plans as $plan) {
+ $knowledge_base .= "- Plan Name: " . $plan['name'] . "\n";
+ $knowledge_base .= " - Speed: " . $plan['speed_mbps'] . " Mbps\n";
+ $knowledge_base .= " - Price: $" . $plan['price_monthly'] . " per month\n";
+ $knowledge_base .= " - Contract: " . $plan['contract_length_months'] . " months\n\n";
+ }
+ $knowledge_base .= "You are a helpful and friendly customer support assistant for a telecommunications company. Your goal is to answer customer questions based ONLY on the information provided in this knowledge base. Do not invent or assume any details. If the answer is not in the knowledge base, say 'I'm sorry, I don't have that information, but I can connect you with a human agent.' Keep your answers concise and to the point.";
+ }
+} catch (PDOException $e) {
+ // In a real app, you'd log this error.
+ // For now, we'll proceed with an empty knowledge base on failure.
+}
+
+// 2. Get the user's question
+$data = json_decode(file_get_contents('php://input'), true);
+$user_question = $data['question'] ?? '';
+
+if (empty($user_question)) {
+ echo json_encode(['error' => 'No question provided.']);
+ exit;
+}
+
+// 3. Prepare the data for the Gemini API
+// IMPORTANT: You must replace getenv('GEMINI_API_KEY') with your actual API key.
+$api_key = getenv('GEMINI_API_KEY');
+$api_url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=' . $api_key;
+
+$payload = [
+ 'contents' => [
+ [
+ 'parts' => [
+ ['text' => $knowledge_base . "\n\nCustomer Question: " . $user_question]
+ ]
+ ]
+ ]
+];
+
+// 4. Use cURL to make the API call
+$ch = curl_init();
+curl_setopt($ch, CURLOPT_URL, $api_url);
+curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
+curl_setopt($ch, CURLOPT_POST, true);
+curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // For local development only
+
+$response = curl_exec($ch);
+$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+curl_close($ch);
+
+// 5. Process and return the response
+if ($http_code === 200) {
+ $result = json_decode($response, true);
+ $ai_response = $result['candidates'][0]['content']['parts'][0]['text'] ?? 'I am unable to answer at this time.';
+ echo json_encode(['reply' => $ai_response]);
+} else {
+ // Log the error response for debugging
+ error_log("Gemini API Error: HTTP " . $http_code . " - " . $response);
+ echo json_encode(['error' => 'Failed to get a response from the AI assistant. Please try again later.']);
+}
+
diff --git a/api/process_signup.php b/api/process_signup.php
new file mode 100644
index 0000000..9b74fe0
--- /dev/null
+++ b/api/process_signup.php
@@ -0,0 +1,77 @@
+plan_id) || empty($json_obj->name) || empty($json_obj->email) || empty($json_obj->address)) {
+ throw new Exception('Incomplete data provided.');
+ }
+
+ // 2. Fetch Plan
+ $stmt = $pdo->prepare("SELECT * FROM plans WHERE id = ?");
+ $stmt->execute([$json_obj->plan_id]);
+ $plan = $stmt->fetch(PDO::FETCH_OBJ);
+ if (!$plan) {
+ throw new Exception('Plan not found.');
+ }
+ $order_amount = $plan->price_monthly; // Amount in dollars
+
+ // 3. Create Stripe Customer
+ $stripe_customer = \Stripe\Customer::create([
+ 'name' => $json_obj->name,
+ 'email' => $json_obj->email,
+ 'address' => [
+ 'line1' => $json_obj->address
+ ],
+ ]);
+
+ // 4. Create Local Customer
+ // For now, using a placeholder for the password. In a real app, this should be properly hashed.
+ $password_placeholder = password_hash('password123', PASSWORD_DEFAULT);
+ $stmt = $pdo->prepare("INSERT INTO customers (name, email, password, service_address, stripe_customer_id) VALUES (?, ?, ?, ?, ?)");
+ $stmt->execute([$json_obj->name, $json_obj->email, $password_placeholder, $json_obj->address, $stripe_customer->id]);
+ $customer_id = $pdo->lastInsertId();
+
+ // 5. Create Local Order
+ $stmt = $pdo->prepare("INSERT INTO orders (customer_id, plan_id, order_status, amount) VALUES (?, ?, 'pending', ?)");
+ $stmt->execute([$customer_id, $plan->id, $order_amount]);
+ $order_id = $pdo->lastInsertId();
+
+ // 6. Create Stripe Payment Intent
+ $paymentIntent = \Stripe\PaymentIntent::create([
+ 'customer' => $stripe_customer->id,
+ 'amount' => round($order_amount * 100), // Amount in cents
+ 'currency' => 'aud',
+ 'automatic_payment_methods' => [
+ 'enabled' => true,
+ ],
+ 'metadata' => [
+ 'order_id' => $order_id,
+ 'customer_id' => $customer_id,
+ 'plan_id' => $plan->id
+ ]
+ ]);
+
+ // 7. Update Local Order with Payment Intent ID
+ $stmt = $pdo->prepare("UPDATE orders SET stripe_payment_intent_id = ? WHERE id = ?");
+ $stmt->execute([$paymentIntent->id, $order_id]);
+
+ // 8. Return Client Secret
+ echo json_encode([
+ 'clientSecret' => $paymentIntent->client_secret,
+ ]);
+
+} catch (Exception $e) {
+ http_response_code(500);
+ echo json_encode(['error' => $e->getMessage()]);
+}
diff --git a/assets/js/signup.js b/assets/js/signup.js
index 2095787..3d8a305 100644
--- a/assets/js/signup.js
+++ b/assets/js/signup.js
@@ -1,36 +1,21 @@
-// This is your test publishable key. Don't hardcode this in a real app.
const stripe = Stripe('pk_test_51Hh9Y2L9s5P2Q8Z4sYgY8Z4sYgY8Z4sYgY8Z4sYgY8Z4sYgY8Z4sYgY8Z4sYgY8Z4sYgY8Z4sYgY8Z4sYgY8Z4');
-let elements;
-let clientSecret;
+const form = document.querySelector("#signup-form");
+form.addEventListener("submit", handleSubmit);
-initialize();
+// Initially, disable the submit button until the form is filled
+document.querySelector("#submit").disabled = true;
-document
- .querySelector("#signup-form")
- .addEventListener("submit", handleSubmit);
+// Create and mount the Payment Element
+const elements = stripe.elements();
+const paymentElement = elements.create("payment");
+paymentElement.mount("#payment-element");
-// Fetches a payment intent and captures the client secret
-async function initialize() {
- const urlParams = new URLSearchParams(window.location.search);
- const planId = urlParams.get('plan_id');
+// Re-enable the submit button when the Payment Element is ready
+paymentElement.on('ready', () => {
+ document.querySelector("#submit").disabled = false;
+});
- const response = await fetch("/api/create_payment_intent.php", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ plan_id: planId }),
- });
- const data = await response.json();
- clientSecret = data.clientSecret;
-
- const appearance = {
- theme: 'stripe',
- };
- elements = stripe.elements({ appearance, clientSecret });
-
- const paymentElement = elements.create("payment");
- paymentElement.mount("#payment-element");
-}
async function handleSubmit(e) {
e.preventDefault();
@@ -39,69 +24,64 @@ async function handleSubmit(e) {
const name = document.getElementById('name').value;
const email = document.getElementById('email').value;
const address = document.getElementById('address').value;
-
- if (!name || !email || !address) {
+ const planId = form.dataset.planId;
+
+ if (!name || !email || !address || !planId) {
showMessage("Please fill out all fields.");
setLoading(false);
return;
}
- const payment_intent_id = clientSecret.split('_secret')[0];
-
- // Update the payment intent with customer details
- await fetch("/api/update_payment_intent.php", {
+ // Create customer, order, and payment intent on the server.
+ const response = await fetch("/api/process_signup.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- payment_intent_id: payment_intent_id,
+ body: JSON.stringify({
+ plan_id: planId,
name: name,
email: email,
address: address
}),
});
- const { error } = await stripe.confirmPayment({
+ const { clientSecret, error } = await response.json();
+
+ if (error) {
+ showMessage(error);
+ setLoading(false);
+ return;
+ }
+
+ // Confirm the payment with the client secret.
+ const { error: stripeError } = await stripe.confirmPayment({
elements,
+ clientSecret,
confirmParams: {
- // Make sure to change this to your payment completion page
return_url: window.location.origin + "/order_confirmation.php",
receipt_email: email,
},
});
- // This point will only be reached if there is an immediate error when
- // confirming the payment. Otherwise, your customer will be redirected to
- // your `return_url`. For some payment methods like iDEAL, your customer will
- // be redirected to an intermediate site first to authorize the payment, then
- // redirected to the `return_url`.
- if (error.type === "card_error" || error.type === "validation_error") {
- showMessage(error.message);
- } else {
- showMessage("An unexpected error occurred.");
+ if (stripeError) {
+ showMessage(stripeError.message);
}
setLoading(false);
}
-
// ------- UI helpers -------
-
function showMessage(messageText) {
const messageContainer = document.querySelector("#payment-message");
-
messageContainer.style.display = "block";
messageContainer.textContent = messageText;
-
setTimeout(function () {
messageContainer.style.display = "none";
messageContainer.textContent = "";
- }, 4000);
+ }, 5000);
}
-// Show a spinner on payment submission
function setLoading(isLoading) {
if (isLoading) {
- // Disable the button and show a spinner
document.querySelector("#submit").disabled = true;
document.querySelector("#spinner").style.display = "inline";
document.querySelector("#button-text").style.display = "none";
@@ -110,4 +90,4 @@ function setLoading(isLoading) {
document.querySelector("#spinner").style.display = "none";
document.querySelector("#button-text").style.display = "inline";
}
-}
\ No newline at end of file
+}
diff --git a/db/migrations/005_create_pages_table.sql b/db/migrations/005_create_pages_table.sql
new file mode 100644
index 0000000..e296374
--- /dev/null
+++ b/db/migrations/005_create_pages_table.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS pages (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ slug VARCHAR(255) NOT NULL UNIQUE,
+ title VARCHAR(255) NOT NULL,
+ content TEXT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/footer.php b/footer.php
new file mode 100644
index 0000000..7d87b6a
--- /dev/null
+++ b/footer.php
@@ -0,0 +1,13 @@
+
+
+
+
+
© Australia Broadband Internet. All Rights Reserved.
+
Privacy Policy
+
+
+
+
+
+