diff --git a/add-availability.php b/add-availability.php
new file mode 100644
index 0000000..c88505d
--- /dev/null
+++ b/add-availability.php
@@ -0,0 +1,48 @@
+= strtotime($end_datetime)) {
+ header('Location: dashboard.php?status=error&message=End+time+must+be+after+start+time');
+ exit;
+ }
+
+ try {
+ $stmt = db()->prepare("INSERT INTO coach_availability (coach_id, start_time, end_time) VALUES (?, ?, ?)");
+ $stmt->execute([$coach_id, $start_datetime, $end_datetime]);
+
+ header('Location: dashboard.php?status=success&message=Availability+added+successfully');
+ exit;
+ } catch (PDOException $e) {
+ // Log error and redirect
+ error_log('Error adding availability: ' . $e->getMessage());
+ header('Location: dashboard.php?status=error&message=Could+not+add+availability');
+ exit;
+ }
+}
+
+// Redirect if accessed directly without POST
+header('Location: dashboard.php');
+exit;
diff --git a/add-recurring-availability.php b/add-recurring-availability.php
new file mode 100644
index 0000000..9369af6
--- /dev/null
+++ b/add-recurring-availability.php
@@ -0,0 +1,30 @@
+prepare("INSERT INTO coach_recurring_availability (coach_id, day_of_week, start_time, end_time) VALUES (?, ?, ?, ?)");
+ $stmt->execute([$coach_id, $day_of_week, $start_time, $end_time]);
+ header('Location: dashboard.php?status=recurring_added');
+ } catch (PDOException $e) {
+ header('Location: dashboard.php?status=error');
+ }
+ } else {
+ header('Location: dashboard.php?status=error');
+ }
+} else {
+ header('Location: dashboard.php');
+}
+exit;
diff --git a/admin/assign-content.php b/admin/assign-content.php
new file mode 100644
index 0000000..3e5842f
--- /dev/null
+++ b/admin/assign-content.php
@@ -0,0 +1,74 @@
+prepare("SELECT title FROM content WHERE id = ? AND coach_id = ?");
+$stmt->execute([$content_id, $coach_id]);
+$content = $stmt->fetch();
+
+if (!$content) {
+ header('Location: content.php');
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $client_ids = $_POST['client_ids'];
+ if (!empty($client_ids)) {
+ $assigned_count = 0;
+ foreach ($client_ids as $client_id) {
+ // Check if already assigned
+ $check_stmt = db()->prepare("SELECT id FROM client_content WHERE client_id = ? AND content_id = ?");
+ $check_stmt->execute([$client_id, $content_id]);
+ if ($check_stmt->rowCount() == 0) {
+ $assign_stmt = db()->prepare("INSERT INTO client_content (client_id, content_id) VALUES (?, ?)");
+ if ($assign_stmt->execute([$client_id, $content_id])) {
+ $assigned_count++;
+ }
+ }
+ }
+ $message = "Content assigned to " . $assigned_count . " client(s) successfully!";
+ } else {
+ $error = "Please select at least one client.";
+ }
+}
+
+
+// Fetch all clients of the coach
+$clients_stmt = db()->query('SELECT id, name FROM clients ORDER BY name');
+$clients = $clients_stmt->fetchAll();
+
+?>
+
+
+
Assign Content:
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/assign-package-content.php b/admin/assign-package-content.php
new file mode 100644
index 0000000..a3546a6
--- /dev/null
+++ b/admin/assign-package-content.php
@@ -0,0 +1,82 @@
+prepare("SELECT title FROM content WHERE id = ? AND coach_id = ?");
+$stmt->execute([$content_id, $coach_id]);
+$content = $stmt->fetch();
+
+if (!$content) {
+ header('Location: content.php');
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $package_id = $_POST['package_id'];
+ $delay_days = $_POST['delay_days'];
+
+ if (!empty($package_id)) {
+ // Check if already assigned
+ $check_stmt = db()->prepare("SELECT id FROM package_content WHERE package_id = ? AND content_id = ?");
+ $check_stmt->execute([$package_id, $content_id]);
+ if ($check_stmt->rowCount() == 0) {
+ $assign_stmt = db()->prepare("INSERT INTO package_content (package_id, content_id, delay_days) VALUES (?, ?, ?)");
+ if ($assign_stmt->execute([$package_id, $content_id, $delay_days])) {
+ $message = "Content assigned to the package successfully!";
+ } else {
+ $error = "Error: Could not assign content to the package.";
+ }
+ } else {
+ $error = "This content is already assigned to the selected package.";
+ }
+ } else {
+ $error = "Please select a package.";
+ }
+}
+
+// Fetch all packages of the coach
+$packages_stmt = db()->prepare('SELECT id, name FROM service_packages WHERE coach_id = ? ORDER BY name');
+$packages_stmt->execute([$coach_id]);
+$packages = $packages_stmt->fetchAll();
+
+?>
+
+
+
Assign Content to Package:
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/assign_survey.php b/admin/assign_survey.php
new file mode 100644
index 0000000..5ec377a
--- /dev/null
+++ b/admin/assign_survey.php
@@ -0,0 +1,80 @@
+prepare('SELECT * FROM surveys WHERE id = ? AND coach_id = ?');
+$stmt->execute([$survey_id, $coach_id]);
+$survey = $stmt->fetch();
+
+if (!$survey) {
+ header('Location: surveys.php');
+ exit;
+}
+
+// Fetch clients
+$clients_stmt = db()->query('SELECT id, name FROM clients');
+$clients = $clients_stmt->fetchAll();
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (isset($_POST['assign_survey'])) {
+ $client_ids = $_POST['client_ids'] ?? [];
+ if (!empty($client_ids)) {
+ $stmt = db()->prepare('INSERT INTO client_surveys (survey_id, client_id) VALUES (?, ?)');
+ foreach ($client_ids as $client_id) {
+ // Check if already assigned
+ $check_stmt = db()->prepare('SELECT id FROM client_surveys WHERE survey_id = ? AND client_id = ?');
+ $check_stmt->execute([$survey_id, $client_id]);
+ if ($check_stmt->rowCount() === 0) {
+ $stmt->execute([$survey_id, $client_id]);
+ }
+ }
+ header('Location: surveys.php');
+ exit;
+ }
+ }
+}
+
+// Get already assigned clients
+$assigned_stmt = db()->prepare('SELECT client_id FROM client_surveys WHERE survey_id = ?');
+$assigned_stmt->execute([$survey_id]);
+$assigned_clients = $assigned_stmt->fetchAll(PDO::FETCH_COLUMN);
+
+?>
+
+
+
+
diff --git a/admin/broadcast.php b/admin/broadcast.php
new file mode 100644
index 0000000..68584b1
--- /dev/null
+++ b/admin/broadcast.php
@@ -0,0 +1,51 @@
+
+
+
+
Broadcast Message
+
Send a message to all clients.
+
+
+
+
+
+
+
+
diff --git a/admin/client.php b/admin/client.php
new file mode 100644
index 0000000..15c4ede
--- /dev/null
+++ b/admin/client.php
@@ -0,0 +1,283 @@
+No client ID specified.
";
+ require_once '../includes/footer.php';
+ exit;
+}
+
+$client_id = $_GET['id'];
+
+// Handle adding a new note
+if (isset($_POST['add_note'])) {
+ $note = $_POST['note'];
+ $coach_id = $_SESSION['user_id']; // Assuming the logged-in user is a coach
+
+ $sql = "INSERT INTO client_notes (client_id, coach_id, note) VALUES (?, ?, ?)";
+ $stmt = db()->prepare($sql);
+ $stmt->execute([$client_id, $coach_id, $note]);
+}
+
+// Fetch client data from the database
+$sql = "SELECT * FROM clients WHERE id = ?";
+$stmt = db()->prepare($sql);
+$stmt->execute([$client_id]);
+$client = $stmt->fetch(PDO::FETCH_ASSOC);
+
+// Fetch notes for the client
+$notes_sql = "SELECT cn.*, c.name as coach_name FROM client_notes cn JOIN coaches c ON cn.coach_id = c.id WHERE cn.client_id = ? ORDER BY cn.created_at DESC";
+$notes_stmt = db()->prepare($notes_sql);
+$notes_stmt->execute([$client_id]);
+$notes = $notes_stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Fetch purchase history for the client
+$purchases_sql = "SELECT sp.name, sp.price, cp.purchased_at FROM client_packages cp JOIN service_packages sp ON cp.package_id = sp.id WHERE cp.client_id = ? ORDER BY cp.purchased_at DESC";
+$purchases_stmt = db()->prepare($purchases_sql);
+$purchases_stmt->execute([$client_id]);
+$purchases = $purchases_stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Fetch signed contracts for the client
+$contracts_sql = "SELECT c.title, cc.signed_at, cc.docuseal_document_url FROM client_contracts cc JOIN contracts c ON cc.contract_id = c.id WHERE cc.client_id = ? AND cc.status = 'signed' ORDER BY cc.signed_at DESC";
+$contracts_stmt = db()->prepare($contracts_sql);
+$contracts_stmt->execute([$client_id]);
+$contracts = $contracts_stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Fetch appointment history for the client
+$appointments_sql = "SELECT b.start_time, b.end_time, b.status, c.name as coach_name FROM bookings b JOIN coaches c ON b.coach_id = c.id WHERE b.client_id = ? ORDER BY b.start_time DESC";
+$appointments_stmt = db()->prepare($appointments_sql);
+$appointments_stmt->execute([$client_id]);
+$appointments = $appointments_stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Fetch SMS logs for the client
+$sms_logs_sql = "SELECT * FROM sms_logs WHERE client_id = ? ORDER BY created_at DESC";
+$sms_logs_stmt = db()->prepare($sms_logs_sql);
+$sms_logs_stmt->execute([$client_id]);
+$sms_logs = $sms_logs_stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// If client not found
+if (!$client) {
+ echo "";
+ require_once '../includes/footer.php';
+ exit;
+}
+?>
+
+
+
Client Details
+
+
+
+
Client Information
+
Name:
+
Email:
+
Joined:
+
+
+
+ Send SMS
+
+
+
+
+
+
+
Client Activity
+
+
+ Notes
+
+
+ SMS Logs
+
+
+ Purchase History
+
+
+ Signed Contracts
+
+
+ Completed Surveys
+
+
+ Appointment History
+
+
+
+
+
+
Add a Note
+
+
+
+
+ Add Note
+
+
+
Notes
+
+
No notes found for this client.
+
+
+
+
+
+
+
+
+
+
+
No purchase history found for this client.
+
+
+
+
+ Package Name
+ Price
+ Date
+
+
+
+
+
+
+ $
+
+
+
+
+
+
+
+
+
+
+
+
No signed contracts found for this client.
+
+
+
+
+ Contract Title
+ Date Signed
+ View Contract
+
+
+
+
+
+
+
+
+
+ View
+
+ Not available
+
+
+
+
+
+
+
+
+
+
+
+
No completed surveys found for this client.
+
+
+
+
+
+
No SMS history found for this client.
+
+
+
+
+ From
+ To
+ Message
+ Status
+ Date
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Message
+
+
+
+ Send
+
+
+
+
+
+
+
+
+
diff --git a/admin/clients.php b/admin/clients.php
new file mode 100644
index 0000000..3efe995
--- /dev/null
+++ b/admin/clients.php
@@ -0,0 +1,62 @@
+query('SELECT id, name, email FROM clients ORDER BY name');
+$clients = $stmt->fetchAll();
+
+?>
+
+
+
Clients
+
View your clients and their signed contracts.
+
+
+
+
+ Name
+ Email
+ Signed Contracts
+
+
+
+
+
+
+
+
+
+ prepare('SELECT cc.id, c.title FROM client_contracts cc JOIN contracts c ON cc.contract_id = c.id WHERE cc.client_id = ?');
+ $contract_stmt->execute([$client['id']]);
+ $signed_contracts = $contract_stmt->fetchAll();
+ ?>
+
+
+
+
+
+ No signed contracts
+
+
+
+
+ View
+
+
+
+
+
+
+
+
diff --git a/admin/content.php b/admin/content.php
new file mode 100644
index 0000000..2cd0983
--- /dev/null
+++ b/admin/content.php
@@ -0,0 +1,90 @@
+prepare('SELECT file_path FROM content WHERE id = ? AND coach_id = ?');
+ $stmt->execute([$content_id, $coach_id]);
+ $content = $stmt->fetch();
+
+ if ($content) {
+ // Delete file from server
+ if (file_exists($content['file_path'])) {
+ unlink($content['file_path']);
+ }
+
+ // Delete from database
+ $delete_stmt = db()->prepare('DELETE FROM content WHERE id = ?');
+ $delete_stmt->execute([$content_id]);
+ $message = "Content deleted successfully!";
+ } else {
+ $error = "Error: Content not found or you don't have permission to delete it.";
+ }
+}
+
+$stmt = db()->prepare('SELECT * FROM content WHERE coach_id = ? ORDER BY created_at DESC');
+$stmt->execute([$coach_id]);
+$contents = $stmt->fetchAll();
+
+?>
+
+
+
Content
+
Manage your content library. You can upload PDFs, videos, worksheets, etc.
+
+
Upload New Content
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/contracts.php b/admin/contracts.php
new file mode 100644
index 0000000..7990dfd
--- /dev/null
+++ b/admin/contracts.php
@@ -0,0 +1,50 @@
+query('SELECT * FROM contracts ORDER BY created_at DESC');
+$contracts = $stmt->fetchAll();
+
+?>
+
+
+
Manage Contracts
+
Create, edit, and delete contract templates for your clients.
+
+
Add New Contract
+
+
+
+
+ Title
+ Created At
+ Actions
+
+
+
+
+
+
+
+
+ Edit
+ Delete
+
+
+
+
+
+ No contracts found.
+
+
+
+
+
+
+
diff --git a/admin/create_survey.php b/admin/create_survey.php
new file mode 100644
index 0000000..b1d4813
--- /dev/null
+++ b/admin/create_survey.php
@@ -0,0 +1,52 @@
+prepare('INSERT INTO surveys (coach_id, title, description) VALUES (?, ?, ?)');
+ if ($stmt->execute([$coach_id, $title, $description])) {
+ $survey_id = db()->lastInsertId();
+ header('Location: edit_survey.php?id=' . $survey_id);
+ exit;
+ } else {
+ $error = 'Failed to create survey. Please try again.';
+ }
+ }
+}
+?>
+
+
+
Create New Survey
+
+
+
+
+
+
+
+ Survey Title
+
+
+
+ Description (Optional)
+
+
+ Create and Add Questions
+ Cancel
+
+
+
+
diff --git a/admin/dashboard.php b/admin/dashboard.php
new file mode 100644
index 0000000..fd50a0d
--- /dev/null
+++ b/admin/dashboard.php
@@ -0,0 +1,186 @@
+query($clients_sql);
+$total_clients = $clients_result->fetch(PDO::FETCH_ASSOC)['total_clients'];
+
+$coaches_sql = "SELECT COUNT(*) as total_coaches FROM coaches";
+$coaches_result = db()->query($coaches_sql);
+$total_coaches = $coaches_result->fetch(PDO::FETCH_ASSOC)['total_coaches'];
+
+$bookings_sql = "SELECT COUNT(*) as total_bookings FROM bookings";
+$bookings_result = db()->query($bookings_sql);
+$total_bookings = $bookings_result->fetch(PDO::FETCH_ASSOC)['total_bookings'];
+
+$bookings_revenue_sql = "SELECT SUM(price) as total_revenue FROM bookings WHERE status = 'confirmed'";
+$bookings_revenue_result = db()->query($bookings_revenue_sql);
+$total_bookings_revenue = $bookings_revenue_result->fetch(PDO::FETCH_ASSOC)['total_revenue'] ?? 0;
+
+$packages_revenue_sql = "SELECT SUM(p.price) as total_revenue FROM client_packages cp JOIN service_packages p ON cp.package_id = p.id";
+$packages_revenue_result = db()->query($packages_revenue_sql);
+$total_packages_revenue = $packages_revenue_result->fetch(PDO::FETCH_ASSOC)['total_revenue'] ?? 0;
+
+$popular_packages_sql = "SELECT p.name, COUNT(cp.id) as purchase_count FROM client_packages cp JOIN service_packages p ON cp.package_id = p.id GROUP BY p.name ORDER BY purchase_count DESC LIMIT 5";
+$popular_packages_result = db()->query($popular_packages_sql);
+$popular_packages = $popular_packages_result->fetchAll(PDO::FETCH_ASSOC);
+
+$client_signups_sql = "SELECT DATE(created_at) as signup_date, COUNT(*) as signup_count FROM clients WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY DATE(created_at) ORDER BY signup_date ASC";
+$client_signups_result = db()->query($client_signups_sql);
+$client_signups = $client_signups_result->fetchAll(PDO::FETCH_ASSOC);
+
+$daily_revenue_sql = "SELECT DATE(created_at) as revenue_date, SUM(price) as daily_revenue FROM bookings WHERE status = 'confirmed' AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY DATE(created_at) ORDER BY revenue_date ASC";
+$daily_revenue_result = db()->query($daily_revenue_sql);
+$daily_revenue = $daily_revenue_result->fetchAll(PDO::FETCH_ASSOC);
+
+
+// You can add more queries to fetch other business insights
+
+?>
+
+
+
Business Insights Dashboard
+
+
+
+
+
+
+
Total Revenue (Bookings)
+
$
+
+
+
+
+
+
+
Total Revenue (Packages)
+
$
+
+
+
+
+
+
+
Popular Service Packages
+
+
+
+
+
+
+
+
+
+ No packages sold yet.
+
+
+
+
+
Recent Client Signups
+
+
+
+
+
+
Daily Revenue (Last 30 Days)
+
+
+
+
+
+
+
+
+
diff --git a/admin/delete-contract.php b/admin/delete-contract.php
new file mode 100644
index 0000000..c8ebf60
--- /dev/null
+++ b/admin/delete-contract.php
@@ -0,0 +1,17 @@
+prepare('DELETE FROM contracts WHERE id = ?');
+ $stmt->execute([$_GET['id']]);
+}
+
+header('Location: /admin/contracts.php');
+exit;
diff --git a/admin/discounts.php b/admin/discounts.php
new file mode 100644
index 0000000..1a92feb
--- /dev/null
+++ b/admin/discounts.php
@@ -0,0 +1,121 @@
+prepare('INSERT INTO discounts (code, type, value, start_date, end_date, uses_limit) VALUES (?, ?, ?, ?, ?, ?)');
+ $stmt->execute([$code, $type, $value, $start_date, $end_date, $uses_limit]);
+ } elseif (isset($_POST['update_discount'])) {
+ $id = $_POST['id'];
+ $code = $_POST['code'];
+ $type = $_POST['type'];
+ $value = $_POST['value'];
+ $start_date = empty($_POST['start_date']) ? null : $_POST['start_date'];
+ $end_date = empty($_POST['end_date']) ? null : $_POST['end_date'];
+ $uses_limit = empty($_POST['uses_limit']) ? null : $_POST['uses_limit'];
+ $is_active = isset($_POST['is_active']) ? 1 : 0;
+
+ $stmt = db()->prepare('UPDATE discounts SET code = ?, type = ?, value = ?, start_date = ?, end_date = ?, uses_limit = ?, is_active = ? WHERE id = ?');
+ $stmt->execute([$code, $type, $value, $start_date, $end_date, $uses_limit, $is_active, $id]);
+ } elseif (isset($_POST['delete_discount'])) {
+ $id = $_POST['id'];
+ $stmt = db()->prepare('DELETE FROM discounts WHERE id = ?');
+ $stmt->execute([$id]);
+ }
+ header('Location: discounts.php');
+ exit;
+}
+
+// Fetch all discounts
+$stmt = db()->query('SELECT * FROM discounts ORDER BY created_at DESC');
+$discounts = $stmt->fetchAll();
+?>
+
+
+
Manage Discounts
+
+
+
+
+
+
+
+
+
+
+
+ Code
+ Type
+ Value
+ Used/Limit
+ Active
+ Actions
+
+
+
+
+
+ = htmlspecialchars($discount['code']) ?>
+ = htmlspecialchars($discount['type']) ?>
+ = htmlspecialchars($discount['value']) ?>
+ = $discount['times_used'] ?> / = $discount['uses_limit'] ?? '∞' ?>
+ = $discount['is_active'] ? 'Yes' : 'No' ?>
+
+ Edit
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/edit-content.php b/admin/edit-content.php
new file mode 100644
index 0000000..5e799e7
--- /dev/null
+++ b/admin/edit-content.php
@@ -0,0 +1,56 @@
+prepare("SELECT * FROM content WHERE id = ? AND coach_id = ?");
+$stmt->execute([$content_id, $coach_id]);
+$content = $stmt->fetch();
+
+if (!$content) {
+ header('Location: content.php');
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $title = $_POST['title'];
+ $description = $_POST['description'];
+
+ $update_stmt = db()->prepare("UPDATE content SET title = ?, description = ? WHERE id = ?");
+ if ($update_stmt->execute([$title, $description, $content_id])) {
+ header("Location: content.php");
+ exit;
+ } else {
+ $error = "Error: Could not update content.";
+ }
+}
+?>
+
+
+
Edit Content
+
+
+
+
+
+
+
+ Title
+
+
+
+ Description
+
+
+ Save Changes
+
+
+
+
diff --git a/admin/edit-contract.php b/admin/edit-contract.php
new file mode 100644
index 0000000..59dfab1
--- /dev/null
+++ b/admin/edit-contract.php
@@ -0,0 +1,69 @@
+ '', 'title' => '', 'content' => '', 'role' => 'Client'];
+$is_edit = false;
+
+if (isset($_GET['id'])) {
+ $is_edit = true;
+ $stmt = db()->prepare('SELECT * FROM contracts WHERE id = ?');
+ $stmt->execute([$_GET['id']]);
+ $contract = $stmt->fetch();
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $title = $_POST['title'];
+ $content = $_POST['content'];
+ $role = $_POST['role'];
+ $contract_id = $_POST['id'];
+
+ if ($contract_id) {
+ // Update existing contract
+ $stmt = db()->prepare('UPDATE contracts SET title = ?, content = ?, role = ? WHERE id = ?');
+ $stmt->execute([$title, $content, $role, $contract_id]);
+ } else {
+ // Create new contract
+ $stmt = db()->prepare('INSERT INTO contracts (title, content, role) VALUES (?, ?, ?)');
+ $stmt->execute([$title, $content, $role]);
+ }
+
+ header('Location: /admin/contracts.php');
+ exit;
+}
+
+?>
+
+
+
+
+
+
+
+
+ Title
+
+
+
+
+ Role
+
+
+
+
+ Content
+
+
+
+
+ Cancel
+
+
+
+
diff --git a/admin/edit-discount.php b/admin/edit-discount.php
new file mode 100644
index 0000000..21dec42
--- /dev/null
+++ b/admin/edit-discount.php
@@ -0,0 +1,82 @@
+prepare('SELECT * FROM discounts WHERE id = ?');
+$stmt->execute([$id]);
+$discount = $stmt->fetch();
+
+if (!$discount) {
+ header('Location: discounts.php');
+ exit;
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $code = $_POST['code'];
+ $type = $_POST['type'];
+ $value = $_POST['value'];
+ $start_date = empty($_POST['start_date']) ? null : $_POST['start_date'];
+ $end_date = empty($_POST['end_date']) ? null : $_POST['end_date'];
+ $uses_limit = empty($_POST['uses_limit']) ? null : $_POST['uses_limit'];
+ $is_active = isset($_POST['is_active']) ? 1 : 0;
+
+ $stmt = db()->prepare('UPDATE discounts SET code = ?, type = ?, value = ?, start_date = ?, end_date = ?, uses_limit = ?, is_active = ? WHERE id = ?');
+ $stmt->execute([$code, $type, $value, $start_date, $end_date, $uses_limit, $is_active, $id]);
+
+ header('Location: discounts.php');
+ exit;
+}
+
+?>
+
+
+
+
\ No newline at end of file
diff --git a/admin/edit-package.php b/admin/edit-package.php
new file mode 100644
index 0000000..9409688
--- /dev/null
+++ b/admin/edit-package.php
@@ -0,0 +1,134 @@
+prepare(
+ 'UPDATE service_packages SET '
+ . 'name = ?, coach_id = ?, description = ?, price = ?, payment_type = ?, '
+ . 'deposit_amount = ?, installments = ?, installment_interval = ?, pay_in_full_discount_percentage = ? '
+ . 'WHERE id = ?'
+ );
+ $stmt->execute([
+ $name, $coach_id, $description, $price, $payment_type,
+ $deposit_amount, $installments, $installment_interval, $pay_in_full_discount_percentage,
+ $id
+ ]);
+
+ header('Location: manage-packages.php');
+ exit;
+}
+
+$stmt = db()->prepare('SELECT * FROM service_packages WHERE id = ?');
+$stmt->execute([$id]);
+$package = $stmt->fetch();
+
+$coaches = db()->query('SELECT * FROM coaches')->fetchAll();
+
+?>
+
+
+
Edit Package
+
+
+
+
+
+
+
+
+ Package Name
+
+
+
+
+ Coach
+
+
+ >= htmlspecialchars($coach['name']) ?>
+
+
+
+
+
+ Description
+ = htmlspecialchars($package['description']) ?>
+
+
+
+ Price
+
+
+
+
+ Payment Type
+
+ >One Time
+ >Subscription
+ >Payment Plan
+
+
+
+
+
+ Deposit Amount (Optional)
+
+
+
+ Number of Installments
+
+
+
+ Installment Interval
+
+ >Day
+ >Week
+ >Month
+ >Year
+
+
+
+
+
+ Pay in Full Discount (%) (Optional)
+
+
+
+ Update Package
+
+
+
+
+
+
+
+
diff --git a/admin/edit_question_options.php b/admin/edit_question_options.php
new file mode 100644
index 0000000..f4a18ed
--- /dev/null
+++ b/admin/edit_question_options.php
@@ -0,0 +1,93 @@
+prepare('SELECT q.*, s.coach_id, s.id as survey_id FROM survey_questions q JOIN surveys s ON q.survey_id = s.id WHERE q.id = ? AND s.coach_id = ?');
+$stmt->execute([$question_id, $coach_id]);
+$question = $stmt->fetch();
+
+if (!$question) {
+ header('Location: surveys.php');
+ exit;
+}
+
+// Handle option logic (add/delete)
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (isset($_POST['add_option'])) {
+ $option_text = trim($_POST['option_text']);
+ if (!empty($option_text)) {
+ $stmt = db()->prepare('INSERT INTO survey_question_options (question_id, option_text) VALUES (?, ?)');
+ $stmt->execute([$question_id, $option_text]);
+ }
+ } elseif (isset($_POST['delete_option'])) {
+ $option_id = $_POST['option_id'];
+ $stmt = db()->prepare('DELETE FROM survey_question_options WHERE id = ? AND question_id = ?');
+ $stmt->execute([$option_id, $question_id]);
+ }
+}
+
+// Fetch options
+$options_stmt = db()->prepare('SELECT * FROM survey_question_options WHERE question_id = ?');
+$options_stmt->execute([$question_id]);
+$options = $options_stmt->fetchAll();
+
+?>
+
+
+
Manage Options for:
+
+
+
+
+
+
+ Option Text
+
+
+ Add Option
+
+
+
+
+
+
+
+
+
No options have been added for this question yet.
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/edit_survey.php b/admin/edit_survey.php
new file mode 100644
index 0000000..9f96819
--- /dev/null
+++ b/admin/edit_survey.php
@@ -0,0 +1,110 @@
+prepare('SELECT * FROM surveys WHERE id = ? AND coach_id = ?');
+$stmt->execute([$survey_id, $coach_id]);
+$survey = $stmt->fetch();
+
+if (!$survey) {
+ header('Location: surveys.php');
+ exit;
+}
+
+// Handle question logic (add/delete)
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (isset($_POST['add_question'])) {
+ $question = trim($_POST['question']);
+ $type = $_POST['type'];
+
+ if (!empty($question)) {
+ $stmt = db()->prepare('INSERT INTO survey_questions (survey_id, question, type) VALUES (?, ?, ?)');
+ $stmt->execute([$survey_id, $question, $type]);
+ }
+ } elseif (isset($_POST['delete_question'])) {
+ $question_id = $_POST['question_id'];
+ $stmt = db()->prepare('DELETE FROM survey_questions WHERE id = ? AND survey_id = ?');
+ $stmt->execute([$question_id, $survey_id]);
+ }
+}
+
+// Fetch questions
+$questions_stmt = db()->prepare('SELECT * FROM survey_questions WHERE survey_id = ? ORDER BY id');
+$questions_stmt->execute([$survey_id]);
+$questions = $questions_stmt->fetchAll();
+
+?>
+
+
+
Edit Survey:
+
+
+
+
+
+
+ Question Text
+
+
+
+ Question Type
+
+ Text (Single Line)
+ Textarea (Multi-line)
+ Dropdown
+ Radio Buttons
+ Checkboxes
+
+
+ Add Question
+
+
+
+
+
+
+
+
+
No questions have been added to this survey yet.
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/manage-packages.php b/admin/manage-packages.php
new file mode 100644
index 0000000..4669969
--- /dev/null
+++ b/admin/manage-packages.php
@@ -0,0 +1,97 @@
+prepare('INSERT INTO service_packages (name, price, coach_id) VALUES (?, ?, ?)');
+ $stmt->execute([$name, $price, $coach_id]);
+ header('Location: manage-packages.php');
+ exit;
+ } elseif (isset($_POST['delete_package'])) {
+ $id = $_POST['id'];
+ $stmt = db()->prepare('DELETE FROM service_packages WHERE id = ?');
+ $stmt->execute([$id]);
+ header('Location: manage-packages.php');
+ exit;
+ }
+}
+
+// Fetch all packages
+$packages = db()->query('SELECT sp.*, c.name as coach_name FROM service_packages sp JOIN coaches c ON sp.coach_id = c.id ORDER BY sp.created_at DESC')->fetchAll();
+$coaches = db()->query('SELECT * FROM coaches')->fetchAll();
+
+?>
+
+
+
Manage Service Packages
+
+
+
+
+
+
+
+
+
+
+
+ Name
+ Coach
+ Price
+ Payment Type
+ Actions
+
+
+
+
+
+ = htmlspecialchars($package['name']) ?>
+ = htmlspecialchars($package['coach_name']) ?>
+ $= htmlspecialchars($package['price']) ?>
+ = htmlspecialchars(ucfirst(str_replace('_', ' ', $package['payment_type']))) ?>
+
+ Edit
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/settings.php b/admin/settings.php
new file mode 100644
index 0000000..6462c8b
--- /dev/null
+++ b/admin/settings.php
@@ -0,0 +1,123 @@
+query('SELECT * FROM settings');
+$settings = $settings_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
+
+$coach_mode = $settings['coach_mode'] ?? 'multi';
+$single_coach_id = $settings['single_coach_id'] ?? null;
+
+// Fetch all coaches for the dropdown
+$coaches_stmt = db()->query('SELECT id, name FROM coaches');
+$coaches = $coaches_stmt->fetchAll(PDO::FETCH_ASSOC);
+
+$success_message = '';
+$error_message = '';
+
+// Handle form submission
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $new_coach_mode = $_POST['coach_mode'] ?? 'multi';
+ $new_single_coach_id = $_POST['single_coach_id'] ?? null;
+
+ if ($new_coach_mode === 'single' && empty($new_single_coach_id)) {
+ $error_message = 'Please select a coach for single coach mode.';
+ } else {
+ try {
+ db()->beginTransaction();
+ $update_mode_stmt = db()->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'coach_mode'");
+ $update_mode_stmt->execute([$new_coach_mode]);
+
+ $update_id_stmt = db()->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'single_coach_id'");
+ $update_id_stmt->execute([$new_single_coach_id]);
+ db()->commit();
+ $success_message = 'Settings updated successfully!';
+ // Refresh settings after update
+ $coach_mode = $new_coach_mode;
+ $single_coach_id = $new_single_coach_id;
+ } catch (PDOException $e) {
+ db()->rollBack();
+ $error_message = 'Failed to update settings: ' . $e->getMessage();
+ }
+ }
+}
+?>
+
+
+
+
+
+ Admin Settings
+
+
+
+
+
Admin Settings
+
+
+
+ = $success_message ?>
+
+
+
+
+ = $error_message ?>
+
+
+
+
+
+
Coach Mode
+
Switch between featuring multiple coaches or a single business owner.
+
+
+ class="form-radio h-5 w-5 text-blue-600">
+ Multi-Coach Mode
+
+
+ class="form-radio h-5 w-5 text-blue-600">
+ Single-Coach Mode
+
+
+
+
+
+
Single Coach Settings
+ Select the coach to feature:
+
+ -- Select a Coach --
+
+ >= htmlspecialchars($coach['name']) ?>
+
+
+
+
+ Save Settings
+ Back to Dashboard
+
+
+
+
+
+
diff --git a/admin/support.php b/admin/support.php
new file mode 100644
index 0000000..b6a54ae
--- /dev/null
+++ b/admin/support.php
@@ -0,0 +1,46 @@
+prepare('SELECT st.*, c.name as client_name FROM support_tickets st JOIN clients c ON st.client_id = c.id ORDER BY st.updated_at DESC');
+$stmt->execute();
+$tickets = $stmt->fetchAll();
+
+?>
+
+
+
Manage Support Tickets
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/survey_responses.php b/admin/survey_responses.php
new file mode 100644
index 0000000..9706f96
--- /dev/null
+++ b/admin/survey_responses.php
@@ -0,0 +1,84 @@
+prepare('SELECT * FROM surveys WHERE id = ? AND coach_id = ?');
+$stmt->execute([$survey_id, $coach_id]);
+$survey = $stmt->fetch();
+
+if (!$survey) {
+ header('Location: surveys.php');
+ exit;
+}
+
+// Fetch completed surveys by clients
+$completed_stmt = db()->prepare(
+ 'SELECT cs.id, c.name, c.email, cs.completed_at ' .
+ 'FROM client_surveys cs ' .
+ 'JOIN clients c ON cs.client_id = c.id ' .
+ 'WHERE cs.survey_id = ? AND cs.status = \'completed\' ' .
+ 'ORDER BY cs.completed_at DESC'
+);
+$completed_stmt->execute([$survey_id]);
+$completed_surveys = $completed_stmt->fetchAll();
+?>
+
+
+
Responses for:
+
+
+
+
+
+
No clients have completed this survey yet.
+
+
+
+
+ Client Name
+ Client Email
+ Completed On
+ Action
+
+
+
+
+
+
+
+
+
+ View Responses
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/admin/surveys.php b/admin/surveys.php
new file mode 100644
index 0000000..f25bfec
--- /dev/null
+++ b/admin/surveys.php
@@ -0,0 +1,58 @@
+prepare('SELECT id, title, description, created_at FROM surveys WHERE coach_id = ? ORDER BY created_at DESC');
+$stmt->execute([$coach_id]);
+$surveys = $stmt->fetchAll();
+
+?>
+
+
+
+
Create and manage surveys for your clients.
+
+
+
+
+
diff --git a/admin/upload-content.php b/admin/upload-content.php
new file mode 100644
index 0000000..14ec205
--- /dev/null
+++ b/admin/upload-content.php
@@ -0,0 +1,69 @@
+prepare("INSERT INTO content (coach_id, title, description, file_path, file_type) VALUES (?, ?, ?, ?, ?)");
+ if ($stmt->execute([$coach_id, $title, $description, 'uploads/content/' . $file_name, $file_type])) {
+ header("Location: content.php");
+ exit;
+ } else {
+ $error = "Error: Could not save content to the database.";
+ }
+ } else {
+ $error = "Sorry, there was an error uploading your file.";
+ }
+ } else {
+ $error = "Sorry, only JPG, JPEG, PNG, GIF, PDF, MP4, MOV, & AVI files are allowed.";
+ }
+ } else {
+ $error = "Please select a file to upload.";
+ }
+}
+?>
+
+
+
Upload New Content
+
+
+
+
+
+
+
+ Title
+
+
+
+ Description
+
+
+
+ Content File
+
+
+ Upload
+
+
+
+
diff --git a/admin/view-ticket.php b/admin/view-ticket.php
new file mode 100644
index 0000000..7946404
--- /dev/null
+++ b/admin/view-ticket.php
@@ -0,0 +1,142 @@
+prepare('SELECT st.*, c.name as client_name FROM support_tickets st JOIN clients c ON st.client_id = c.id WHERE st.id = ?');
+$stmt->execute([$ticket_id]);
+$ticket = $stmt->fetch();
+
+if (!$ticket) {
+ header('Location: admin/support.php');
+ exit;
+}
+
+// Handle new message submission
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['reply_to_ticket'])) {
+ $message = trim($_POST['message']);
+
+ if (!empty($message)) {
+ try {
+ $pdo->beginTransaction();
+
+ // Insert the new message
+ $stmt = $pdo->prepare('INSERT INTO support_ticket_messages (ticket_id, user_id, is_admin, message) VALUES (?, ?, ?, ?)');
+ $stmt->execute([$ticket_id, $admin_id, true, $message]);
+
+ // Update the ticket's updated_at timestamp
+ $stmt = $pdo->prepare("UPDATE support_tickets SET updated_at = CURRENT_TIMESTAMP, status = 'In Progress' WHERE id = ?");
+ $stmt->execute([$ticket_id]);\n\n $pdo->commit();\n\n // Send email notification to client\n require_once \'../mail/MailService.php\';\n $stmt = $pdo->prepare(\'SELECT email FROM clients WHERE id = ?\');\n $stmt->execute([$ticket[\'client_id\']]);\n $client = $stmt->fetch();\n\n if ($client) {\n $email_subject = \"Re: Support Ticket #{$ticket_id}: {$ticket[\'subject\']}\";\n $email_html = \"A support team member has replied to your ticket.
\"\n . \"Reply:
\" . nl2br(htmlspecialchars($message)) . \"
\"\n . \"You can view the ticket here: View Ticket
\";\n $email_text = \"A support team member has replied to your ticket.\\n\\n\"\n . \"Reply:\\n\" . htmlspecialchars($message) . \"\\n\\n\"\n . \"You can view the ticket here: http://{\$GLOBALS[HTTP_HOST]}/view-ticket.php?id={$ticket_id}\";\n\n MailService::sendMail($client[\'email\'], $email_subject, $email_html, $email_text);\n }\n\n $_SESSION[\'success_message\'] = \'Your reply has been sent.\';\n header(\'Location: view-ticket.php?id=\' . $ticket_id);
+ exit;
+ } catch (Exception $e) {
+ $pdo->rollBack();
+ $error_message = 'Failed to send reply. Please try again.';
+ }
+ }
+}
+
+// Handle status change
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['change_status'])) {
+ $status = $_POST['status'];
+ if (in_array($status, ['Open', 'In Progress', 'Closed'])) {
+ $stmt = $pdo->prepare('UPDATE support_tickets SET status = ? WHERE id = ?');
+ $stmt->execute([$status, $ticket_id]);
+ $_SESSION['success_message'] = 'Ticket status updated successfully.';
+ header('Location: view-ticket.php?id=' . $ticket_id);
+ exit;
+ }
+}
+
+// Fetch all messages for the ticket
+$stmt = $pdo->prepare("SELECT m.*, c.name as client_name, co.name as coach_name FROM support_ticket_messages m LEFT JOIN clients c ON m.user_id = c.id AND m.is_admin = 0 LEFT JOIN coaches co ON m.user_id = co.id AND m.is_admin = 1 WHERE m.ticket_id = ? ORDER BY m.created_at ASC");
+$stmt->execute([$ticket_id]);
+$messages = $stmt->fetchAll();
+
+?>
+
+
+
Back to Tickets
+
+
+
Client:
+
Status:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >Open
+ >In Progress
+ >Closed
+
+
+ Change Status
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/admin/view_survey_response.php b/admin/view_survey_response.php
new file mode 100644
index 0000000..b611c15
--- /dev/null
+++ b/admin/view_survey_response.php
@@ -0,0 +1,67 @@
+prepare(
+ 'SELECT cs.*, s.title, s.description, c.name as client_name ' .
+ 'FROM client_surveys cs ' .
+ 'JOIN surveys s ON cs.survey_id = s.id ' .
+ 'JOIN clients c ON cs.client_id = c.id ' .
+ 'WHERE cs.id = ? AND s.coach_id = ?'
+);
+$stmt->execute([$client_survey_id, $coach_id]);
+$client_survey = $stmt->fetch();
+
+if (!$client_survey) {
+ header('Location: surveys.php');
+ exit;
+}
+
+// Fetch questions and responses
+$qa_stmt = db()->prepare(
+ 'SELECT q.question, qr.response ' .
+ 'FROM survey_responses qr ' .
+ 'JOIN survey_questions q ON qr.question_id = q.id ' .
+ 'WHERE qr.client_survey_id = ? ORDER BY q.id'
+);
+$qa_stmt->execute([$client_survey_id]);
+$qas = $qa_stmt->fetchAll();
+?>
+
+
+
Survey Responses from
+
+
+
+
+
+
+
+
diff --git a/api/availability.php b/api/availability.php
new file mode 100644
index 0000000..8291196
--- /dev/null
+++ b/api/availability.php
@@ -0,0 +1,107 @@
+ 'Coach ID is required']);
+ exit;
+}
+
+$coach_id = $_GET['coach_id'];
+$start_date_str = isset($_GET['start']) ? $_GET['start'] : 'first day of this month';
+$end_date_str = isset($_GET['end']) ? $_GET['end'] : 'last day of this month';
+
+// Fetch coach data
+$coach_stmt = db()->prepare("SELECT buffer_time, timezone FROM coaches WHERE id = ?");
+$coach_stmt->execute([$coach_id]);
+$coach = $coach_stmt->fetch();
+$buffer_time = $coach ? (int)$coach['buffer_time'] : 0;
+$coach_timezone = new DateTimeZone($coach ? $coach['timezone'] : 'UTC');
+
+// Fetch client timezone
+$client_timezone_str = 'UTC'; // Default
+if (isset($_SESSION['user_id'])) {
+ $client_stmt = db()->prepare("SELECT timezone FROM clients WHERE id = ?");
+ $client_stmt->execute([$_SESSION['user_id']]);
+ $client = $client_stmt->fetch();
+ if ($client && $client['timezone']) {
+ $client_timezone_str = $client['timezone'];
+ }
+}
+$client_timezone = new DateTimeZone($client_timezone_str);
+
+$start_date = new DateTime($start_date_str, $client_timezone);
+$end_date = new DateTime($end_date_str, $client_timezone);
+
+$response = [
+ 'availability' => [],
+ 'bookings' => []
+];
+
+// Helper function to convert time
+function convert_time($time_str, $from_tz, $to_tz) {
+ $dt = new DateTime($time_str, $from_tz);
+ $dt->setTimezone($to_tz);
+ return $dt->format('Y-m-d H:i:s');
+}
+
+// Fetch one-off availability
+$stmt_availability = db()->prepare("SELECT start_time, end_time FROM coach_availability WHERE coach_id = ? AND start_time BETWEEN ? AND ?");
+$stmt_availability->execute([$coach_id, $start_date->format('Y-m-d H:i:s'), $end_date->format('Y-m-d H:i:s')]);
+$one_off_availability = $stmt_availability->fetchAll(PDO::FETCH_ASSOC);
+
+foreach ($one_off_availability as $slot) {
+ $response['availability'][] = [
+ 'start_time' => convert_time($slot['start_time'], $coach_timezone, $client_timezone),
+ 'end_time' => convert_time($slot['end_time'], $coach_timezone, $client_timezone)
+ ];
+}
+
+
+// Fetch recurring availability and generate slots
+$stmt_recurring = db()->prepare("SELECT day_of_week, start_time, end_time FROM coach_recurring_availability WHERE coach_id = ?");
+$stmt_recurring->execute([$coach_id]);
+$recurring_availability = $stmt_recurring->fetchAll(PDO::FETCH_ASSOC);
+
+$interval = new DateInterval('P1D');
+$period = new DatePeriod($start_date, $interval, $end_date->modify('+1 day'));
+
+foreach ($period as $date) {
+ $day_of_week = $date->format('w');
+ foreach ($recurring_availability as $rule) {
+ if ((int)$rule['day_of_week'] === (int)$day_of_week) {
+ $start_time_str = $date->format('Y-m-d') . ' ' . $rule['start_time'];
+ $end_time_str = $date->format('Y-m-d') . ' ' . $rule['end_time'];
+
+ $start_dt = new DateTime($start_time_str, $coach_timezone);
+
+ if ($start_dt >= new DateTime('now', $coach_timezone)) {
+ $response['availability'][] = [
+ 'start_time' => convert_time($start_time_str, $coach_timezone, $client_timezone),
+ 'end_time' => convert_time($end_time_str, $coach_timezone, $client_timezone)
+ ];
+ }
+ }
+ }
+}
+
+// Fetch confirmed and pending bookings
+$stmt_bookings = db()->prepare("SELECT booking_time FROM bookings WHERE coach_id = ? AND (status = 'confirmed' OR status = 'pending') AND booking_time BETWEEN ? AND ?");
+$stmt_bookings->execute([$coach_id, $start_date->format('Y-m-d H:i:s'), $end_date->format('Y-m-d H:i:s')]);
+$bookings = $stmt_bookings->fetchAll(PDO::FETCH_ASSOC);
+
+// Assume each booking is for one hour and add buffer
+foreach ($bookings as $booking) {
+ $start_booking_time = new DateTime($booking['booking_time'], $coach_timezone);
+ $end_booking_time = clone $start_booking_time;
+ $end_booking_time->add(new DateInterval('PT1H')); // Assuming 1-hour bookings
+ $end_booking_time->add(new DateInterval('PT' . $buffer_time . 'M'));
+
+ $response['bookings'][] = [
+ 'start_time' => $start_booking_time->setTimezone($client_timezone)->format('Y-m-d H:i:s'),
+ 'end_time' => $end_booking_time->setTimezone($client_timezone)->format('Y-m-d H:i:s')
+ ];
+}
+
+echo json_encode($response);
\ No newline at end of file
diff --git a/api/broadcast_sms.php b/api/broadcast_sms.php
new file mode 100644
index 0000000..d3b03f9
--- /dev/null
+++ b/api/broadcast_sms.php
@@ -0,0 +1,70 @@
+ 'Method not allowed']);
+ exit;
+}
+
+$message = $_POST['message'] ?? null;
+
+if (empty($message)) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Message is required']);
+ exit;
+}
+
+// Get all clients with phone numbers
+$stmt = db()->prepare("SELECT id, name, phone FROM clients WHERE phone IS NOT NULL AND phone != ''");
+$stmt->execute();
+$clients = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+// Set up Telnyx API
+\Telnyx\Telnyx::setApiKey(TELNYX_API_KEY);
+
+$success_count = 0;
+$error_count = 0;
+
+foreach ($clients as $client) {
+ try {
+ $response = \Telnyx\Message::create([
+ 'from' => TELNYX_MESSAGING_PROFILE_ID,
+ 'to' => $client['phone'],
+ 'text' => $message,
+ ]);
+
+ // Log the message
+ $log_stmt = db()->prepare("INSERT INTO sms_logs (client_id, sender, recipient, message, status) VALUES (?, ?, ?, ?, ?, ?)");
+ $log_stmt->execute([
+ $client['id'],
+ TELNYX_MESSAGING_PROFILE_ID,
+ $client['phone'],
+ $message,
+ 'sent'
+ ]);
+
+ $success_count++;
+
+ } catch (\Telnyx\Exception\ApiErrorException $e) {
+ $error_count++;
+ // Log the failure
+ $log_stmt = db()->prepare("INSERT INTO sms_logs (client_id, sender, recipient, message, status) VALUES (?, ?, ?, ?, ?, ?)");
+ $log_stmt->execute([
+ $client['id'],
+ TELNYX_MESSAGING_PROFILE_ID,
+ $client['phone'],
+ $message,
+ 'failed'
+ ]);
+ }
+}
+
+if ($success_count > 0) {
+ echo json_encode(['success' => true, 'message' => "Broadcast sent to " . $success_count . " clients. " . $error_count . " failed."]);
+} else {
+ http_response_code(500);
+ echo json_encode(['error' => 'Failed to send broadcast to any clients.']);
+}
diff --git a/api/messages.php b/api/messages.php
new file mode 100644
index 0000000..567bad5
--- /dev/null
+++ b/api/messages.php
@@ -0,0 +1,112 @@
+ 'Unauthorized']);
+ exit;
+}
+
+$action = $_GET['action'] ?? null;
+$userId = $_SESSION['user_id'];
+$userType = $_SESSION['user_type'];
+
+header('Content-Type: application/json');
+
+switch ($action) {
+ case 'get_conversations':
+ $db = db();
+ // This query is complex. It gets the last message for each conversation.
+ $stmt = $db->prepare("
+ SELECT
+ other_user.id as user_id,
+ other_user.type as user_type,
+ other_user.name,
+ last_message.message,
+ last_message.created_at,
+ (SELECT COUNT(*) FROM messages WHERE receiver_id = :user_id AND receiver_type = :user_type AND sender_id = other_user.id AND sender_type = other_user.type AND is_read = 0) as unread_count
+ FROM (
+ SELECT
+ CASE WHEN sender_id = :user_id AND sender_type = :user_type THEN receiver_id ELSE sender_id END as other_id,
+ CASE WHEN sender_id = :user_id AND sender_type = :user_type THEN receiver_type ELSE sender_type END as other_type,
+ MAX(id) as last_message_id
+ FROM messages
+ WHERE (sender_id = :user_id AND sender_type = :user_type) OR (receiver_id = :user_id AND receiver_type = :user_type)
+ GROUP BY other_id, other_type
+ ) as conversations
+ JOIN messages as last_message ON last_message.id = conversations.last_message_id
+ JOIN (
+ SELECT id, name, 'coach' as type FROM coaches
+ UNION ALL
+ SELECT id, name, 'client' as type FROM clients
+ ) as other_user ON other_user.id = conversations.other_id AND other_user.type = conversations.other_type
+ ORDER BY last_message.created_at DESC
+ ");
+
+ $stmt->execute(['user_id' => $userId, 'user_type' => $userType]);
+ $conversations = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ echo json_encode($conversations);
+ break;
+
+ case 'get_messages':
+ $peerId = $_GET['user_id'] ?? null;
+ $peerType = $_GET['user_type'] ?? null;
+
+ if (empty($peerId) || empty($peerType)) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Missing user_id or user_type']);
+ exit;
+ }
+
+ $db = db();
+ // Mark messages as read
+ $updateStmt = $db->prepare("UPDATE messages SET is_read = 1 WHERE sender_id = ? AND sender_type = ? AND receiver_id = ? AND receiver_type = ?");
+ $updateStmt->execute([$peerId, $peerType, $userId, $userType]);
+
+ // Fetch messages
+ $stmt = $db->prepare("SELECT * FROM messages WHERE (sender_id = ? AND sender_type = ? AND receiver_id = ? AND receiver_type = ?) OR (sender_id = ? AND sender_type = ? AND receiver_id = ? AND receiver_type = ?) ORDER BY created_at ASC");
+ $stmt->execute([$userId, $userType, $peerId, $peerType, $peerId, $peerType, $userId, $userType]);
+ $messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ echo json_encode($messages);
+ break;
+
+ case 'send_message':
+ $data = json_decode(file_get_contents('php://input'), true);
+ if (empty($data['receiver_id']) || empty($data['receiver_type']) || empty($data['message'])) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Missing required fields']);
+ exit;
+ }
+
+ $db = db();
+ $stmt = $db->prepare("INSERT INTO messages (sender_id, sender_type, receiver_id, receiver_type, message) VALUES (?, ?, ?, ?, ?)");
+ if ($stmt->execute([$userId, $userType, $data['receiver_id'], $data['receiver_type'], $data['message']])) {
+ // Send email notification
+ require_once __DIR__ . '/../mail/MailService.php';
+
+ $receiverTable = $data['receiver_type'] === 'coach' ? 'coaches' : 'clients';
+ $stmt = $db->prepare("SELECT email FROM {$receiverTable} WHERE id = ?");
+ $stmt->execute([$data['receiver_id']]);
+ $recipient = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($recipient && !empty($recipient['email'])) {
+ $to = $recipient['email'];
+ $subject = 'You have a new message';
+ $messageBody = 'You have received a new message. Click here to view: View Messages ';
+ MailService::sendMail($to, $subject, $messageBody, strip_tags($messageBody));
+ }
+ echo json_encode(['success' => true]);
+ } else {
+ http_response_code(500);
+ echo json_encode(['error' => 'Failed to send message']);
+ }
+ break;
+
+ default:
+ http_response_code(400);
+ echo json_encode(['error' => 'Invalid action']);
+ break;
+}
diff --git a/api/send_sms.php b/api/send_sms.php
new file mode 100644
index 0000000..7826e49
--- /dev/null
+++ b/api/send_sms.php
@@ -0,0 +1,71 @@
+ 'Method not allowed']);
+ exit;
+}
+
+$client_id = $_POST['client_id'] ?? null;
+$message = $_POST['message'] ?? null;
+
+if (empty($client_id) || empty($message)) {
+ http_response_code(400);
+ echo json_encode(['error' => 'Client ID and message are required']);
+ exit;
+}
+
+// Fetch client phone number
+$stmt = db()->prepare("SELECT phone FROM clients WHERE id = ?");
+$stmt->execute([$client_id]);
+$client = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if (!$client || empty($client['phone'])) {
+ http_response_code(404);
+ echo json_encode(['error' => 'Client not found or phone number is missing']);
+ exit;
+}
+
+$recipient_phone = $client['phone'];
+
+// Initialize Telnyx
+\Telnyx\Telnyx::setApiKey(TELNYX_API_KEY);
+
+try {
+ $response = \Telnyx\Message::create([
+ 'from' => TELNYX_MESSAGING_PROFILE_ID, // Your Telnyx sending number or messaging profile ID
+ 'to' => $recipient_phone,
+ 'text' => $message,
+ ]);
+
+ // Log the message to the database
+ $log_stmt = db()->prepare("INSERT INTO sms_logs (client_id, user_id, sender, recipient, message, status) VALUES (?, ?, ?, ?, ?, ?)");
+ $log_stmt->execute([
+ $client_id,
+ $_SESSION['user_id'], // Assuming admin/coach is logged in
+ TELNYX_MESSAGING_PROFILE_ID,
+ $recipient_phone,
+ $message,
+ 'sent' // or $response->status
+ ]);
+
+ echo json_encode(['success' => true, 'message_id' => $response->id]);
+
+} catch (\Telnyx\Exception\ApiErrorException $e) {
+ http_response_code(500);
+ echo json_encode(['error' => 'Telnyx API error: ' . $e->getMessage()]);
+
+ // Log the failure
+ $log_stmt = db()->prepare("INSERT INTO sms_logs (client_id, user_id, sender, recipient, message, status) VALUES (?, ?, ?, ?, ?, ?)");
+ $log_stmt->execute([
+ $client_id,
+ $_SESSION['user_id'],
+ TELNYX_MESSAGING_PROFILE_ID,
+ $recipient_phone,
+ $message,
+ 'failed'
+ ]);
+}
diff --git a/api/validate_coupon.php b/api/validate_coupon.php
new file mode 100644
index 0000000..fa93a2d
--- /dev/null
+++ b/api/validate_coupon.php
@@ -0,0 +1,62 @@
+ false, 'error' => 'Missing coupon code or package ID']);
+ exit;
+}
+
+// Fetch package details
+$stmt = db()->prepare('SELECT * FROM service_packages WHERE id = ?');
+$stmt->execute([$package_id]);
+$package = $stmt->fetch();
+
+if (!$package) {
+ echo json_encode(['success' => false, 'error' => 'Invalid package']);
+ exit;
+}
+
+// Fetch coupon details
+$stmt = db()->prepare('SELECT * FROM discounts WHERE code = ? AND is_active = 1');
+$stmt->execute([$coupon_code]);
+$coupon = $stmt->fetch();
+
+if (!$coupon) {
+ echo json_encode(['success' => false, 'error' => 'Invalid or inactive coupon']);
+ exit;
+}
+
+// Check date validity
+if (($coupon['start_date'] && strtotime($coupon['start_date']) > time()) || ($coupon['end_date'] && strtotime($coupon['end_date']) < time())) {
+ echo json_encode(['success' => false, 'error' => 'Coupon is not valid at this time']);
+ exit;
+}
+
+// Check usage limit
+if ($coupon['uses_limit'] !== null && $coupon['times_used'] >= $coupon['uses_limit']) {
+ echo json_encode(['success' => false, 'error' => 'Coupon has reached its usage limit']);
+ exit;
+}
+
+// Calculate discounted price
+$original_price = $package['price'];
+$discounted_price = 0;
+
+if ($coupon['type'] === 'percentage') {
+ $discounted_price = $original_price - ($original_price * ($coupon['value'] / 100));
+} else { // fixed
+ $discounted_price = $original_price - $coupon['value'];
+}
+
+if ($discounted_price < 0) {
+ $discounted_price = 0;
+}
+
+echo json_encode(['success' => true, 'discounted_price' => $discounted_price]);
diff --git a/api/validate_subscription_coupon.php b/api/validate_subscription_coupon.php
new file mode 100644
index 0000000..c593671
--- /dev/null
+++ b/api/validate_subscription_coupon.php
@@ -0,0 +1,60 @@
+ false, 'error' => 'Missing coupon code or plan ID']);
+ exit;
+}
+
+global $subscriptions;
+if (!isset($subscriptions[$plan_id])) {
+ echo json_encode(['success' => false, 'error' => 'Invalid plan']);
+ exit;
+}
+$plan = $subscriptions[$plan_id];
+
+// Fetch coupon details
+$stmt = db()->prepare('SELECT * FROM discounts WHERE code = ? AND is_active = 1');
+$stmt->execute([$coupon_code]);
+$coupon = $stmt->fetch();
+
+if (!$coupon) {
+ echo json_encode(['success' => false, 'error' => 'Invalid or inactive coupon']);
+ exit;
+}
+
+// Check date validity
+if (($coupon['start_date'] && strtotime($coupon['start_date']) > time()) || ($coupon['end_date'] && strtotime($coupon['end_date']) < time())) {
+ echo json_encode(['success' => false, 'error' => 'Coupon is not valid at this time']);
+ exit;
+}
+
+// Check usage limit
+if ($coupon['uses_limit'] !== null && $coupon['times_used'] >= $coupon['uses_limit']) {
+ echo json_encode(['success' => false, 'error' => 'Coupon has reached its usage limit']);
+ exit;
+}
+
+// Calculate discounted price
+$original_price = $plan['price'] / 100;
+$discounted_price = 0;
+
+if ($coupon['type'] === 'percentage') {
+ $discounted_price = $original_price - ($original_price * ($coupon['value'] / 100));
+} else { // fixed
+ $discounted_price = $original_price - $coupon['value'];
+}
+
+if ($discounted_price < 0) {
+ $discounted_price = 0;
+}
+
+echo json_encode(['success' => true, 'discounted_price' => $discounted_price]);
diff --git a/approve-booking.php b/approve-booking.php
new file mode 100644
index 0000000..106eddf
--- /dev/null
+++ b/approve-booking.php
@@ -0,0 +1,48 @@
+prepare("SELECT * FROM bookings WHERE id = ? AND coach_id = ?");
+$stmt->execute([$booking_id, $coach_id]);
+$booking = $stmt->fetch();
+
+if ($booking) {
+ $stmt = db()->prepare("UPDATE bookings SET status = 'confirmed' WHERE id = ?");
+ $stmt->execute([$booking_id]);
+
+ // Notify client
+ $stmt = db()->prepare("SELECT c.email, c.name as client_name, co.name as coach_name, b.booking_time FROM bookings b JOIN clients c ON b.client_id = c.id JOIN coaches co ON b.coach_id = co.id WHERE b.id = ?");
+ $stmt->execute([$booking_id]);
+ $booking_details = $stmt->fetch();
+
+ if ($booking_details) {
+ $to = $booking_details['email'];
+ $subject = 'Booking Confirmed';
+ $body = "Hello " . htmlspecialchars($booking_details['client_name']) . ",
";
+ $body .= "Your booking with " . htmlspecialchars($booking_details['coach_name']) . " on " . htmlspecialchars(date('F j, Y, g:i a', strtotime($booking_details['booking_time']))) . " has been confirmed.
";
+ $body .= "Thank you for using CoachConnect.
";
+
+ MailService::sendMail($to, $subject, $body, strip_tags($body));
+ }
+
+ header('Location: dashboard.php?status=approved');
+} else {
+ header('Location: dashboard.php?status=error');
+}
+
+exit;
diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..b5b73b1
--- /dev/null
+++ b/assets/css/custom.css
@@ -0,0 +1,85 @@
+body {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ background-color: #F9FAFB;
+ color: #111827;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: Georgia, 'Times New Roman', Times, serif;
+}
+
+.navbar {
+ background-color: #FFFFFF;
+ border-bottom: 1px solid #E5E7EB;
+}
+
+.hero {
+ background: linear-gradient(45deg, #1E3A8A, #3B82F6);
+ color: #FFFFFF;
+ padding: 100px 0;
+ text-align: center;
+}
+
+.hero h1 {
+ font-size: 3.5rem;
+ font-weight: bold;
+}
+
+.hero p {
+ font-size: 1.25rem;
+ margin-bottom: 30px;
+}
+
+.btn-primary {
+ background-color: #F59E0B;
+ border-color: #F59E0B;
+ padding: 15px 30px;
+ font-size: 1.25rem;
+ border-radius: 0.5rem;
+}
+
+.btn-primary:hover {
+ background-color: #D97706;
+ border-color: #D97706;
+}
+
+.featured-coaches {
+ padding: 80px 0;
+}
+
+.coach-card {
+ background-color: #FFFFFF;
+ border: 1px solid #E5E7EB;
+ border-radius: 0.5rem;
+ padding: 20px;
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+.coach-card img {
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ object-fit: cover;
+ margin-bottom: 20px;
+}
+
+.how-it-works {
+ background-color: #FFFFFF;
+ padding: 80px 0;
+}
+
+.step {
+ text-align: center;
+}
+
+.step .icon {
+ font-size: 3rem;
+ color: #3B82F6;
+}
+
+footer {
+ background-color: #111827;
+ color: #FFFFFF;
+ padding: 40px 0;
+}
diff --git a/assets/js/main.js b/assets/js/main.js
new file mode 100644
index 0000000..0ad547b
--- /dev/null
+++ b/assets/js/main.js
@@ -0,0 +1,9 @@
+document.addEventListener('DOMContentLoaded', function() {
+ const findCoachBtn = document.querySelector('.find-coach-btn');
+ if (findCoachBtn) {
+ findCoachBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ document.querySelector('#featured-coaches').scrollIntoView({ behavior: 'smooth' });
+ });
+ }
+});
diff --git a/book-session.php b/book-session.php
new file mode 100644
index 0000000..6f871fd
--- /dev/null
+++ b/book-session.php
@@ -0,0 +1,108 @@
+prepare(
+ "SELECT cp.id, cp.sessions_remaining, sp.type, sp.client_limit FROM client_packages cp JOIN service_packages sp ON cp.package_id = sp.id WHERE cp.client_id = ? AND sp.coach_id = ? AND cp.sessions_remaining > 0 ORDER BY cp.purchase_date ASC LIMIT 1"
+ );
+ $pkg_stmt->execute([$client_id, $coach_id]);
+ $active_package = $pkg_stmt->fetch();
+
+ if ($active_package) {
+ // Use a session from the package
+ try {
+ $pdo->beginTransaction();
+
+ $is_recurring = isset($_POST['recurring']) && $_POST['recurring'] === 'on';
+ $recurrences = $is_recurring ? (int)$_POST['recurrences'] : 1;
+ $frequency = $is_recurring ? $_POST['frequency'] : 'weekly'; // Default to weekly
+
+ if ($active_package['sessions_remaining'] < $recurrences) {
+ throw new Exception('Not enough sessions remaining in the package.');
+ }
+
+ $current_booking_time = new DateTime($booking_time);
+
+ for ($i = 0; $i < $recurrences; $i++) {
+ if ($active_package['type'] === 'group') {
+ $count_stmt = $pdo->prepare("SELECT COUNT(*) FROM bookings WHERE coach_id = ? AND booking_time = ? AND status IN ('pending', 'confirmed')");
+ $count_stmt->execute([$coach_id, $current_booking_time->format('Y-m-d H:i:s')]);
+ $current_bookings = $count_stmt->fetchColumn();
+
+ if ($current_bookings >= $active_package['client_limit']) {
+ throw new Exception('This group session is already full.');
+ }
+ }
+
+ // 1. Create the booking
+ $book_stmt = $pdo->prepare(
+ "INSERT INTO bookings (coach_id, client_id, booking_time, status, payment_status) VALUES (?, ?, ?, 'pending', 'paid_with_package')"
+ );
+ $book_stmt->execute([$coach_id, $client_id, $current_booking_time->format('Y-m-d H:i:s')]);
+
+ // 2. Decrement remaining sessions
+ $update_pkg_stmt = $pdo->prepare("UPDATE client_packages SET sessions_remaining = sessions_remaining - 1 WHERE id = ?");
+ $update_pkg_stmt->execute([$active_package['id']]);
+
+ // Calculate next booking time
+ if ($i < $recurrences - 1) {
+ if ($frequency === 'weekly') {
+ $current_booking_time->modify('+1 week');
+ } elseif ($frequency === 'bi-weekly') {
+ $current_booking_time->modify('+2 weeks');
+ }
+ }
+ }
+
+ $pdo->commit();
+
+ // Send email notification to coach
+ require_once __DIR__ . '/mail/MailService.php';
+ $coach_stmt = $pdo->prepare("SELECT email FROM coaches WHERE id = ?");
+ $coach_stmt->execute([$coach_id]);
+ $coach = $coach_stmt->fetch();
+
+ $client_stmt = $pdo->prepare("SELECT name FROM clients WHERE id = ?");
+ $client_stmt->execute([$client_id]);
+ $client = $client_stmt->fetch();
+
+ if ($coach && $client) {
+ $to = $coach['email'];
+ $subject = 'New Pending Booking';
+ $message = "You have a new booking from {$client['name']} for {$booking_time}. Please log in to your dashboard to approve or decline it.";
+ MailService::sendMail($to, $subject, $message, $message);
+ }
+
+ header('Location: dashboard.php?booking=pending');
+ exit;
+
+ } catch (Exception $e) {
+ $pdo->rollBack();
+ error_log('Package booking failed: ' . $e->getMessage());
+ header('Location: profile.php?id=' . $coach_id . '&error=booking_failed');
+ exit;
+ }
+
+ } else {
+ // No active package, redirect to coach's profile to purchase one
+ header('Location: profile.php?id=' . $coach_id . '&error=no_package');
+ exit;
+ }
+} else {
+ header('Location: coaches.php');
+ exit;
+}
\ No newline at end of file
diff --git a/cancel-booking.php b/cancel-booking.php
new file mode 100644
index 0000000..581e8f5
--- /dev/null
+++ b/cancel-booking.php
@@ -0,0 +1,72 @@
+prepare("SELECT * FROM bookings WHERE id = ? AND client_id = ?");
+$stmt->execute([$booking_id, $client_id]);
+$booking = $stmt->fetch();
+
+if ($booking) {
+ if (!empty($booking['stripe_payment_intent_id'])) {
+ try {
+ $refund = \Stripe\Refund::create([
+ 'payment_intent' => $booking['stripe_payment_intent_id'],
+ ]);
+
+ if ($refund->status == 'succeeded') {
+ // Optionally, you can store the refund ID in your database
+ $stmt = db()->prepare("UPDATE bookings SET status = 'cancelled', payment_status = 'refunded' WHERE id = ?");
+ $stmt->execute([$booking_id]);
+ } else {
+ // Handle refund failure
+ header('Location: dashboard.php?status=error');
+ exit;
+ }
+ } catch (\Stripe\Exception\ApiErrorException $e) {
+ // Handle Stripe API errors
+ error_log('Stripe API error: ' . $e->getMessage());
+ header('Location: dashboard.php?status=error');
+ exit;
+ }
+ } else {
+ $stmt = db()->prepare("UPDATE bookings SET status = 'cancelled' WHERE id = ?");
+ $stmt->execute([$booking_id]);
+ }
+
+ // Notify coach
+ $stmt = db()->prepare("SELECT co.email, co.name as coach_name, c.name as client_name, b.booking_time FROM bookings b JOIN coaches co ON b.coach_id = co.id JOIN clients c ON b.client_id = c.id WHERE b.id = ?");
+ $stmt->execute([$booking_id]);
+ $booking_details = $stmt->fetch();
+
+ if ($booking_details) {
+ $to = $booking_details['email'];
+ $subject = 'Booking Cancellation & Refund';
+ $body = "Hello " . htmlspecialchars($booking_details['coach_name']) . ",
";
+ $body .= "The booking with " . htmlspecialchars($booking_details['client_name']) . " on " . htmlspecialchars(date('F j, Y, g:i a', strtotime($booking_details['booking_time']))) . " has been cancelled and a refund has been issued.
";
+ $body .= "Thank you for using CoachConnect.
";
+
+ MailService::sendMail($to, $subject, $body, strip_tags($body));
+ }
+
+ header('Location: dashboard.php?status=cancelled');
+} else {
+ header('Location: dashboard.php?status=error');
+}
+
+exit;
diff --git a/checkout.php b/checkout.php
new file mode 100644
index 0000000..3419fe1
--- /dev/null
+++ b/checkout.php
@@ -0,0 +1,99 @@
+prepare('SELECT * FROM service_packages WHERE id = ?');
+$stmt->execute([$package_id]);
+$package = $stmt->fetch();
+
+if (!$package) {
+ header('Location: coaches.php?error=invalid_package');
+ exit;
+}
+?>
+
+
+
+
+
+
diff --git a/coaches.php b/coaches.php
new file mode 100644
index 0000000..89f17cc
--- /dev/null
+++ b/coaches.php
@@ -0,0 +1,183 @@
+query('SELECT * FROM settings');
+$settings = $settings_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
+$coach_mode = $settings['coach_mode'] ?? 'multi';
+$single_coach_id = $settings['single_coach_id'] ?? null;
+
+if ($coach_mode === 'single' && !empty($single_coach_id)) {
+ header('Location: profile.php?id=' . $single_coach_id);
+ exit;
+}
+
+// Pagination
+$limit = 6; // Number of coaches per page
+$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;
+$offset = ($page - 1) * $limit;
+
+try {
+ // Get total number of coaches for pagination
+ $total_coaches_stmt = db()->query('SELECT count(*) FROM coaches');
+ $total_coaches = $total_coaches_stmt->fetchColumn();
+ $total_pages = ceil($total_coaches / $limit);
+
+ // Fetch coaches for the current page
+ $stmt = db()->prepare('SELECT id, name, specialties, bio, photo_url FROM coaches ORDER BY name ASC LIMIT :limit OFFSET :offset');
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
+ $stmt->execute();
+ $coaches = $stmt->fetchAll(PDO::FETCH_ASSOC);
+} catch (PDOException $e) {
+ // It's a good practice to log the error and show a user-friendly message
+ error_log($e->getMessage());
+ $coaches = [];
+ $total_pages = 0;
+ // For a real application, you might want to redirect to an error page
+}
+
+?>
+
+
+
+
+
+ Browse Coaches
+
+
+
+
+
+
+
+
+
+
+ Meet Our Coaches
+
+
+
+
+ prepare('SELECT * FROM service_packages WHERE coach_id = ?');
+ $package_stmt->execute([$coach['id']]);
+ $packages = $package_stmt->fetchAll(PDO::FETCH_ASSOC);
+ ?>
+
+
+
+
= htmlspecialchars($coach['name']) ?>
+
= htmlspecialchars($coach['specialties']) ?>
+
+
= htmlspecialchars(substr($coach['bio'], 0, 100)) ?>...
+
+
View Profile
+
+
+
+
Coaching Packages
+
+
+ prepare('SELECT SUM(quantity) as total_sessions FROM package_service_items WHERE package_id = ? AND service_type IN ("one_on_one", "group_session")');
+ $items_stmt->execute([$package['id']]);
+ $items_result = $items_stmt->fetch();
+ $total_sessions = $items_result['total_sessions'] ?? 0;
+ ?>
+
+
= htmlspecialchars($package['name']) ?>
+
= htmlspecialchars($package['description']) ?>
+
+
+
+
+ $= htmlspecialchars(number_format($package['price'], 2)) ?>
+
+ $= htmlspecialchars(number_format($package['price'], 2)) ?> / = htmlspecialchars($package['payment_plan_interval']) ?> for = htmlspecialchars($package['payment_plan_installments']) ?> installments
+
+
+ 0): ?>
+ for = htmlspecialchars($total_sessions) ?> sessions
+
+
+
Purchase
+
Purchase as Gift
+
+
+
+ Type: = htmlspecialchars(ucfirst(str_replace('_', ' ', $package['type']))) ?>
+
+ | Client Limit: = htmlspecialchars($package['client_limit']) ?>
+
+
+ | Starts: = htmlspecialchars(date('M j, Y', strtotime($package['start_date']))) ?>
+
+
+ | Ends: = htmlspecialchars(date('M j, Y', strtotime($package['end_date']))) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
© = date("Y") ?> Coaching Platform. All rights reserved.
+
+
+
+
+
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..6cf0dfe
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,7 @@
+{
+ "require": {
+ "stripe/stripe-php": "^1.8",
+ "docusealco/docuseal-php": "^1.0",
+ "telnyx/telnyx-php": "*"
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..65f892d
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,365 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "b63113b587353672ca1085f160a5b41e",
+ "packages": [
+ {
+ "name": "docusealco/docuseal-php",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/docusealco/docuseal-php.git",
+ "reference": "f12a490e95bdb13ef61f46b72dffcdc646d8b0a4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/docusealco/docuseal-php/zipball/f12a490e95bdb13ef61f46b72dffcdc646d8b0a4",
+ "reference": "f12a490e95bdb13ef61f46b72dffcdc646d8b0a4",
+ "shasum": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Docuseal\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "DocuSeal"
+ }
+ ],
+ "description": "PHP bindings for DocuSeal API",
+ "support": {
+ "issues": "https://github.com/docusealco/docuseal-php/issues",
+ "source": "https://github.com/docusealco/docuseal-php/tree/1.0.5"
+ },
+ "time": "2025-09-14T21:27:12+00:00"
+ },
+ {
+ "name": "php-http/discovery",
+ "version": "1.20.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/discovery.git",
+ "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d",
+ "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0|^2.0",
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "nyholm/psr7": "<1.0",
+ "zendframework/zend-diactoros": "*"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "*",
+ "psr/http-factory-implementation": "*",
+ "psr/http-message-implementation": "*"
+ },
+ "require-dev": {
+ "composer/composer": "^1.0.2|^2.0",
+ "graham-campbell/phpspec-skip-example-extension": "^5.0",
+ "php-http/httplug": "^1.0 || ^2.0",
+ "php-http/message-factory": "^1.0",
+ "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
+ "sebastian/comparator": "^3.0.5 || ^4.0.8",
+ "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Http\\Discovery\\Composer\\Plugin",
+ "plugin-optional": true
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Discovery\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "src/Composer/Plugin.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "adapter",
+ "client",
+ "discovery",
+ "factory",
+ "http",
+ "message",
+ "psr17",
+ "psr7"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/discovery/issues",
+ "source": "https://github.com/php-http/discovery/tree/1.20.0"
+ },
+ "time": "2024-10-02T11:20:13+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "stripe/stripe-php",
+ "version": "v1.18.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/stripe/stripe-php.git",
+ "reference": "022c3f21ec1e4141b46738bd5e7ab730d04f78cc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/stripe/stripe-php/zipball/022c3f21ec1e4141b46738bd5e7ab730d04f78cc",
+ "reference": "022c3f21ec1e4141b46738bd5e7ab730d04f78cc",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "php": ">=5.2"
+ },
+ "require-dev": {
+ "simpletest/simpletest": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "lib/Stripe/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Stripe and contributors",
+ "homepage": "https://github.com/stripe/stripe-php/contributors"
+ }
+ ],
+ "description": "Stripe PHP Library",
+ "homepage": "https://stripe.com/",
+ "keywords": [
+ "api",
+ "payment processing",
+ "stripe"
+ ],
+ "support": {
+ "issues": "https://github.com/stripe/stripe-php/issues",
+ "source": "https://github.com/stripe/stripe-php/tree/v1.18.0"
+ },
+ "time": "2015-01-22T05:01:46+00:00"
+ },
+ {
+ "name": "telnyx/telnyx-php",
+ "version": "v5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/team-telnyx/telnyx-php.git",
+ "reference": "0e6da9d1afe3bb9844079b163a207c4546b49d36"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/team-telnyx/telnyx-php/zipball/0e6da9d1afe3bb9844079b163a207c4546b49d36",
+ "reference": "0e6da9d1afe3bb9844079b163a207c4546b49d36",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1",
+ "php-http/discovery": "^1",
+ "psr/http-client": "^1",
+ "psr/http-client-implementation": "^1",
+ "psr/http-factory-implementation": "^1",
+ "psr/http-message": "^1|^2"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3",
+ "nyholm/psr7": "^1",
+ "pestphp/pest": "^3",
+ "phpstan/extension-installer": "^1",
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-phpunit": "^2",
+ "phpunit/phpunit": "^11",
+ "symfony/http-client": "^7"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Core.php",
+ "src/Client.php"
+ ],
+ "psr-4": {
+ "Telnyx\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Official Telnyx PHP SDK — APIs for Voice, SMS, MMS, WhatsApp, Fax, SIP Trunking, Wireless IoT, Call Control, and more. Build global communications on Telnyx's private carrier-grade network.",
+ "keywords": [
+ "api",
+ "call control",
+ "communications",
+ "connectivity",
+ "emergency services",
+ "esim",
+ "fax",
+ "iot",
+ "mms",
+ "numbers",
+ "porting",
+ "sip",
+ "sms",
+ "telephony",
+ "telnyx",
+ "trunking",
+ "voice",
+ "voip",
+ "whatsapp"
+ ],
+ "support": {
+ "issues": "https://github.com/team-telnyx/telnyx-php/issues",
+ "source": "https://github.com/team-telnyx/telnyx-php/tree/v5.0.0"
+ },
+ "time": "2025-10-27T22:06:49+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {},
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {},
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/content.php b/content.php
new file mode 100644
index 0000000..493e5d1
--- /dev/null
+++ b/content.php
@@ -0,0 +1,68 @@
+prepare(
+ 'SELECT c.id, c.title, c.description, c.file_path, c.file_type, 'now()' as available_at FROM content c JOIN client_content cc ON c.id = cc.content_id WHERE cc.client_id = ?'
+);
+$direct_content_stmt->execute([$client_id]);
+$direct_content = $direct_content_stmt->fetchAll();
+
+// Fetch content assigned via packages
+$package_content_stmt = db()->prepare(
+ 'SELECT c.id, c.title, c.description, c.file_path, c.file_type, DATE_ADD(cp.purchase_date, INTERVAL pc.delay_days DAY) as available_at FROM content c JOIN package_content pc ON c.id = pc.content_id JOIN client_packages cp ON pc.package_id = cp.package_id WHERE cp.client_id = ?'
+);
+$package_content_stmt->execute([$client_id]);
+$package_content = $package_content_stmt->fetchAll();
+
+$all_content = array_merge($direct_content, $package_content);
+
+// Filter out duplicates and unavailable content
+$displayed_content_ids = [];
+$final_content_list = [];
+$current_time = new DateTime();
+
+foreach ($all_content as $content) {
+ $available_at = new DateTime($content['available_at']);
+ if (!in_array($content['id'], $displayed_content_ids) && $current_time >= $available_at) {
+ $final_content_list[] = $content;
+ $displayed_content_ids[] = $content['id'];
+ }
+}
+
+?>
+
+
+
My Content
+
Here you can find all the materials shared with you by your coach.
+
+
+
+
+
No content has been assigned to you yet.
+
+
+
+
+
+
+
+
+
+
diff --git a/create-checkout-session.php b/create-checkout-session.php
new file mode 100644
index 0000000..119121d
--- /dev/null
+++ b/create-checkout-session.php
@@ -0,0 +1,146 @@
+ 'Unauthorized']);
+ exit;
+}
+
+// --- 1. Get Input Data ---
+$package_id = $_POST['package_id'] ?? null;
+$coupon_code = $_POST['coupon_code'] ?? null;
+$is_gift = isset($_POST['is_gift']) && $_POST['is_gift'] === 'true';
+$payment_option = $_POST['payment_option'] ?? 'pay_full';
+$client_id = $_SESSION['user_id'];
+
+if (!$package_id) {
+ header('Location: coaches.php?error=missing_package');
+ exit;
+}
+
+// --- 2. Fetch Package Details ---
+$stmt = db()->prepare('SELECT * FROM service_packages WHERE id = ?');
+$stmt->execute([$package_id]);
+$package = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if (!$package) {
+ header('Location: coaches.php?error=invalid_package');
+ exit;
+}
+
+// --- 3. Determine Price, Mode, and Metadata ---
+$line_items = [];
+$metadata = [
+ 'package_id' => $package_id,
+ 'client_id' => $client_id,
+ 'is_gift' => $is_gift ? 'true' : 'false',
+ 'payment_option' => $payment_option
+];
+$mode = 'payment'; // Default to one-time payment
+
+$unit_amount = 0;
+$product_name = $package['name'];
+
+if ($package['payment_type'] === 'payment_plan' && $payment_option === 'payment_plan') {
+ // --- PAYMENT PLAN ---
+ if ($package['deposit_amount'] > 0) {
+ // A. Plan with a deposit: Create a one-time payment for the deposit.
+ // Webhook will handle creating the subscription for the rest.
+ $unit_amount = $package['deposit_amount'] * 100;
+ $product_name .= ' - Deposit';
+ $metadata['payment_plan_action'] = 'start_subscription_after_deposit';
+ } else {
+ // B. Plan with no deposit: Create a subscription directly.
+ $mode = 'subscription';
+ $unit_amount = ($package['price'] * 100) / $package['installments'];
+ $line_items[] = [
+ 'price_data' => [
+ 'currency' => 'usd',
+ 'product_data' => ['name' => $product_name],
+ 'unit_amount' => $unit_amount,
+ 'recurring' => [
+ 'interval' => $package['installment_interval'],
+ 'interval_count' => 1,
+ ],
+ ],
+ 'quantity' => 1,
+ ];
+ }
+} else {
+ // --- PAY IN FULL (or one-time package) ---
+ $unit_amount = $package['price'] * 100;
+ if ($package['payment_type'] === 'payment_plan' && !empty($package['pay_in_full_discount_percentage'])) {
+ $discount = $unit_amount * ($package['pay_in_full_discount_percentage'] / 100);
+ $unit_amount -= $discount;
+ $metadata['discount_applied'] = 'pay_in_full';
+ }
+}
+
+// Add the primary line item if not already added (for non-subscription cases)
+if (empty($line_items)) {
+ $line_items[] = [
+ 'price_data' => [
+ 'currency' => 'usd',
+ 'product_data' => ['name' => $product_name],
+ 'unit_amount' => round($unit_amount, 0),
+ ],
+ 'quantity' => 1,
+ ];
+}
+
+
+// --- 4. Handle Coupon ---
+$discounts = [];
+if ($coupon_code) {
+ $stmt = db()->prepare('SELECT * FROM discounts WHERE code = ? AND is_active = 1');
+ $stmt->execute([$coupon_code]);
+ $coupon = $stmt->fetch();
+
+ $is_valid = $coupon &&
+ (!$coupon['start_date'] || strtotime($coupon['start_date']) <= time()) &&
+ (!$coupon['end_date'] || strtotime($coupon['end_date']) >= time()) &&
+ ($coupon['uses_limit'] === null || $coupon['times_used'] < $coupon['uses_limit']);
+
+ if ($is_valid) {
+ try {
+ $stripe_coupon_params = ['duration' => 'once', 'name' => $coupon['code']];
+ if ($coupon['type'] === 'percentage') {
+ $stripe_coupon_params['percent_off'] = $coupon['value'];
+ } else {
+ $stripe_coupon_params['amount_off'] = $coupon['value'] * 100;
+ $stripe_coupon_params['currency'] = 'usd';
+ }
+ $stripe_coupon = \Stripe\Coupon::create($stripe_coupon_params);
+ $discounts[] = ['coupon' => $stripe_coupon->id];
+ $metadata['coupon_code'] = $coupon_code;
+ } catch (\Exception $e) { /* Ignore Stripe error, proceed without coupon */ }
+ }
+}
+
+// --- 5. Define Success/Cancel URLs ---
+$success_url = 'http://' . $_SERVER['HTTP_HOST'] . ($is_gift ? '/purchase-gift-success.php' : '/purchase-package-success.php') . '?session_id={CHECKOUT_SESSION_ID}';
+$cancel_url = 'http://' . $_SERVER['HTTP_HOST'] . '/purchase-package-cancel.php';
+
+// --- 6. Create and Redirect to Checkout ---
+try {
+ $checkout_session = \Stripe\Checkout\Session::create([
+ 'payment_method_types' => ['card'],
+ 'line_items' => $line_items,
+ 'mode' => $mode,
+ 'success_url' => $success_url,
+ 'cancel_url' => $cancel_url,
+ 'metadata' => $metadata,
+ 'discounts' => $discounts,
+ ]);
+
+ header("Location: " . $checkout_session->url);
+ exit;
+} catch (\Exception $e) {
+ $error_message = urlencode($e->getMessage());
+ header("Location: purchase-package.php?package_id={$package_id}&error=stripe_error&message={$error_message}");
+ exit;
+}
\ No newline at end of file
diff --git a/create-portal-session.php b/create-portal-session.php
new file mode 100644
index 0000000..e801c7b
--- /dev/null
+++ b/create-portal-session.php
@@ -0,0 +1,31 @@
+prepare("SELECT stripe_customer_id FROM clients WHERE id = ?");
+$stmt->execute([$user_id]);
+$client = $stmt->fetch();
+
+if (!$client || !$client['stripe_customer_id']) {
+ // This should not happen if the user has a subscription
+ header('Location: subscribe.php');
+ exit;
+}
+
+$return_url = 'http://' . $_SERVER['HTTP_HOST'] . '/manage-subscription.php';
+
+$portalSession = \Stripe\BillingPortal\Session::create([
+ 'customer' => $client['stripe_customer_id'],
+ 'return_url' => $return_url,
+]);
+
+header("Location: " . $portalSession->url);
+exit();
diff --git a/create-subscription-checkout-session.php b/create-subscription-checkout-session.php
new file mode 100644
index 0000000..dc5b496
--- /dev/null
+++ b/create-subscription-checkout-session.php
@@ -0,0 +1,117 @@
+prepare('SELECT * FROM discounts WHERE code = ? AND is_active = 1');
+ $stmt->execute([$coupon_code]);
+ $coupon = $stmt->fetch();
+
+ if ($coupon) {
+ // Check date validity and usage limit (already done in previous step, but good to double check)
+ // ...
+
+ try {
+ $stripe_coupon_params = [];
+ if ($coupon['type'] === 'percentage') {
+ $stripe_coupon_params['percent_off'] = $coupon['value'];
+ } else { // fixed
+ $stripe_coupon_params['amount_off'] = $coupon['value'] * 100;
+ $stripe_coupon_params['currency'] = 'usd';
+ }
+ $stripe_coupon_params['duration'] = 'once'; // Or 'repeating', 'forever'
+ $stripe_coupon_params['name'] = $coupon['code'];
+
+ $stripe_coupon = \Stripe\Coupon::create($stripe_coupon_params);
+ $stripe_coupon_id = $stripe_coupon->id;
+ } catch (\Stripe\Exception\ApiErrorException $e) {
+ // Coupon creation failed, proceed without discount
+ }
+ }
+}
+
+// Get client's stripe customer id or create a new one
+$stmt = db()->prepare("SELECT stripe_customer_id, email, name FROM clients WHERE id = ?");
+$stmt->execute([$client_id]);
+$client = $stmt->fetch();
+
+$stripe_customer_id = $client['stripe_customer_id'];
+if (!$stripe_customer_id) {
+ $customer = \Stripe\Customer::create([
+ 'email' => $client['email'],
+ 'name' => $client['name'],
+ ]);
+ $stripe_customer_id = $customer->id;
+ $update_stmt = db()->prepare("UPDATE clients SET stripe_customer_id = ? WHERE id = ?");
+ $update_stmt->execute([$stripe_customer_id, $client_id]);
+}
+
+// Create a Stripe Checkout Session
+try {
+ $checkout_params = [
+ 'payment_method_types' => ['card'],
+ 'line_items' => [[
+ 'price_data' => [
+ 'currency' => $plan['currency'],
+ 'product_data' => [
+ 'name' => $plan['name'],
+ ],
+ 'unit_amount' => $plan['price'],
+ 'recurring' => [
+ 'interval' => $plan['interval'],
+ ],
+ ],
+ 'quantity' => 1,
+ ]],
+ 'mode' => 'subscription',
+ 'success_url' => 'http://' . $_SERVER['HTTP_HOST'] . '/subscription-success.php?session_id={CHECKOUT_SESSION_ID}',
+ 'cancel_url' => 'http://' . $_SERVER['HTTP_HOST'] . '/subscription-cancel.php',
+ 'client_reference_id' => $client_id,
+ 'customer' => $stripe_customer_id,
+ ];
+
+ if ($stripe_coupon_id) {
+ $checkout_params['discounts'] = [['coupon' => $stripe_coupon_id]];
+ }
+
+ $checkout_session = \Stripe\Checkout\Session::create($checkout_params);
+
+ header("HTTP/1.1 303 See Other");
+ header("Location: " . $checkout_session->url);
+ exit;
+
+} catch (\Stripe\Exception\ApiErrorException $e) {
+ header('Location: subscribe-checkout.php?plan_id='. $plan_id .'&error=stripe_error&message=' . urlencode($e->getMessage()));
+ exit;
+} catch (Exception $e) {
+ header('Location: subscribe-checkout.php?plan_id=' . $plan_id .'&error=generic_error&message=' . urlencode($e->getMessage()));
+ exit;
+}
diff --git a/cron/send_reminders.php b/cron/send_reminders.php
new file mode 100644
index 0000000..73e4a24
--- /dev/null
+++ b/cron/send_reminders.php
@@ -0,0 +1,54 @@
+prepare("SELECT b.*, c.name as client_name, c.phone as client_phone FROM bookings b JOIN clients c ON b.client_id = c.id WHERE b.start_time BETWEEN NOW() AND NOW() + INTERVAL 24 HOUR AND b.status = 'confirmed'");
+$stmt->execute();
+$bookings = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+foreach ($bookings as $booking) {
+ if (empty($booking['client_phone'])) {
+ continue;
+ }
+
+ $message = "Hi " . $booking['client_name'] . ", this is a reminder for your upcoming session tomorrow at " . date("g:i a", strtotime($booking['start_time'])) . ".";
+
+ try {
+ $response = \Telnyx\Message::create([
+ 'from' => TELNYX_MESSAGING_PROFILE_ID,
+ 'to' => $booking['client_phone'],
+ 'text' => $message,
+ ]);
+
+ // Log the message
+ $log_stmt = db()->prepare("INSERT INTO sms_logs (client_id, sender, recipient, message, status) VALUES (?, ?, ?, ?, ?, ?)");
+ $log_stmt->execute([
+ $booking['client_id'],
+ TELNYX_MESSAGING_PROFILE_ID,
+ $booking['client_phone'],
+ $message,
+ 'sent'
+ ]);
+
+ echo "Reminder sent to " . $booking['client_name'] . "\n";
+
+ } catch (\Telnyx\Exception\ApiErrorException $e) {
+ echo "Error sending reminder to " . $booking['client_name'] . ": " . $e->getMessage() . "\n";
+
+ // Log the failure
+ $log_stmt = db()->prepare("INSERT INTO sms_logs (client_id, sender, recipient, message, status) VALUES (?, ?, ?, ?, ?, ?)");
+ $log_stmt->execute([
+ $booking['client_id'],
+ TELNYX_MESSAGING_PROFILE_ID,
+ $booking['client_phone'],
+ $message,
+ 'failed'
+ ]);
+ }
+}
+
diff --git a/dashboard.php b/dashboard.php
new file mode 100644
index 0000000..8f55658
--- /dev/null
+++ b/dashboard.php
@@ -0,0 +1,497 @@
+query('SELECT setting_key, setting_value FROM settings');
+$settings = $settings_stmt->fetchAll(PDO::FETCH_KEY_PAIR);
+$coach_mode = $settings['coach_mode'] ?? 'multi';
+
+if (!isset($_SESSION['user_id'])) {
+ header('Location: login.php');
+ exit;
+}
+
+$user_id = $_SESSION['user_id'];
+$user_role = $_SESSION['user_role'];
+$bookings = [];
+$existing_reviews = [];
+$subscription = null;
+
+// Update status of past bookings to 'completed'
+$update_stmt = db()->prepare("UPDATE bookings SET status = 'completed' WHERE status = 'confirmed' AND booking_time < NOW()");
+$update_stmt->execute();
+
+if ($user_role === 'client') {
+ $stmt = db()->prepare("SELECT b.*, c.name as coach_name FROM bookings b JOIN coaches c ON b.coach_id = c.id WHERE b.client_id = ? ORDER BY b.booking_time DESC");
+ $stmt->execute([$user_id]);
+ $bookings = $stmt->fetchAll();
+
+ $review_stmt = db()->prepare("SELECT booking_id FROM reviews WHERE client_id = ?");
+ $review_stmt->execute([$user_id]);
+ $existing_reviews = $review_stmt->fetchAll(PDO::FETCH_COLUMN);
+
+ $sub_stmt = db()->prepare("SELECT * FROM client_subscriptions WHERE client_id = ? AND (status = 'active' OR status = 'trialing') ORDER BY created_at DESC LIMIT 1");
+ $sub_stmt->execute([$user_id]);
+ $subscription = $sub_stmt->fetch();
+
+ $client_stmt = db()->prepare("SELECT timezone FROM clients WHERE id = ?");
+ $client_stmt->execute([$user_id]);
+ $client = $client_stmt->fetch();
+
+} elseif ($user_role === 'coach') {
+ $stmt = db()->prepare("SELECT b.*, c.name as client_name FROM bookings b JOIN clients c ON b.client_id = c.id WHERE b.coach_id = ? ORDER BY b.booking_time DESC");
+ $stmt->execute([$user_id]);
+ $bookings = $stmt->fetchAll();
+
+ $coach_stmt = db()->prepare("SELECT buffer_time, timezone FROM coaches WHERE id = ?");
+ $coach_stmt->execute([$user_id]);
+ $coach = $coach_stmt->fetch();
+}
+
+?>
+
+
+
+
+
+ Dashboard - CoachConnect
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Your booking is pending approval.
+
You will be notified once the coach confirms the booking.
+
+
+
+
+
+
+
Manage Your One-Off Availability
+
+
+
+
+ Add Availability
+
+
+
+
+
+
Manage Your Recurring Weekly Availability
+
+
+
+ Day of Week
+
+ Monday
+ Tuesday
+ Wednesday
+ Thursday
+ Friday
+ Saturday
+ Sunday
+
+
+
+ Start Time
+
+
+
+ End Time
+
+
+
+
+
+ Add Recurring Availability
+
+
+
+
+
+
Manage Your Settings
+
+
+
+ Buffer Time (minutes)
+
+
+
+ Timezone
+
+ ' . htmlspecialchars($tz) . '';
+ }
+ ?>
+
+
+
+
+
+ Update Settings
+
+
+
+
+
+
Manage Service Packages
+
Create, view, and manage your coaching packages.
+
Manage Packages
+
+
+
Manage Your Portfolio
+
Update your bio, specialties, and upload media to showcase your work.
+
Edit Portfolio
+
+
+
Admin Settings
+
Configure site-wide settings and modes.
+
Go to Settings
+
+
+
+
+
Manage Your Settings
+
+
+
+ Timezone
+
+ ' . htmlspecialchars($tz) . '';
+ }
+ ?>
+
+
+
+
+
+ Update Settings
+
+
+
+
+
+
Your Packages
+ prepare("SELECT cp.sessions_remaining, sp.name FROM client_packages cp JOIN service_packages sp ON cp.package_id = sp.id WHERE cp.client_id = ?");
+ $packages_stmt->execute([$user_id]);
+ $client_packages = $packages_stmt->fetchAll();
+ ?>
+
+
+
+ = htmlspecialchars($pkg['name']) ?>: = htmlspecialchars($pkg['sessions_remaining']) ?> sessions remaining
+
+
+
+
You have not purchased any packages yet.
+
Browse Packages
+
+
+
+
+
Your Subscriptions
+ prepare("SELECT cs.*, sp.name FROM client_subscriptions cs JOIN service_packages sp ON cs.package_id = sp.id WHERE cs.client_id = ? ORDER BY cs.created_at DESC");
+ $subs_stmt->execute([$user_id]);
+ $subscriptions = $subs_stmt->fetchAll();
+ ?>
+
+
+
+ = htmlspecialchars($sub['name']) ?>: = htmlspecialchars(ucfirst($sub['status'])) ?>
+
+
+
Manage Subscriptions
+
+
You do not have any active subscriptions.
+
+
+
+
+ Your Bookings
+
+
+
+
+
+
+
+
+
+ Success!
+ Your booking request has been sent.
+
+
+
+
+
+
+
+
+
+ Start Time
+
+
+ End Time
+
+
+
+
+
+ prepare("SELECT * FROM coach_availability WHERE coach_id = ? ORDER BY start_time DESC");
+ $stmt->execute([$user_id]);
+ $availability = $stmt->fetchAll();
+ }
+
+ if (empty($availability)):
+ ?>
+
+ No availability set.
+
+
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
Your Recurring Weekly Availability
+
+
+
+
+ Day of Week
+
+
+ Start Time
+
+
+ End Time
+
+
+
+
+
+ prepare("SELECT * FROM coach_recurring_availability WHERE coach_id = ? ORDER BY day_of_week, start_time");
+ $stmt->execute([$user_id]);
+ $recurring_availability = $stmt->fetchAll();
+ $days_of_week = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ }
+
+ if (empty($recurring_availability)):
+ ?>
+
+ No recurring availability set.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Booking Time
+
+
+ Status
+
+
+
+
+
+
+
+ No bookings found.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Approve
+ Decline
+
+ Cancel
+
+ Review
+
+ Refund
+
+
+
+
+
+
+
+
+
+
+
+
+
+
© CoachConnect. All rights reserved.
+
+
+
+
+
diff --git a/db/migrate.php b/db/migrate.php
new file mode 100644
index 0000000..37b0bfe
--- /dev/null
+++ b/db/migrate.php
@@ -0,0 +1,43 @@
+exec('CREATE TABLE IF NOT EXISTS migrations (migration VARCHAR(255) PRIMARY KEY)');
+ } catch (PDOException $e) {
+ die("Could not create migrations table: " . $e->getMessage() . "\n");
+ }
+
+ // Get all migration files
+ $migrations_dir = __DIR__ . '/migrations';
+ $files = glob($migrations_dir . '/*.sql');
+
+ // Get already run migrations
+ $ran_migrations_stmt = $pdo->query('SELECT migration FROM migrations');
+ $ran_migrations = $ran_migrations_stmt->fetchAll(PDO::FETCH_COLUMN);
+
+ foreach ($files as $file) {
+ $migration_name = basename($file);
+
+ if (in_array($migration_name, $ran_migrations)) {
+ continue; // Skip already run migration
+ }
+
+ $sql = file_get_contents($file);
+ try {
+ $pdo->exec($sql);
+ $stmt = $pdo->prepare('INSERT INTO migrations (migration) VALUES (?)');
+ $stmt->execute([$migration_name]);
+ echo "Migration from $file ran successfully.\n";
+ } catch (PDOException $e) {
+ die("Migration failed on file $file: " . $e->getMessage() . "\n");
+ }
+ }
+ echo "All new migrations have been run.\n";
+}
+
+run_migrations();
+
diff --git a/db/migrations/001_create_coaches_table.sql b/db/migrations/001_create_coaches_table.sql
new file mode 100644
index 0000000..738d1db
--- /dev/null
+++ b/db/migrations/001_create_coaches_table.sql
@@ -0,0 +1,8 @@
+CREATE TABLE IF NOT EXISTS coaches (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ specialty VARCHAR(255) NOT NULL,
+ bio TEXT,
+ photo_url VARCHAR(255),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/db/migrations/002_create_clients_table.sql b/db/migrations/002_create_clients_table.sql
new file mode 100644
index 0000000..00f4960
--- /dev/null
+++ b/db/migrations/002_create_clients_table.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS clients (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/db/migrations/003_create_bookings_table.sql b/db/migrations/003_create_bookings_table.sql
new file mode 100644
index 0000000..b084799
--- /dev/null
+++ b/db/migrations/003_create_bookings_table.sql
@@ -0,0 +1,10 @@
+CREATE TABLE IF NOT EXISTS bookings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ client_id INT NOT NULL,
+ coach_id INT NOT NULL,
+ booking_time DATETIME NOT NULL,
+ status VARCHAR(255) NOT NULL DEFAULT 'pending',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (client_id) REFERENCES clients(id),
+ FOREIGN KEY (coach_id) REFERENCES coaches(id)
+);
diff --git a/db/migrations/004_add_status_to_bookings.sql b/db/migrations/004_add_status_to_bookings.sql
new file mode 100644
index 0000000..94e30b2
--- /dev/null
+++ b/db/migrations/004_add_status_to_bookings.sql
@@ -0,0 +1 @@
+ALTER TABLE `bookings` ADD `status` VARCHAR(255) NOT NULL DEFAULT 'pending';
\ No newline at end of file
diff --git a/db/migrations/005_create_coach_availability_table.sql b/db/migrations/005_create_coach_availability_table.sql
new file mode 100644
index 0000000..00eb3f0
--- /dev/null
+++ b/db/migrations/005_create_coach_availability_table.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS coach_availability (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ coach_id INT NOT NULL,
+ start_time DATETIME NOT NULL,
+ end_time DATETIME NOT NULL,
+ FOREIGN KEY (coach_id) REFERENCES coaches(id)
+);
diff --git a/db/migrations/006_create_coach_recurring_availability_table.sql b/db/migrations/006_create_coach_recurring_availability_table.sql
new file mode 100644
index 0000000..4faf385
--- /dev/null
+++ b/db/migrations/006_create_coach_recurring_availability_table.sql
@@ -0,0 +1,8 @@
+CREATE TABLE IF NOT EXISTS coach_recurring_availability (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ coach_id INT NOT NULL,
+ day_of_week INT NOT NULL, -- 0 for Sunday, 1 for Monday, etc.
+ start_time TIME NOT NULL,
+ end_time TIME NOT NULL,
+ FOREIGN KEY (coach_id) REFERENCES coaches(id)
+);
diff --git a/db/migrations/007_create_reviews_table.sql b/db/migrations/007_create_reviews_table.sql
new file mode 100644
index 0000000..60e741b
--- /dev/null
+++ b/db/migrations/007_create_reviews_table.sql
@@ -0,0 +1,13 @@
+CREATE TABLE IF NOT EXISTS reviews (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ booking_id INT NOT NULL,
+ coach_id INT NOT NULL,
+ client_id INT NOT NULL,
+ rating INT NOT NULL,
+ review TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY `uniq_booking_id` (`booking_id`),
+ FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE CASCADE,
+ FOREIGN KEY (coach_id) REFERENCES coaches(id) ON DELETE CASCADE,
+ FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/db/migrations/008_add_stripe_to_bookings.sql b/db/migrations/008_add_stripe_to_bookings.sql
new file mode 100644
index 0000000..305a421
--- /dev/null
+++ b/db/migrations/008_add_stripe_to_bookings.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `bookings`
+ADD COLUMN `stripe_payment_intent_id` VARCHAR(255) DEFAULT NULL,
+ADD COLUMN `payment_status` VARCHAR(50) DEFAULT 'unpaid';
\ No newline at end of file
diff --git a/db/migrations/009_create_client_subscriptions_table.sql b/db/migrations/009_create_client_subscriptions_table.sql
new file mode 100644
index 0000000..662b67d
--- /dev/null
+++ b/db/migrations/009_create_client_subscriptions_table.sql
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS `client_subscriptions` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `client_id` INT NOT NULL,
+ `stripe_subscription_id` VARCHAR(255) NOT NULL,
+ `stripe_product_id` VARCHAR(255) NOT NULL,
+ `status` VARCHAR(50) NOT NULL,
+ `start_date` TIMESTAMP NOT NULL,
+ `end_date` TIMESTAMP NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
\ No newline at end of file
diff --git a/db/migrations/010_create_service_packages_table.sql b/db/migrations/010_create_service_packages_table.sql
new file mode 100644
index 0000000..e135b05
--- /dev/null
+++ b/db/migrations/010_create_service_packages_table.sql
@@ -0,0 +1,14 @@
+-- 010_create_service_packages_table.sql
+CREATE TABLE IF NOT EXISTS service_packages (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ coach_id INT NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ price DECIMAL(10, 2) NOT NULL,
+ num_sessions INT NOT NULL,
+ stripe_product_id VARCHAR(255),
+ stripe_price_id VARCHAR(255),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (coach_id) REFERENCES coaches(id) ON DELETE CASCADE
+);
diff --git a/db/migrations/011_create_client_packages_table.sql b/db/migrations/011_create_client_packages_table.sql
new file mode 100644
index 0000000..55dd44f
--- /dev/null
+++ b/db/migrations/011_create_client_packages_table.sql
@@ -0,0 +1,11 @@
+-- 011_create_client_packages_table.sql
+CREATE TABLE IF NOT EXISTS client_packages (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ client_id INT NOT NULL,
+ package_id INT NOT NULL,
+ sessions_remaining INT NOT NULL,
+ purchase_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ stripe_checkout_session_id VARCHAR(255),
+ FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE,
+ FOREIGN KEY (package_id) REFERENCES service_packages(id) ON DELETE CASCADE
+);
diff --git a/db/migrations/012_add_portfolio_to_coaches.sql b/db/migrations/012_add_portfolio_to_coaches.sql
new file mode 100644
index 0000000..d9b2f33
--- /dev/null
+++ b/db/migrations/012_add_portfolio_to_coaches.sql
@@ -0,0 +1 @@
+ALTER TABLE `coaches` ADD `bio` TEXT, ADD `specialties` VARCHAR(255);
\ No newline at end of file
diff --git a/db/migrations/013_create_coach_portfolio_items_table.sql b/db/migrations/013_create_coach_portfolio_items_table.sql
new file mode 100644
index 0000000..e41e6b1
--- /dev/null
+++ b/db/migrations/013_create_coach_portfolio_items_table.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS `coach_portfolio_items` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `coach_id` INT NOT NULL,
+ `item_type` ENUM('image', 'video') NOT NULL,
+ `url` VARCHAR(255) NOT NULL,
+ `caption` VARCHAR(255),
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (`coach_id`) REFERENCES `coaches`(`id`) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/db/migrations/014_create_settings_table.sql b/db/migrations/014_create_settings_table.sql
new file mode 100644
index 0000000..7b55af9
--- /dev/null
+++ b/db/migrations/014_create_settings_table.sql
@@ -0,0 +1,8 @@
+CREATE TABLE settings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ setting_key VARCHAR(255) NOT NULL UNIQUE,
+ setting_value VARCHAR(255)
+);
+
+INSERT INTO settings (setting_key, setting_value) VALUES ('coach_mode', 'multi');
+INSERT INTO settings (setting_key, setting_value) VALUES ('single_coach_id', NULL);
diff --git a/db/migrations/015_create_messages_table.sql b/db/migrations/015_create_messages_table.sql
new file mode 100644
index 0000000..f200dad
--- /dev/null
+++ b/db/migrations/015_create_messages_table.sql
@@ -0,0 +1,10 @@
+CREATE TABLE IF NOT EXISTS messages (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ sender_id INT NOT NULL,
+ sender_type ENUM('coach', 'client') NOT NULL,
+ receiver_id INT NOT NULL,
+ receiver_type ENUM('coach', 'client') NOT NULL,
+ message TEXT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ is_read BOOLEAN DEFAULT FALSE
+);
diff --git a/db/migrations/016_add_stripe_customer_id_to_clients.sql b/db/migrations/016_add_stripe_customer_id_to_clients.sql
new file mode 100644
index 0000000..c2367a1
--- /dev/null
+++ b/db/migrations/016_add_stripe_customer_id_to_clients.sql
@@ -0,0 +1 @@
+ALTER TABLE `clients` ADD `stripe_customer_id` VARCHAR(255) NULL;
diff --git a/db/migrations/017_create_support_tickets_table.sql b/db/migrations/017_create_support_tickets_table.sql
new file mode 100644
index 0000000..c2692d6
--- /dev/null
+++ b/db/migrations/017_create_support_tickets_table.sql
@@ -0,0 +1,10 @@
+
+CREATE TABLE IF NOT EXISTS `support_tickets` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `client_id` INT NOT NULL,
+ `subject` VARCHAR(255) NOT NULL,
+ `status` ENUM('Open', 'In Progress', 'Closed') NOT NULL DEFAULT 'Open',
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE
+);
diff --git a/db/migrations/018_create_support_ticket_messages_table.sql b/db/migrations/018_create_support_ticket_messages_table.sql
new file mode 100644
index 0000000..4129c84
--- /dev/null
+++ b/db/migrations/018_create_support_ticket_messages_table.sql
@@ -0,0 +1,10 @@
+
+CREATE TABLE IF NOT EXISTS `support_ticket_messages` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `ticket_id` INT NOT NULL,
+ `user_id` INT NOT NULL COMMENT 'Can be client_id or coach_id/admin_id',
+ `is_admin` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'TRUE if the message is from an admin/coach, FALSE if from a client',
+ `message` TEXT NOT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (`ticket_id`) REFERENCES `support_tickets`(`id`) ON DELETE CASCADE
+);
diff --git a/db/migrations/019_create_contracts_table.sql b/db/migrations/019_create_contracts_table.sql
new file mode 100644
index 0000000..c7bbe47
--- /dev/null
+++ b/db/migrations/019_create_contracts_table.sql
@@ -0,0 +1,7 @@
+CREATE TABLE IF NOT EXISTS `contracts` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `title` VARCHAR(255) NOT NULL,
+ `content` TEXT NOT NULL,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
diff --git a/db/migrations/020_create_client_contracts_table.sql b/db/migrations/020_create_client_contracts_table.sql
new file mode 100644
index 0000000..a8569b7
--- /dev/null
+++ b/db/migrations/020_create_client_contracts_table.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS `client_contracts` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `client_id` INT NOT NULL,
+ `contract_id` INT NOT NULL,
+ `signature_data` TEXT NOT NULL,
+ `signed_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE,
+ FOREIGN KEY (`contract_id`) REFERENCES `contracts`(`id`) ON DELETE CASCADE
+);
diff --git a/db/migrations/021_add_docuseal_to_client_contracts.sql b/db/migrations/021_add_docuseal_to_client_contracts.sql
new file mode 100644
index 0000000..9a9654f
--- /dev/null
+++ b/db/migrations/021_add_docuseal_to_client_contracts.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `client_contracts`
+ADD COLUMN `docuseal_submission_id` VARCHAR(255) DEFAULT NULL,
+DROP COLUMN `signature_data`;
\ No newline at end of file
diff --git a/db/migrations/022_add_docuseal_document_url_to_client_contracts.sql b/db/migrations/022_add_docuseal_document_url_to_client_contracts.sql
new file mode 100644
index 0000000..9db2b8b
--- /dev/null
+++ b/db/migrations/022_add_docuseal_document_url_to_client_contracts.sql
@@ -0,0 +1 @@
+ALTER TABLE `client_contracts` ADD COLUMN `docuseal_document_url` VARCHAR(255) DEFAULT NULL;
\ No newline at end of file
diff --git a/db/migrations/023_add_role_to_contracts.sql b/db/migrations/023_add_role_to_contracts.sql
new file mode 100644
index 0000000..48eacd1
--- /dev/null
+++ b/db/migrations/023_add_role_to_contracts.sql
@@ -0,0 +1 @@
+ALTER TABLE `contracts` ADD COLUMN `role` VARCHAR(255) NOT NULL DEFAULT 'Client';
\ No newline at end of file
diff --git a/db/migrations/024_create_client_notes_table.sql b/db/migrations/024_create_client_notes_table.sql
new file mode 100644
index 0000000..7a028d6
--- /dev/null
+++ b/db/migrations/024_create_client_notes_table.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS client_notes (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ client_id INT NOT NULL,
+ coach_id INT NOT NULL,
+ note TEXT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE,
+ FOREIGN KEY (coach_id) REFERENCES coaches(id) ON DELETE CASCADE
+);
diff --git a/db/migrations/025_create_surveys_table.sql b/db/migrations/025_create_surveys_table.sql
new file mode 100644
index 0000000..a2163d1
--- /dev/null
+++ b/db/migrations/025_create_surveys_table.sql
@@ -0,0 +1,11 @@
+
+CREATE TABLE `surveys` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `coach_id` int(11) NOT NULL,
+ `title` varchar(255) NOT NULL,
+ `description` text,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `coach_id` (`coach_id`),
+ CONSTRAINT `surveys_ibfk_1` FOREIGN KEY (`coach_id`) REFERENCES `coaches` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/db/migrations/026_create_survey_questions_table.sql b/db/migrations/026_create_survey_questions_table.sql
new file mode 100644
index 0000000..1d190db
--- /dev/null
+++ b/db/migrations/026_create_survey_questions_table.sql
@@ -0,0 +1,11 @@
+
+CREATE TABLE `survey_questions` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `survey_id` int(11) NOT NULL,
+ `question` text NOT NULL,
+ `type` enum('text','textarea','select','radio','checkbox') NOT NULL,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `survey_id` (`survey_id`),
+ CONSTRAINT `survey_questions_ibfk_1` FOREIGN KEY (`survey_id`) REFERENCES `surveys` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/db/migrations/027_create_survey_question_options_table.sql b/db/migrations/027_create_survey_question_options_table.sql
new file mode 100644
index 0000000..4c1537e
--- /dev/null
+++ b/db/migrations/027_create_survey_question_options_table.sql
@@ -0,0 +1,9 @@
+
+CREATE TABLE `survey_question_options` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `question_id` int(11) NOT NULL,
+ `option_text` varchar(255) NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `question_id` (`question_id`),
+ CONSTRAINT `survey_question_options_ibfk_1` FOREIGN KEY (`question_id`) REFERENCES `survey_questions` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/db/migrations/028_create_client_surveys_table.sql b/db/migrations/028_create_client_surveys_table.sql
new file mode 100644
index 0000000..82cf88c
--- /dev/null
+++ b/db/migrations/028_create_client_surveys_table.sql
@@ -0,0 +1,14 @@
+
+CREATE TABLE `client_surveys` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `client_id` int(11) NOT NULL,
+ `survey_id` int(11) NOT NULL,
+ `status` enum('pending','completed') NOT NULL DEFAULT 'pending',
+ `completed_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `client_id` (`client_id`),
+ KEY `survey_id` (`survey_id`),
+ CONSTRAINT `client_surveys_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `client_surveys_ibfk_2` FOREIGN KEY (`survey_id`) REFERENCES `surveys` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/db/migrations/029_create_survey_responses_table.sql b/db/migrations/029_create_survey_responses_table.sql
new file mode 100644
index 0000000..a4ea25f
--- /dev/null
+++ b/db/migrations/029_create_survey_responses_table.sql
@@ -0,0 +1,13 @@
+
+CREATE TABLE `survey_responses` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `client_survey_id` int(11) NOT NULL,
+ `question_id` int(11) NOT NULL,
+ `response` text,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `client_survey_id` (`client_survey_id`),
+ KEY `question_id` (`question_id`),
+ CONSTRAINT `survey_responses_ibfk_1` FOREIGN KEY (`client_survey_id`) REFERENCES `client_surveys` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `survey_responses_ibfk_2` FOREIGN KEY (`question_id`) REFERENCES `survey_questions` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/db/migrations/030_create_content_table.sql b/db/migrations/030_create_content_table.sql
new file mode 100644
index 0000000..5c0b5aa
--- /dev/null
+++ b/db/migrations/030_create_content_table.sql
@@ -0,0 +1,44 @@
+--
+-- Table structure for table `content`
+--
+
+CREATE TABLE `content` (
+ `id` int(11) NOT NULL,
+ `coach_id` int(11) NOT NULL,
+ `title` varchar(255) NOT NULL,
+ `description` text DEFAULT NULL,
+ `file_path` varchar(255) NOT NULL,
+ `file_type` varchar(50) DEFAULT NULL,
+ `created_at` timestamp NOT NULL DEFAULT current_timestamp()
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+--
+-- Indexes for dumped tables
+--
+
+--
+-- Indexes for table `content`
+--
+ALTER TABLE `content`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `coach_id` (`coach_id`);
+
+--
+-- AUTO_INCREMENT for dumped tables
+--
+
+--
+-- AUTO_INCREMENT for table `content`
+--
+ALTER TABLE `content`
+ MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
+
+--
+-- Constraints for dumped tables
+--
+
+--
+-- Constraints for table `content`
+--
+ALTER TABLE `content`
+ ADD CONSTRAINT `content_ibfk_1` FOREIGN KEY (`coach_id`) REFERENCES `coaches` (`id`) ON DELETE CASCADE;
diff --git a/db/migrations/031_create_client_content_table.sql b/db/migrations/031_create_client_content_table.sql
new file mode 100644
index 0000000..6861675
--- /dev/null
+++ b/db/migrations/031_create_client_content_table.sql
@@ -0,0 +1,43 @@
+--
+-- Table structure for table `client_content`
+--
+
+CREATE TABLE `client_content` (
+ `id` int(11) NOT NULL,
+ `client_id` int(11) NOT NULL,
+ `content_id` int(11) NOT NULL,
+ `assigned_at` timestamp NOT NULL DEFAULT current_timestamp()
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+--
+-- Indexes for dumped tables
+--
+
+--
+-- Indexes for table `client_content`
+--
+ALTER TABLE `client_content`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `client_id` (`client_id`),
+ ADD KEY `content_id` (`content_id`);
+
+--
+-- AUTO_INCREMENT for dumped tables
+--
+
+--
+-- AUTO_INCREMENT for table `client_content`
+--
+ALTER TABLE `client_content`
+ MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
+
+--
+-- Constraints for dumped tables
+--
+
+--
+-- Constraints for table `client_content`
+--
+ALTER TABLE `client_content`
+ ADD CONSTRAINT `client_content_ibfk_1` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE,
+ ADD CONSTRAINT `client_content_ibfk_2` FOREIGN KEY (`content_id`) REFERENCES `content` (`id`) ON DELETE CASCADE;
diff --git a/db/migrations/032_create_package_content_table.sql b/db/migrations/032_create_package_content_table.sql
new file mode 100644
index 0000000..56022b8
--- /dev/null
+++ b/db/migrations/032_create_package_content_table.sql
@@ -0,0 +1,43 @@
+--
+-- Table structure for table `package_content`
+--
+
+CREATE TABLE `package_content` (
+ `id` int(11) NOT NULL,
+ `package_id` int(11) NOT NULL,
+ `content_id` int(11) NOT NULL,
+ `delay_days` int(11) DEFAULT 0
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+--
+-- Indexes for dumped tables
+--
+
+--
+-- Indexes for table `package_content`
+--
+ALTER TABLE `package_content`
+ ADD PRIMARY KEY (`id`),
+ ADD KEY `package_id` (`package_id`),
+ ADD KEY `content_id` (`content_id`);
+
+--
+-- AUTO_INCREMENT for dumped tables
+--
+
+--
+-- AUTO_INCREMENT for table `package_content`
+--
+ALTER TABLE `package_content`
+ MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
+
+--
+-- Constraints for dumped tables
+--
+
+--
+-- Constraints for table `package_content`
+--
+ALTER TABLE `package_content`
+ ADD CONSTRAINT `package_content_ibfk_1` FOREIGN KEY (`package_id`) REFERENCES `service_packages` (`id`) ON DELETE CASCADE,
+ ADD CONSTRAINT `package_content_ibfk_2` FOREIGN KEY (`content_id`) REFERENCES `content` (`id`) ON DELETE CASCADE;
diff --git a/db/migrations/033_alter_service_packages_table.sql b/db/migrations/033_alter_service_packages_table.sql
new file mode 100644
index 0000000..cf2c940
--- /dev/null
+++ b/db/migrations/033_alter_service_packages_table.sql
@@ -0,0 +1,7 @@
+-- 033_alter_service_packages_table.sql
+ALTER TABLE service_packages
+ADD COLUMN package_type ENUM('individual', 'group', 'workshop') NOT NULL DEFAULT 'individual',
+ADD COLUMN max_clients INT,
+ADD COLUMN start_date DATETIME,
+ADD COLUMN end_date DATETIME,
+ADD COLUMN payment_plan TEXT;
diff --git a/db/migrations/034_create_package_service_items_table.sql b/db/migrations/034_create_package_service_items_table.sql
new file mode 100644
index 0000000..40c3ad5
--- /dev/null
+++ b/db/migrations/034_create_package_service_items_table.sql
@@ -0,0 +1,10 @@
+-- 034_create_package_service_items_table.sql
+CREATE TABLE IF NOT EXISTS package_service_items (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ package_id INT NOT NULL,
+ service_type ENUM('individual_session', 'group_session') NOT NULL,
+ quantity INT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (package_id) REFERENCES service_packages(id) ON DELETE CASCADE
+);
diff --git a/db/migrations/035_add_package_id_to_client_subscriptions.sql b/db/migrations/035_add_package_id_to_client_subscriptions.sql
new file mode 100644
index 0000000..49fe08b
--- /dev/null
+++ b/db/migrations/035_add_package_id_to_client_subscriptions.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `client_subscriptions` ADD `package_id` INT NOT NULL AFTER `client_id`;
+ALTER TABLE `client_subscriptions` ADD FOREIGN KEY (`package_id`) REFERENCES `service_packages`(`id`) ON DELETE CASCADE;
\ No newline at end of file
diff --git a/db/migrations/036_add_buffer_time_to_coaches.sql b/db/migrations/036_add_buffer_time_to_coaches.sql
new file mode 100644
index 0000000..49c92e8
--- /dev/null
+++ b/db/migrations/036_add_buffer_time_to_coaches.sql
@@ -0,0 +1 @@
+ALTER TABLE coaches ADD COLUMN buffer_time INT NOT NULL DEFAULT 0;
\ No newline at end of file
diff --git a/db/migrations/037_add_timezone_to_coaches.sql b/db/migrations/037_add_timezone_to_coaches.sql
new file mode 100644
index 0000000..284a178
--- /dev/null
+++ b/db/migrations/037_add_timezone_to_coaches.sql
@@ -0,0 +1 @@
+ALTER TABLE coaches ADD COLUMN timezone VARCHAR(255) NOT NULL DEFAULT 'UTC';
\ No newline at end of file
diff --git a/db/migrations/038_add_timezone_to_clients.sql b/db/migrations/038_add_timezone_to_clients.sql
new file mode 100644
index 0000000..abd65ed
--- /dev/null
+++ b/db/migrations/038_add_timezone_to_clients.sql
@@ -0,0 +1 @@
+ALTER TABLE clients ADD COLUMN timezone VARCHAR(255) NOT NULL DEFAULT 'UTC';
\ No newline at end of file
diff --git a/db/migrations/039_add_email_to_coaches.sql b/db/migrations/039_add_email_to_coaches.sql
new file mode 100644
index 0000000..e544133
--- /dev/null
+++ b/db/migrations/039_add_email_to_coaches.sql
@@ -0,0 +1 @@
+ALTER TABLE coaches ADD COLUMN email VARCHAR(255) NOT NULL UNIQUE;
\ No newline at end of file
diff --git a/db/migrations/040_add_password_to_coaches.sql b/db/migrations/040_add_password_to_coaches.sql
new file mode 100644
index 0000000..49ae279
--- /dev/null
+++ b/db/migrations/040_add_password_to_coaches.sql
@@ -0,0 +1 @@
+ALTER TABLE coaches ADD COLUMN password VARCHAR(255) NOT NULL;
\ No newline at end of file
diff --git a/db/migrations/041_create_discounts_table.sql b/db/migrations/041_create_discounts_table.sql
new file mode 100644
index 0000000..dfb4cfa
--- /dev/null
+++ b/db/migrations/041_create_discounts_table.sql
@@ -0,0 +1,14 @@
+
+CREATE TABLE IF NOT EXISTS `discounts` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `code` VARCHAR(255) NOT NULL UNIQUE,
+ `type` ENUM('percentage', 'fixed') NOT NULL,
+ `value` DECIMAL(10, 2) NOT NULL,
+ `start_date` DATETIME,
+ `end_date` DATETIME,
+ `uses_limit` INT,
+ `times_used` INT DEFAULT 0,
+ `is_active` BOOLEAN DEFAULT TRUE,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
diff --git a/db/migrations/042_create_gift_codes_table.sql b/db/migrations/042_create_gift_codes_table.sql
new file mode 100644
index 0000000..4369851
--- /dev/null
+++ b/db/migrations/042_create_gift_codes_table.sql
@@ -0,0 +1,14 @@
+
+CREATE TABLE IF NOT EXISTS `gift_codes` (
+ `id` INT PRIMARY KEY AUTO_INCREMENT,
+ `code` VARCHAR(255) NOT NULL UNIQUE,
+ `package_id` INT NOT NULL,
+ `stripe_checkout_session_id` VARCHAR(255) NOT NULL,
+ `is_redeemed` BOOLEAN DEFAULT FALSE,
+ `redeemed_by_client_id` INT,
+ `redeemed_at` DATETIME,
+ `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ FOREIGN KEY (`package_id`) REFERENCES `service_packages`(`id`),
+ FOREIGN KEY (`redeemed_by_client_id`) REFERENCES `clients`(`id`)
+);
diff --git a/db/migrations/043_alter_service_packages_for_payment_plans.sql b/db/migrations/043_alter_service_packages_for_payment_plans.sql
new file mode 100644
index 0000000..2a3cd2b
--- /dev/null
+++ b/db/migrations/043_alter_service_packages_for_payment_plans.sql
@@ -0,0 +1,7 @@
+
+ALTER TABLE `service_packages`
+ADD COLUMN `payment_type` ENUM('one_time', 'subscription', 'payment_plan') NOT NULL DEFAULT 'one_time',
+ADD COLUMN `deposit_amount` DECIMAL(10, 2) DEFAULT NULL,
+ADD COLUMN `installments` INT DEFAULT NULL,
+ADD COLUMN `installment_interval` ENUM('day', 'week', 'month', 'year') DEFAULT NULL,
+ADD COLUMN `pay_in_full_discount_percentage` DECIMAL(5, 2) DEFAULT NULL;
diff --git a/db/migrations/044_create_sms_logs_table.sql b/db/migrations/044_create_sms_logs_table.sql
new file mode 100644
index 0000000..f9ddd21
--- /dev/null
+++ b/db/migrations/044_create_sms_logs_table.sql
@@ -0,0 +1,13 @@
+CREATE TABLE IF NOT EXISTS `sms_logs` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `client_id` int(11) DEFAULT NULL,
+ `user_id` int(11) DEFAULT NULL,
+ `sender` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `recipient` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `message` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `status` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
+ PRIMARY KEY (`id`),
+ KEY `client_id` (`client_id`),
+ KEY `user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/decline-booking.php b/decline-booking.php
new file mode 100644
index 0000000..0d2a737
--- /dev/null
+++ b/decline-booking.php
@@ -0,0 +1,69 @@
+prepare("SELECT * FROM bookings WHERE id = ? AND coach_id = ?");
+$stmt->execute([$booking_id, $coach_id]);
+$booking = $stmt->fetch();
+
+if ($booking) {
+ if (!empty($booking['stripe_payment_intent_id'])) {
+ try {
+ $refund = \Stripe\Refund::create([
+ 'payment_intent' => $booking['stripe_payment_intent_id'],
+ ]);
+
+ if ($refund->status == 'succeeded') {
+ $stmt = db()->prepare("UPDATE bookings SET status = 'declined', payment_status = 'refunded' WHERE id = ?");
+ $stmt->execute([$booking_id]);
+ } else {
+ header('Location: dashboard.php?status=error');
+ exit;
+ }
+ } catch (\Stripe\Exception\ApiErrorException $e) {
+ error_log('Stripe API error: ' . $e->getMessage());
+ header('Location: dashboard.php?status=error');
+ exit;
+ }
+ } else {
+ $stmt = db()->prepare("UPDATE bookings SET status = 'declined' WHERE id = ?");
+ $stmt->execute([$booking_id]);
+ }
+
+ // Notify client
+ $stmt = db()->prepare("SELECT c.email, c.name as client_name, co.name as coach_name, b.booking_time FROM bookings b JOIN clients c ON b.client_id = c.id JOIN coaches co ON b.coach_id = co.id WHERE b.id = ?");
+ $stmt->execute([$booking_id]);
+ $booking_details = $stmt->fetch();
+
+ if ($booking_details) {
+ $to = $booking_details['email'];
+ $subject = 'Booking Declined and Refunded';
+ $body = "Hello " . htmlspecialchars($booking_details['client_name']) . ",
";
+ $body .= "We are sorry to inform you that your booking with " . htmlspecialchars($booking_details['coach_name']) . " on " . htmlspecialchars(date('F j, Y, g:i a', strtotime($booking_details['booking_time']))) . " has been declined. A full refund has been issued.
";
+ $body .= "Thank you for using CoachConnect.
";
+
+ MailService::sendMail($to, $subject, $body, strip_tags($body));
+ }
+
+ header('Location: dashboard.php?status=declined');
+} else {
+ header('Location: dashboard.php?status=error');
+}
+
+exit;
diff --git a/delete-availability.php b/delete-availability.php
new file mode 100644
index 0000000..d0bb3d6
--- /dev/null
+++ b/delete-availability.php
@@ -0,0 +1,39 @@
+prepare("SELECT * FROM coach_availability WHERE id = ? AND coach_id = ?");
+ $stmt->execute([$availability_id, $coach_id]);
+ $availability = $stmt->fetch();
+
+ if (!$availability) {
+ header('Location: dashboard.php?status=error&message=Availability+not+found');
+ exit;
+ }
+
+ // Delete the availability slot
+ $delete_stmt = db()->prepare("DELETE FROM coach_availability WHERE id = ?");
+ $delete_stmt->execute([$availability_id]);
+
+ header('Location: dashboard.php?status=success&message=Availability+deleted+successfully');
+ exit;
+} catch (PDOException $e) {
+ error_log('Error deleting availability: ' . $e->getMessage());
+ header('Location: dashboard.php?status=error&message=Could+not+delete+availability');
+ exit;
+}
diff --git a/delete-recurring-availability.php b/delete-recurring-availability.php
new file mode 100644
index 0000000..eaf3c6f
--- /dev/null
+++ b/delete-recurring-availability.php
@@ -0,0 +1,24 @@
+prepare("DELETE FROM coach_recurring_availability WHERE id = ? AND coach_id = ?");
+ $stmt->execute([$availability_id, $coach_id]);
+ header('Location: dashboard.php?status=recurring_deleted');
+ } catch (PDOException $e) {
+ header('Location: dashboard.php?status=error');
+ }
+} else {
+ header('Location: dashboard.php');
+}
+exit;
diff --git a/docuseal-webhook.php b/docuseal-webhook.php
new file mode 100644
index 0000000..ba0db9c
--- /dev/null
+++ b/docuseal-webhook.php
@@ -0,0 +1,30 @@
+submissions->retrieve($submissionId);
+ $documentUrl = $submission['url'];
+
+ // Update the client_contracts table
+ $stmt = db()->prepare('UPDATE client_contracts SET docuseal_document_url = ? WHERE docuseal_submission_id = ?');
+ $stmt->execute([$documentUrl, $submissionId]);
+
+ } catch (Exception $e) {
+ // Log the error
+ error_log('DocuSeal webhook error: ' . $e->getMessage());
+ }
+ }
+ }
+}
+
+http_response_code(200);
diff --git a/docuseal/config.php b/docuseal/config.php
new file mode 100644
index 0000000..6abd1b9
--- /dev/null
+++ b/docuseal/config.php
@@ -0,0 +1,7 @@
+prepare("SELECT bio, specialties FROM coaches WHERE id = ?");
+$stmt->execute([$coach_id]);
+$coach = $stmt->fetch();
+
+$bio = $coach['bio'] ?? '';
+$specialties = $coach['specialties'] ?? '';
+
+// Handle form submission for bio and specialties
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_profile'])) {
+ $bio = trim($_POST['bio']);
+ $specialties = trim($_POST['specialties']);
+
+ $stmt = $pdo->prepare("UPDATE coaches SET bio = ?, specialties = ? WHERE id = ?");
+ $stmt->execute([$bio, $specialties, $coach_id]);
+
+ header('Location: edit-portfolio.php?success=1');
+ exit();
+}
+
+// Handle media upload
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upload_media'])) {
+ if (isset($_FILES['media']) && $_FILES['media']['error'] == 0) {
+ $caption = trim($_POST['caption']);
+ $allowed = ['jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif'];
+ $filename = $_FILES['media']['name'];
+ $filetype = $_FILES['media']['type'];
+ $filesize = $_FILES['media']['size'];
+
+ $ext = pathinfo($filename, PATHINFO_EXTENSION);
+ if (!array_key_exists($ext, $allowed)) {
+ die("Error: Please select a valid file format.");
+ }
+
+ $maxsize = 5 * 1024 * 1024;
+ if ($filesize > $maxsize) {
+ die("Error: File size is larger than the allowed limit.");
+ }
+
+ if (in_array($filetype, $allowed)) {
+ $new_filename = uniqid() . '.' . $ext;
+ $filepath = 'uploads/portfolio/' . $new_filename;
+
+ $error_message = '';
+ if (move_uploaded_file($_FILES['media']['tmp_name'], $filepath)) {
+ $stmt = $pdo->prepare("INSERT INTO coach_portfolio_items (coach_id, item_type, url, caption) VALUES (?, 'image', ?, ?)");
+ $stmt->execute([$coach_id, $filepath, $caption]);
+ header('Location: edit-portfolio.php?success=2');
+ exit();
+ } else {
+ $error_message = 'Error: There was a problem uploading your file. Please try again.';
+ } }
+ }
+}
+
+// Handle media deletion
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_media'])) {
+ $item_id = $_POST['delete_item_id'];
+
+ // First, get the file path to delete the file
+ $stmt = $pdo->prepare("SELECT url FROM coach_portfolio_items WHERE id = ? AND coach_id = ?");
+ $stmt->execute([$item_id, $coach_id]);
+ $item = $stmt->fetch();
+
+ if ($item) {
+ // Delete file from server
+ if (file_exists($item['url'])) {
+ unlink($item['url']);
+ }
+
+ // Delete from database
+ $stmt = $pdo->prepare("DELETE FROM coach_portfolio_items WHERE id = ?");
+ $stmt->execute([$item_id]);
+
+ header('Location: edit-portfolio.php?success=3');
+ exit();
+ }
+}
+
+// Fetch all portfolio items for the coach
+$stmt = $pdo->prepare("SELECT * FROM coach_portfolio_items WHERE coach_id = ? ORDER BY created_at DESC");
+$stmt->execute([$coach_id]);
+$portfolio_items = $stmt->fetchAll();
+
+?>
+
+
+
+
+
+ Edit Portfolio
+
+
+
+
+
+
+
Edit Portfolio
+
+
+
Portfolio updated successfully.
+
+
+
+
+
+
+
+
+
Profile Information
+
+
+ Biography
+
+
+
+ Specialties
+
+
+ Save Changes
+
+
+
+
+
+
+
Portfolio Media
+
+
+ Upload Image
+
+
+
+ Caption
+
+
+ Upload Media
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/includes/footer.php b/includes/footer.php
new file mode 100644
index 0000000..9c0896d
--- /dev/null
+++ b/includes/footer.php
@@ -0,0 +1,12 @@
+
+
+
+
+ © 2025 Coaching Platform
+
+
+
+
+
+