+
+
+
+
+ = $success ?>
+
-
-
-
+
+
+
+
+ = $error ?>
+
-
+
-
-
-
-
-
-
-
-
 ?>?v=<?= time() ?>)
-
-
- = strtoupper(substr($user['full_name'] ?: $user['username'], 0, 1)) ?>
-
-
-
-
-
Allowed: JPG, PNG, GIF, WebP. Recommended: Square image.
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
 ?>?v=<?= time() ?>)
-
-
- = strtoupper(substr($user['full_name'] ?: $user['username'], 0, 1)) ?>
+
+
+
Change Password
+
-
-
-
= htmlspecialchars($user['full_name']) ?>
-
@= htmlspecialchars($user['username']) ?> • = htmlspecialchars($user['group_name']) ?>
-
-
- Active Account
-
-
-
- Member since = date('F d, Y', strtotime($user['created_at'])) ?>
-
-
-
-
-
-
Account Security
-
- - Password is encrypted
- - Role-based access control
- - Session-based authentication
-
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/db/init.php b/db/init.php
index 9375db9..6f9d174 100644
--- a/db/init.php
+++ b/db/init.php
@@ -4,36 +4,64 @@ require_once __DIR__ . '/config.php';
try {
$pdo = db();
- // Execute schema
- $sql = file_get_contents(__DIR__ . '/schema.sql');
- $pdo->exec($sql);
+ // 1. Run Consolidated Schema
+ $schema = file_get_contents(__DIR__ . '/schema.sql');
+ // Split by ; to handle multiple statements if PDO::exec has issues,
+ // but schema.sql is designed to be executed as one block or via a loop.
+ // For simplicity and to handle the large schema:
+ $queries = array_filter(array_map('trim', explode(';', $schema)));
+ foreach ($queries as $query) {
+ if (!empty($query)) {
+ try {
+ $pdo->exec($query);
+ } catch (PDOException $e) {
+ // Ignore errors like "Duplicate column" if re-running on existing DB
+ if (strpos($e->getMessage(), 'Duplicate column') === false &&
+ strpos($e->getMessage(), 'already exists') === false) {
+ echo "Notice (Schema): " . $e->getMessage() . "\n";
+ }
+ }
+ }
+ }
- // Check if data exists
+ // 2. Run Company Settings if table exists
+ if (file_exists(__DIR__ . '/company_settings.sql')) {
+ $cs_sql = file_get_contents(__DIR__ . '/company_settings.sql');
+ $cs_queries = array_filter(array_map('trim', explode(';', $cs_sql)));
+ foreach ($cs_queries as $query) {
+ if (!empty($query)) {
+ try {
+ $pdo->exec($query);
+ } catch (PDOException $e) {
+ // Ignore
+ }
+ }
+ }
+ }
+
+ // 3. Seed initial data if empty
$stmt = $pdo->query("SELECT COUNT(*) FROM outlets");
if ($stmt->fetchColumn() == 0) {
- // Seed Outlets
$pdo->exec("INSERT INTO outlets (name, address) VALUES ('Main Downtown', '123 Main St'), ('Westside Hub', '456 West Blvd')");
-
- // Seed Categories
$pdo->exec("INSERT INTO categories (name, sort_order) VALUES ('Burgers', 1), ('Sides', 2), ('Drinks', 3)");
-
- // Seed Products
$pdo->exec("INSERT INTO products (category_id, name, description, price) VALUES
(1, 'Signature Burger', 'Juicy beef patty with special sauce', 12.99),
(1, 'Veggie Delight', 'Plant-based patty with fresh avocado', 11.50),
(2, 'Truffle Fries', 'Crispy fries with truffle oil and parmesan', 5.99),
(3, 'Craft Cola', 'House-made sparkling cola', 3.50)");
- // Seed Variants
- $pdo->exec("INSERT INTO product_variants (product_id, name, price_adjustment) VALUES
- (1, 'Double Patty', 4.00),
- (1, 'Extra Cheese', 1.00),
- (3, 'Large Portion', 2.00)");
+ // Ensure Admin group and user exist
+ $pdo->exec("INSERT IGNORE INTO user_groups (id, name, permissions) VALUES (1, 'Administrator', 'all')");
+
+ // admin123 hash
+ $hashed_pass = password_hash('admin123', PASSWORD_DEFAULT);
+ $pdo->exec("INSERT IGNORE INTO users (group_id, username, password, full_name, is_active)
+ VALUES (1, 'admin', '$hashed_pass', 'Super Admin', 1)");
echo "Database initialized and seeded successfully.";
} else {
- echo "Database already initialized.";
+ echo "Database structure updated. Data already exists.";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
-}
+}
\ No newline at end of file
diff --git a/db/schema.sql b/db/schema.sql
index 78d2335..439f88e 100644
--- a/db/schema.sql
+++ b/db/schema.sql
@@ -17,7 +17,12 @@ CREATE TABLE IF NOT EXISTS products (
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
+ cost_price DECIMAL(10, 2) DEFAULT 0.00,
+ stock_quantity INT DEFAULT 0,
image_url VARCHAR(255),
+ promo_discount_percent DECIMAL(5, 2) DEFAULT NULL,
+ promo_date_from DATE DEFAULT NULL,
+ promo_date_to DATE DEFAULT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id)
);
@@ -46,18 +51,72 @@ CREATE TABLE IF NOT EXISTS tables (
FOREIGN KEY (area_id) REFERENCES areas(id)
);
+CREATE TABLE IF NOT EXISTS user_groups (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ permissions TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS users (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ group_id INT,
+ username VARCHAR(255) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ full_name VARCHAR(255),
+ employee_id VARCHAR(50) UNIQUE,
+ email VARCHAR(255) UNIQUE,
+ profile_pic VARCHAR(255) DEFAULT NULL,
+ is_active BOOLEAN DEFAULT TRUE,
+ is_ratable TINYINT(1) DEFAULT 0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE SET NULL
+);
+
+CREATE TABLE IF NOT EXISTS user_outlets (
+ user_id INT(11) NOT NULL,
+ outlet_id INT(11) NOT NULL,
+ PRIMARY KEY (user_id, outlet_id),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS customers (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ phone VARCHAR(20) UNIQUE,
+ email VARCHAR(255),
+ points INT DEFAULT 0,
+ loyalty_redemptions_count INT DEFAULT 0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS payment_types (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ type ENUM('cash', 'card', 'api') DEFAULT 'cash',
+ api_provider VARCHAR(50) DEFAULT NULL,
+ is_active BOOLEAN DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
CREATE TABLE IF NOT EXISTS orders (
id INT AUTO_INCREMENT PRIMARY KEY,
outlet_id INT,
+ user_id INT(11) NULL,
+ customer_id INT DEFAULT NULL,
table_id INT,
table_number VARCHAR(50),
- order_type ENUM('dine-in', 'delivery', 'drive-thru') DEFAULT 'dine-in',
+ order_type ENUM('dine-in', 'delivery', 'drive-thru', 'takeaway') DEFAULT 'dine-in',
status ENUM('pending', 'preparing', 'ready', 'completed', 'cancelled') DEFAULT 'pending',
+ payment_type_id INT DEFAULT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
customer_name VARCHAR(255),
customer_phone VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (outlet_id) REFERENCES outlets(id),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
+ FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL,
FOREIGN KEY (table_id) REFERENCES tables(id)
);
@@ -72,11 +131,123 @@ CREATE TABLE IF NOT EXISTS order_items (
FOREIGN KEY (product_id) REFERENCES products(id)
);
--- Loyalty
-CREATE TABLE IF NOT EXISTS loyalty_customers (
+CREATE TABLE IF NOT EXISTS company_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
- name VARCHAR(255),
- email VARCHAR(255) UNIQUE,
- points INT DEFAULT 0,
+ company_name VARCHAR(255) NOT NULL DEFAULT 'My Restaurant',
+ address TEXT,
+ phone VARCHAR(50),
+ email VARCHAR(255),
+ vat_rate DECIMAL(5, 2) DEFAULT 0.00,
+ currency_symbol VARCHAR(10) DEFAULT '$',
+ currency_decimals INT DEFAULT 2,
+ logo_url VARCHAR(255),
+ favicon_url VARCHAR(255),
+ ctr_number VARCHAR(50),
+ vat_number VARCHAR(50),
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS loyalty_settings (
+ id INT PRIMARY KEY,
+ points_per_order INT DEFAULT 10,
+ points_for_free_meal INT DEFAULT 70,
+ is_enabled TINYINT(1) DEFAULT 1
+);
+
+CREATE TABLE IF NOT EXISTS integration_settings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ provider VARCHAR(50) NOT NULL,
+ setting_key VARCHAR(100) NOT NULL,
+ setting_value TEXT,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY unique_provider_key (provider, setting_key)
+);
+
+CREATE TABLE IF NOT EXISTS expense_categories (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
\ No newline at end of file
+);
+
+CREATE TABLE IF NOT EXISTS expenses (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ category_id INT NOT NULL,
+ outlet_id INT NOT NULL,
+ amount DECIMAL(10, 2) NOT NULL,
+ description TEXT,
+ expense_date DATE NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (category_id) REFERENCES expense_categories(id),
+ FOREIGN KEY (outlet_id) REFERENCES outlets(id)
+);
+
+CREATE TABLE IF NOT EXISTS suppliers (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ contact_person VARCHAR(255),
+ email VARCHAR(255),
+ phone VARCHAR(50),
+ address TEXT,
+ vat_no VARCHAR(50),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS purchases (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ supplier_id INT NULL,
+ purchase_date DATE NOT NULL,
+ total_amount DECIMAL(10, 2) DEFAULT 0.00,
+ status ENUM('pending', 'completed', 'cancelled') DEFAULT 'pending',
+ notes TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL
+);
+
+CREATE TABLE IF NOT EXISTS purchase_items (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ purchase_id INT NOT NULL,
+ product_id INT NOT NULL,
+ quantity INT NOT NULL,
+ cost_price DECIMAL(10, 2) NOT NULL,
+ total_price DECIMAL(10, 2) NOT NULL,
+ FOREIGN KEY (purchase_id) REFERENCES purchases(id) ON DELETE CASCADE,
+ FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS ads_images (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ image_path VARCHAR(255) NOT NULL,
+ title VARCHAR(255) DEFAULT NULL,
+ sort_order INT DEFAULT 0,
+ is_active TINYINT(1) DEFAULT 1,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS attendance_logs (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT,
+ employee_id VARCHAR(50),
+ log_timestamp DATETIME,
+ log_type ENUM('IN', 'OUT', 'OTHER') DEFAULT 'IN',
+ device_id VARCHAR(100),
+ ip_address VARCHAR(45),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
+);
+
+CREATE TABLE IF NOT EXISTS staff_ratings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ user_id INT NOT NULL,
+ rating INT NOT NULL CHECK (rating >= 1 AND rating <= 5),
+ comment TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS service_ratings (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ rating INT NOT NULL CHECK (rating >= 1 AND rating <= 5),
+ comment TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/includes/functions.php b/includes/functions.php
index 70110bd..9265677 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -239,7 +239,7 @@ function login_user($username, $password) {
LEFT JOIN user_groups g ON u.group_id = g.id
WHERE u.username = ? AND u.is_active = 1
LIMIT 1");
- $stmt->execute([$username]);
+ $stmt->execute([username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password'])) {
@@ -264,7 +264,7 @@ function get_logged_user() {
function require_login() {
if (!get_logged_user()) {
- header('Location: /login.php');
+ header('Location: ' . url('login.php'));
exit;
}
}
@@ -291,17 +291,35 @@ function require_permission($permission) {
}
/**
- * Get the base URL of the application.
- *
- * @return string The base URL.
+ * Get the base path of the application.
+ */
+function get_base_path() {
+ static $base_path = null;
+ if ($base_path === null) {
+ $script_dir = dirname($_SERVER['SCRIPT_NAME'] ?? '');
+ // Replace known subfolders at the end of the path
+ $base_path = preg_replace('/(\/admin|\/api|\/includes)$|(\/admin\/.*|\/api\/.*|\/includes\/.*)$/', '', $script_dir);
+ if ($base_path === DIRECTORY_SEPARATOR || $base_path === '/') {
+ $base_path = '';
+ }
+ }
+ return $base_path;
+}
+
+/**
+ * Generate a URL relative to the application base.
+ */
+function url($path = '') {
+ $path = ltrim($path, '/');
+ return get_base_path() . '/' . $path;
+}
+
+/**
+ * Get the full base URL including domain and protocol.
*/
function get_base_url() {
- $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
- $domainName = $_SERVER['HTTP_HOST'];
- // Remove admin/ if we are in it
- $script_dir = dirname($_SERVER['SCRIPT_NAME']);
- $script_dir = str_replace(['/admin', '/api'], '', $script_dir);
- if ($script_dir === '/') $script_dir = '';
+ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || ($_SERVER['SERVER_PORT'] ?? '') == 443) ? "https://" : "http://";
+ $domainName = $_SERVER['HTTP_HOST'] ?? 'localhost';
- return $protocol . $domainName . $script_dir . '/';
-}
+ return $protocol . $domainName . url();
+}
\ No newline at end of file
diff --git a/login.php b/login.php
index a66e114..967bed7 100644
--- a/login.php
+++ b/login.php
@@ -6,7 +6,7 @@ init_session();
// Redirect if already logged in
if (get_logged_user()) {
- header('Location: /admin/index.php');
+ header('Location: ' . url('admin/index.php'));
exit;
}
@@ -17,7 +17,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$password = $_POST['password'] ?? '';
if (login_user($username, $password)) {
- header('Location: /admin/index.php');
+ header('Location: ' . url('admin/index.php'));
exit;
} else {
$error = 'Invalid username or password.';
@@ -34,7 +34,7 @@ $settings = get_company_settings();
Login - = htmlspecialchars($settings['company_name']) ?>
-
+