Autosave: 20260222-172008

This commit is contained in:
Flatlogic Bot 2026-02-22 17:20:08 +00:00
parent 5db529f225
commit 2996ec35e3
15 changed files with 814 additions and 12 deletions

View File

@ -140,6 +140,16 @@ function isActive($page) {
<i class="bi bi-award me-2"></i> Loyalty
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('payment_types.php') ?>" href="payment_types.php">
<i class="bi bi-credit-card me-2"></i> Payment Types
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('integrations.php') ?>" href="integrations.php">
<i class="bi bi-plugin me-2"></i> Integrations
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('company.php') ?>" href="company.php">
<i class="bi bi-building me-2"></i> Company

170
admin/integrations.php Normal file
View File

@ -0,0 +1,170 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/WablasService.php';
$pdo = db();
$wablasTestResult = null;
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$provider = $_POST['provider'] ?? '';
$action = $_POST['action'] ?? 'save';
// Thawani
if ($provider === 'thawani') {
$keys = ['public_key', 'secret_key', 'environment'];
foreach ($keys as $k) {
$val = $_POST[$k] ?? '';
$stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
$stmt->execute(['thawani', $k, $val]);
}
$message = 'saved';
if ($action === 'save') {
header("Location: integrations.php?msg=saved");
exit;
}
}
// Wablas
if ($provider === 'wablas') {
$keys = ['domain', 'token', 'secret_key'];
foreach ($keys as $k) {
$val = $_POST[$k] ?? '';
$stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
$stmt->execute(['wablas', $k, $val]);
}
if ($action === 'save') {
header("Location: integrations.php?msg=saved");
exit;
} elseif ($action === 'test') {
// Instantiate service (loads from DB, which we just updated)
$wablasService = new WablasService($pdo);
$testPhone = $_POST['test_phone'] ?? '';
if (!empty($testPhone)) {
$wablasTestResult = $wablasService->sendMessage($testPhone, "Test message from Flatlogic POS. Connection verified!");
} else {
$wablasTestResult = $wablasService->testConnection();
}
}
}
}
// Fetch current settings
$stmt = $pdo->query("SELECT provider, setting_key, setting_value FROM integration_settings");
$allSettings = $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
function getSetting($settings, $provider, $key) {
if (isset($settings[$provider])) {
foreach ($settings[$provider] as $s) {
if ($s['setting_key'] === $key) return $s['setting_value'];
}
}
return '';
}
$thawaniEnv = getSetting($allSettings, 'thawani', 'environment');
$thawaniPub = getSetting($allSettings, 'thawani', 'public_key');
$thawaniSec = getSetting($allSettings, 'thawani', 'secret_key');
$wablasDom = getSetting($allSettings, 'wablas', 'domain');
$wablasTok = getSetting($allSettings, 'wablas', 'token');
$wablasSecKey = getSetting($allSettings, 'wablas', 'secret_key');
require_once __DIR__ . '/includes/header.php';
?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0 text-gray-800">Integrations</h2>
</div>
<?php if (isset($_GET['msg']) && $_GET['msg'] == 'saved'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
Settings saved successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if ($wablasTestResult): ?>
<div class="alert alert-<?= $wablasTestResult['success'] ? 'success' : 'danger' ?> alert-dismissible fade show" role="alert">
<strong>Wablas Test Result:</strong> <?= htmlspecialchars($wablasTestResult['message']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="row">
<!-- Thawani -->
<div class="col-md-6 mb-4">
<div class="card shadow h-100">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 fw-bold text-primary">Thawani Payments</h6>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="provider" value="thawani">
<div class="mb-3">
<label class="form-label">Environment</label>
<select class="form-select" name="environment">
<option value="sandbox" <?= $thawaniEnv == 'sandbox' ? 'selected' : '' ?>>Sandbox</option>
<option value="production" <?= $thawaniEnv == 'production' ? 'selected' : '' ?>>Production</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Public Key</label>
<input type="text" class="form-control" name="public_key" value="<?= htmlspecialchars($thawaniPub) ?>">
</div>
<div class="mb-3">
<label class="form-label">Secret Key</label>
<input type="password" class="form-control" name="secret_key" value="<?= htmlspecialchars($thawaniSec) ?>">
</div>
<button type="submit" name="action" value="save" class="btn btn-primary">Save Thawani Settings</button>
</form>
</div>
</div>
</div>
<!-- Wablas -->
<div class="col-md-6 mb-4">
<div class="card shadow h-100">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 fw-bold text-success">Wablas WhatsApp</h6>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="provider" value="wablas">
<div class="mb-3">
<label class="form-label">Domain</label>
<input type="text" class="form-control" name="domain" placeholder="https://..." value="<?= htmlspecialchars($wablasDom) ?>">
</div>
<div class="mb-3">
<label class="form-label">Token</label>
<input type="password" class="form-control" name="token" value="<?= htmlspecialchars($wablasTok) ?>">
</div>
<div class="mb-3">
<label class="form-label">Secret Key</label>
<input type="password" class="form-control" name="secret_key" value="<?= htmlspecialchars($wablasSecKey) ?>">
</div>
<div class="mb-3 border-top pt-3">
<label class="form-label text-muted small">Test Configuration</label>
<div class="input-group">
<input type="text" class="form-control" name="test_phone" placeholder="e.g. 62812345678" value="<?= htmlspecialchars($_POST['test_phone'] ?? '') ?>">
<button type="submit" name="action" value="test" class="btn btn-info text-white">Test & Send Message</button>
</div>
<small class="text-muted">Enter a phone number to send a real test message.</small>
</div>
<div class="d-flex justify-content-end">
<button type="submit" name="action" value="save" class="btn btn-success">Save Settings</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

View File

@ -0,0 +1,85 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = $_GET['id'] ?? null;
$pt = [
'name' => '',
'type' => 'cash',
'api_provider' => '',
'is_active' => 1
];
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM payment_types WHERE id = ?");
$stmt->execute([$id]);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existing) {
$pt = $existing;
} else {
header("Location: payment_types.php");
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'];
$type = $_POST['type'];
$api_provider = $_POST['api_provider'] ?: null;
$is_active = isset($_POST['is_active']) ? 1 : 0;
if ($id) {
$stmt = $pdo->prepare("UPDATE payment_types SET name = ?, type = ?, api_provider = ?, is_active = ? WHERE id = ?");
$stmt->execute([$name, $type, $api_provider, $is_active, $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO payment_types (name, type, api_provider, is_active) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $type, $api_provider, $is_active]);
}
header("Location: payment_types.php?msg=saved");
exit;
}
require_once __DIR__ . '/includes/header.php';
?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0 text-gray-800"><?= $id ? 'Edit' : 'Add' ?> Payment Type</h2>
</div>
<div class="card shadow mb-4">
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($pt['name']) ?>" required>
</div>
<div class="mb-3">
<label for="type" class="form-label">Type</label>
<select class="form-select" id="type" name="type" required>
<option value="cash" <?= $pt['type'] == 'cash' ? 'selected' : '' ?>>Cash</option>
<option value="card" <?= $pt['type'] == 'card' ? 'selected' : '' ?>>Card</option>
<option value="api" <?= $pt['type'] == 'api' ? 'selected' : '' ?>>API Integration</option>
</select>
</div>
<div class="mb-3">
<label for="api_provider" class="form-label">API Provider (if Type is API)</label>
<select class="form-select" id="api_provider" name="api_provider">
<option value="">None</option>
<option value="thawani" <?= $pt['api_provider'] == 'thawani' ? 'selected' : '' ?>>Thawani</option>
<option value="wablas" <?= $pt['api_provider'] == 'wablas' ? 'selected' : '' ?>>Wablas (WhatsApp)</option>
</select>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="is_active" name="is_active" <?= $pt['is_active'] ? 'checked' : '' ?>>
<label class="form-check-label" for="is_active">Active</label>
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="payment_types.php" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

88
admin/payment_types.php Normal file
View File

@ -0,0 +1,88 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
// Handle Delete
if (isset($_GET['delete_id'])) {
$stmt = $pdo->prepare("DELETE FROM payment_types WHERE id = ?");
$stmt->execute([$_GET['delete_id']]);
header("Location: payment_types.php?msg=deleted");
exit;
}
// Fetch Payment Types
$stmt = $pdo->query("SELECT * FROM payment_types ORDER BY id DESC");
$paymentTypes = $stmt->fetchAll(PDO::FETCH_ASSOC);
require_once __DIR__ . '/includes/header.php';
?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0 text-gray-800">Payment Types</h2>
<a href="payment_type_edit.php" class="btn btn-primary">
<i class="bi bi-plus-lg me-2"></i>Add Payment Type
</a>
</div>
<?php if (isset($_GET['msg']) && $_GET['msg'] == 'deleted'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
Payment Type deleted successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card shadow mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Name</th>
<th>Type</th>
<th>Provider</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($paymentTypes)): ?>
<tr><td colspan="6" class="text-center py-4">No payment types found.</td></tr>
<?php else: ?>
<?php foreach ($paymentTypes as $pt): ?>
<tr>
<td><?= $pt['id'] ?></td>
<td class="fw-medium"><?= htmlspecialchars($pt['name']) ?></td>
<td>
<span class="badge bg-secondary"><?= htmlspecialchars(ucfirst($pt['type'])) ?></span>
</td>
<td><?= htmlspecialchars($pt['api_provider'] ?? '-') ?></td>
<td>
<?php if ($pt['is_active']): ?>
<span class="badge bg-success">Active</span>
<?php else: ?>
<span class="badge bg-danger">Inactive</span>
<?php endif; ?>
</td>
<td class="text-end">
<a href="payment_type_edit.php?id=<?= $pt['id'] ?>" class="btn btn-sm btn-outline-primary me-1">
<i class="bi bi-pencil"></i>
</a>
<a href="payment_types.php?delete_id=<?= $pt['id'] ?>"
class="btn btn-sm btn-outline-danger"
onclick="return confirm('Are you sure you want to delete this payment type?');">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

84
api/kitchen.php Normal file
View File

@ -0,0 +1,84 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Fetch active kitchen orders
try {
// We want orders that are NOT completed or cancelled
// Status flow: pending -> preparing -> ready -> completed
// Kitchen sees: pending, preparing, ready
$stmt = $pdo->prepare("
SELECT
o.id, o.table_number, o.order_type, o.status, o.created_at, o.customer_name,
oi.quantity, p.name as product_name, v.name as variant_name
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
LEFT JOIN product_variants v ON oi.variant_id = v.id
WHERE o.status IN ('pending', 'preparing', 'ready')
ORDER BY o.created_at ASC
");
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$orders = [];
foreach ($rows as $row) {
$id = $row['id'];
if (!isset($orders[$id])) {
$orders[$id] = [
'id' => $row['id'],
'table_number' => $row['table_number'],
'order_type' => $row['order_type'],
'status' => $row['status'],
'created_at' => $row['created_at'],
'customer_name' => $row['customer_name'],
'items' => []
];
}
$orders[$id]['items'][] = [
'quantity' => $row['quantity'],
'product_name' => $row['product_name'],
'variant_name' => $row['variant_name']
];
}
echo json_encode(array_values($orders));
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Update order status
$data = json_decode(file_get_contents('php://input'), true);
$orderId = $data['order_id'] ?? null;
$newStatus = $data['status'] ?? null;
if (!$orderId || !$newStatus) {
http_response_code(400);
echo json_encode(['error' => 'Missing order_id or status']);
exit;
}
$allowedStatuses = ['pending', 'preparing', 'ready', 'completed', 'cancelled'];
if (!in_array($newStatus, $allowedStatuses)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid status']);
exit;
}
try {
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
$stmt->execute([$newStatus, $orderId]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
exit;
}

View File

@ -20,24 +20,41 @@ try {
? $data['order_type']
: 'dine-in';
// Get outlet_id from input, default to 1 if missing
$outlet_id = isset($data['outlet_id']) ? intval($data['outlet_id']) : 1;
$table_id = null;
$table_number = null;
if ($order_type === 'dine-in') {
$tid = $data['table_number'] ?? null; // Front-end sends ID as table_number
if ($tid) {
$stmt = $pdo->prepare("SELECT id, name FROM tables WHERE id = ?");
$stmt->execute([$tid]);
// Validate table exists AND belongs to the correct outlet
$stmt = $pdo->prepare("
SELECT t.id, t.name
FROM tables t
JOIN areas a ON t.area_id = a.id
WHERE t.id = ? AND a.outlet_id = ?
");
$stmt->execute([$tid, $outlet_id]);
$table = $stmt->fetch(PDO::FETCH_ASSOC);
if ($table) {
$table_id = $table['id'];
$table_number = $table['name'];
}
}
// If not found or not provided, leave null (Walk-in/Counter) or default to 1 if it exists
// If not found or not provided, leave null (Walk-in/Counter) or try to find a default table for this outlet
if (!$table_id) {
// Optional: try to find table 1
$stmt = $pdo->query("SELECT id, name FROM tables LIMIT 1");
// Optional: try to find the first available table for this outlet
$stmt = $pdo->prepare("
SELECT t.id, t.name
FROM tables t
JOIN areas a ON t.area_id = a.id
WHERE a.outlet_id = ?
LIMIT 1
");
$stmt->execute([$outlet_id]);
$table = $stmt->fetch(PDO::FETCH_ASSOC);
if ($table) {
$table_id = $table['id'];
@ -67,7 +84,7 @@ try {
$total_amount = isset($data['total_amount']) ? floatval($data['total_amount']) : 0.00;
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, total_amount, discount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
$stmt->execute([1, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $total_amount, $discount]);
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $total_amount, $discount]);
$order_id = $pdo->lastInsertId();
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)");

View File

@ -5,14 +5,18 @@ require_once __DIR__ . '/../db/config.php';
try {
$pdo = db();
// Fetch all tables with their area names
$outlet_id = isset($_GET['outlet_id']) ? intval($_GET['outlet_id']) : 1;
// Fetch all tables with their area names, filtered by outlet_id
$sql = "
SELECT t.id, t.name, t.capacity, a.name AS area_name
FROM tables t
LEFT JOIN areas a ON t.area_id = a.id
WHERE a.outlet_id = :outlet_id
ORDER BY a.name ASC, t.name ASC
";
$stmt = $pdo->query($sql);
$stmt = $pdo->prepare($sql);
$stmt->execute(['outlet_id' => $outlet_id]);
$tables = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch currently occupied table IDs
@ -22,8 +26,10 @@ try {
FROM orders
WHERE status NOT IN ('completed', 'cancelled')
AND table_id IS NOT NULL
AND outlet_id = :outlet_id
";
$occupiedStmt = $pdo->query($occupiedSql);
$occupiedStmt = $pdo->prepare($occupiedSql);
$occupiedStmt->execute(['outlet_id' => $outlet_id]);
$occupiedTableIds = $occupiedStmt->fetchAll(PDO::FETCH_COLUMN);
// Mark tables as occupied

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -168,7 +168,8 @@ document.addEventListener('DOMContentLoaded', () => {
container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"></div></div>';
tableSelectionModal.show();
fetch('api/tables.php')
const outletId = new URLSearchParams(window.location.search).get('outlet_id') || 1;
fetch(`api/tables.php?outlet_id=${outletId}`)
.then(res => res.json())
.then(data => {
if (data.success) {
@ -390,6 +391,7 @@ document.addEventListener('DOMContentLoaded', () => {
table_number: (orderType === 'dine-in') ? currentTableId : null,
order_type: orderType,
customer_id: custId || null,
outlet_id: new URLSearchParams(window.location.search).get('outlet_id') || 1,
total_amount: totalAmount,
discount: discount,
items: cart.map(item => ({

View File

@ -0,0 +1,20 @@
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 integration_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
provider VARCHAR(50) NOT NULL,
setting_key VARCHAR(100) NOT NULL,
setting_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_setting (provider, setting_key)
);
-- Attempt to add column, ignore if exists (standard SQL doesn't support IF NOT EXISTS for columns easily without procedures, but for this env we'll try)
-- We will run this via PHP and handle errors if column exists

View File

@ -0,0 +1,37 @@
<?php
class ThawaniService {
private $pdo;
private $publicKey;
private $secretKey;
private $isSandbox;
public function __construct($pdo) {
$this->pdo = $pdo;
$this->loadConfig();
}
private function loadConfig() {
$stmt = $this->pdo->prepare("SELECT setting_key, setting_value FROM integration_settings WHERE provider = 'thawani'");
$stmt->execute();
$settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$this->publicKey = $settings['public_key'] ?? '';
$this->secretKey = $settings['secret_key'] ?? '';
$this->isSandbox = ($settings['environment'] ?? 'sandbox') === 'sandbox';
}
public function createCheckoutSession($orderId, $amount, $customerData) {
// Implementation for creating a checkout session
// This is a placeholder for actual API integration
return [
'success' => true,
'session_id' => 'sess_' . uniqid(),
'redirect_url' => 'https://uat.thawani.om/pay/...' // Example URL
];
}
public function handleWebhook($payload) {
// Implementation for webhook handling
return true;
}
}

107
includes/WablasService.php Normal file
View File

@ -0,0 +1,107 @@
<?php
class WablasService {
private $pdo;
private $domain;
private $token;
private $secret_key;
public function __construct($pdo) {
$this->pdo = $pdo;
$this->loadConfig();
}
private function loadConfig() {
$stmt = $this->pdo->prepare("SELECT setting_key, setting_value FROM integration_settings WHERE provider = 'wablas'");
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$settings = [];
foreach ($rows as $r) {
$settings[$r['setting_key']] = $r['setting_value'];
}
$this->domain = $settings['domain'] ?? '';
$this->token = $settings['token'] ?? '';
$this->secret_key = $settings['secret_key'] ?? '';
}
public function testConnection() {
if (empty($this->domain) || empty($this->token)) {
return ['success' => false, 'message' => 'Missing Domain or Token'];
}
// Common Wablas endpoint to check device status
$url = rtrim($this->domain, '/') . '/api/device/info?token=' . $this->token;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
return ['success' => false, 'message' => 'Connection failed: ' . $curlError];
}
if ($httpCode >= 200 && $httpCode < 300) {
$data = json_decode($response, true);
// Some APIs return 200 even on logical error, check for status field if exists
if (isset($data['status']) && $data['status'] === false) {
return ['success' => false, 'message' => 'API Error: ' . ($data['message'] ?? 'Unknown error')];
}
return ['success' => true, 'message' => 'Connection successful! Device info retrieved.'];
}
return ['success' => false, 'message' => 'HTTP Error: ' . $httpCode];
}
public function sendMessage($phone, $message) {
if (empty($this->domain) || empty($this->token)) {
return ['success' => false, 'message' => 'Wablas configuration missing'];
}
$payload = [
'phone' => $phone,
'message' => $message,
'secret' => $this->secret_key
];
// Assuming standard endpoint. If users use v2, it might be /api/v2/send-message
$url = rtrim($this->domain, '/') . '/api/send-message';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: " . $this->token,
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($curlError) {
return ['success' => false, 'message' => 'cURL Error: ' . $curlError];
}
$data = json_decode($response, true);
$msg = $data['message'] ?? $response;
if ($httpCode >= 200 && $httpCode < 300) {
if (isset($data['status']) && $data['status'] === false) {
return ['success' => false, 'message' => 'Wablas Error: ' . ($data['message'] ?? 'Unknown error')];
}
return ['success' => true, 'message' => 'Message sent successfully. Response: ' . (is_string($msg) ? substr($msg, 0, 100) : 'OK')];
}
return ['success' => false, 'message' => 'HTTP Error ' . $httpCode . ': ' . (is_string($msg) ? $msg : 'Unknown')];
}
}

View File

@ -119,6 +119,9 @@ $settings = get_company_settings();
<a href="admin/" class="text-muted small text-decoration-none">
<i class="bi bi-shield-lock me-1"></i> Staff Login
</a>
<a href="kitchen.php" class="text-muted small text-decoration-none ms-3">
<i class="bi bi-egg-fried me-1"></i> Kitchen Display
</a>
</div>
</div>

173
kitchen.php Normal file
View File

@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kitchen Display System</title>
<!-- Re-using existing CSS if any, or adding Bootstrap/Custom -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="assets/css/custom.css" rel="stylesheet">
<style>
body { background-color: #f4f6f9; }
.order-card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
transition: transform 0.2s;
height: 100%;
}
.order-card:hover { transform: translateY(-2px); }
.card-header {
background-color: #fff;
border-bottom: 1px solid #eee;
border-radius: 12px 12px 0 0 !important;
padding: 1rem;
font-weight: 600;
}
.status-pending { border-left: 5px solid #ffc107; }
.status-preparing { border-left: 5px solid #17a2b8; }
.status-ready { border-left: 5px solid #28a745; }
.badge-pending { background-color: #ffc107; color: #000; }
.badge-preparing { background-color: #17a2b8; }
.badge-ready { background-color: #28a745; }
.item-list { list-style: none; padding: 0; margin: 0; }
.item-list li {
padding: 0.5rem 0;
border-bottom: 1px dashed #eee;
display: flex;
justify-content: space-between;
}
.item-list li:last-child { border-bottom: none; }
.item-qty { font-weight: bold; margin-right: 10px; color: #333; }
#kitchen-grid { padding: 20px; }
</style>
</head>
<body>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center py-3 px-4 bg-white shadow-sm mb-4">
<h2 class="h4 mb-0">Kitchen Display</h2>
<div>
<span id="clock" class="text-muted me-3"></span>
<a href="index.php" class="btn btn-outline-secondary btn-sm">Back to Home</a>
</div>
</div>
<div id="kitchen-grid" class="row g-4">
<!-- Orders will be injected here -->
<div class="col-12 text-center text-muted py-5">
Loading orders...
</div>
</div>
</div>
<script>
async function fetchOrders() {
try {
const response = await fetch('api/kitchen.php');
if (!response.ok) throw new Error('Network response was not ok');
const orders = await response.json();
renderOrders(orders);
} catch (error) {
console.error('Error fetching orders:', error);
}
}
function renderOrders(orders) {
const grid = document.getElementById('kitchen-grid');
if (orders.length === 0) {
grid.innerHTML = '<div class="col-12 text-center text-muted py-5">No active orders</div>';
return;
}
grid.innerHTML = orders.map(order => {
let actionBtn = '';
let statusBadge = '';
let cardClass = 'order-card ';
if (order.status === 'pending') {
statusBadge = '<span class="badge badge-pending">Pending</span>';
cardClass += 'status-pending';
actionBtn = `<button class="btn btn-info btn-sm w-100 text-white" onclick="updateStatus(${order.id}, 'preparing')">Start Preparing</button>`;
} else if (order.status === 'preparing') {
statusBadge = '<span class="badge badge-preparing">Preparing</span>';
cardClass += 'status-preparing';
actionBtn = `<button class="btn btn-success btn-sm w-100" onclick="updateStatus(${order.id}, 'ready')">Mark Ready</button>`;
} else if (order.status === 'ready') {
statusBadge = '<span class="badge badge-ready">Ready</span>';
cardClass += 'status-ready';
actionBtn = `<button class="btn btn-secondary btn-sm w-100" onclick="updateStatus(${order.id}, 'completed')">Serve / Complete</button>`;
}
const itemsHtml = order.items.map(item => `
<li>
<span><span class="item-qty">${item.quantity}x</span> ${item.product_name} ${item.variant_name ? '<small class="text-muted">('+item.variant_name+')</small>' : ''}</span>
</li>
`).join('');
// Calculate time elapsed
const created = new Date(order.created_at);
const timeString = created.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
return `
<div class="col-md-4 col-lg-3">
<div class="${cardClass}">
<div class="card-header d-flex justify-content-between align-items-center">
<div>#${order.id} <small class="text-muted">${timeString}</small></div>
${statusBadge}
</div>
<div class="card-body">
<h5 class="card-title mb-3">
${order.order_type === 'dine-in' ? `Table ${order.table_number}` : order.order_type.toUpperCase()}
${order.customer_name ? `<br><small class="text-muted">${order.customer_name}</small>` : ''}
</h5>
<ul class="item-list mb-3">
${itemsHtml}
</ul>
</div>
<div class="card-footer bg-white border-top-0 pb-3">
${actionBtn}
</div>
</div>
</div>
`;
}).join('');
}
async function updateStatus(orderId, newStatus) {
if (!confirm(`Move order #${orderId} to ${newStatus}?`)) return;
try {
const response = await fetch('api/kitchen.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_id: orderId, status: newStatus })
});
const result = await response.json();
if (result.success) {
fetchOrders(); // Refresh immediately
} else {
alert('Error: ' + (result.error || 'Failed to update'));
}
} catch (error) {
console.error('Error updating status:', error);
alert('Failed to connect to server');
}
}
// Clock
setInterval(() => {
document.getElementById('clock').textContent = new Date().toLocaleTimeString();
}, 1000);
// Poll orders
fetchOrders();
setInterval(fetchOrders, 10000); // Poll every 10s
</script>
</body>
</html>