adding cash register

This commit is contained in:
Flatlogic Bot 2026-02-18 12:01:04 +00:00
parent b608d55567
commit 038a4de94d
9 changed files with 1055 additions and 78 deletions

View File

@ -0,0 +1,12 @@
-- Create pos_payments table to support split payments and detailed reporting
CREATE TABLE IF NOT EXISTS pos_payments (
id INT AUTO_INCREMENT PRIMARY KEY,
transaction_id INT NOT NULL,
payment_method VARCHAR(50) NOT NULL,
amount DECIMAL(15, 3) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (transaction_id) REFERENCES pos_transactions(id) ON DELETE CASCADE
);
-- Add register_session_id to pos_transactions if missing (already exists but for idempotency)
-- ALTER TABLE pos_transactions ADD COLUMN IF NOT EXISTS register_session_id INT NULL;

845
index.php
View File

@ -137,6 +137,121 @@ if (isset($_GET['action']) && $_GET['action'] === 'logout') {
exit; exit;
} }
// --- POS AJAX Handlers ---
if (isset($_GET['action']) || isset($_POST['action'])) {
$action = $_GET['action'] ?? $_POST['action'] ?? '';
if ($action === 'validate_discount') {
header('Content-Type: application/json');
$code = $_GET['code'] ?? '';
$stmt = db()->prepare("SELECT * FROM discount_codes WHERE code = ? AND status = 'active' AND (expiry_date IS NULL OR expiry_date >= CURDATE())");
$stmt->execute([$code]);
$discount = $stmt->fetch(PDO::FETCH_ASSOC);
if ($discount) {
echo json_encode(['success' => true, 'discount' => $discount]);
} else {
echo json_encode(['success' => false, 'error' => 'Invalid or expired discount code']);
}
exit;
}
if ($action === 'get_held_carts') {
header('Content-Type: application/json');
$stmt = db()->query("SELECT h.*, c.name as customer_name FROM pos_held_carts h LEFT JOIN customers c ON h.customer_id = c.id ORDER BY h.created_at DESC");
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
exit;
}
if ($action === 'hold_pos_cart') {
header('Content-Type: application/json');
$name = $_POST['cart_name'] ?? 'Untitled Cart';
$items = $_POST['items'] ?? '[]';
$customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
$stmt = db()->prepare("INSERT INTO pos_held_carts (cart_name, items_json, customer_id) VALUES (?, ?, ?)");
$stmt->execute([$name, $items, $customer_id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_held_cart') {
header('Content-Type: application/json');
$id = (int)$_POST['id'];
$stmt = db()->prepare("DELETE FROM pos_held_carts WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'save_pos_transaction') {
header('Content-Type: application/json');
$db = db();
try {
$db->beginTransaction();
$customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
$payments = json_decode($_POST['payments'] ?? '[]', true);
$items = json_decode($_POST['items'] ?? '[]', true);
$total_amount = (float)($_POST['total_amount'] ?? 0);
$discount_code_id = !empty($_POST['discount_code_id']) ? (int)$_POST['discount_code_id'] : null;
$discount_amount = (float)($_POST['discount_amount'] ?? 0);
$loyalty_redeemed = (float)($_POST['loyalty_redeemed'] ?? 0);
$net_amount = $total_amount - $discount_amount - $loyalty_redeemed;
$transaction_no = 'POS-' . time() . rand(10, 99);
$session_id = $_SESSION['register_session_id'] ?? null;
// Insert Transaction
$stmt = $db->prepare("INSERT INTO pos_transactions (transaction_no, customer_id, total_amount, discount_code_id, discount_amount, loyalty_points_redeemed, net_amount, register_session_id, created_by, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'completed')");
$stmt->execute([$transaction_no, $customer_id, $total_amount, $discount_code_id, $discount_amount, $loyalty_redeemed, $net_amount, $session_id, $_SESSION['user_id']]);
$transaction_id = (int)$db->lastInsertId();
// Insert Items & Update Stock
$stmtItem = $db->prepare("INSERT INTO pos_items (transaction_id, product_id, quantity, unit_price, subtotal) VALUES (?, ?, ?, ?, ?)");
$stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
foreach ($items as $item) {
$sub = (float)$item['price'] * (float)$item['qty'];
$stmtItem->execute([$transaction_id, $item['id'], $item['qty'], $item['price'], $sub]);
$stmtStock->execute([$item['qty'], $item['id']]);
}
// Insert Payments
$stmtPay = $db->prepare("INSERT INTO pos_payments (transaction_id, payment_method, amount) VALUES (?, ?, ?)");
foreach ($payments as $p) {
$stmtPay->execute([$transaction_id, $p['method'], $p['amount']]);
}
// Update Loyalty Points if customer exists
if ($customer_id) {
// Earn points
$points_earned = floor($net_amount);
$stmtPoints = $db->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? + ? WHERE id = ?");
$stmtPoints->execute([$loyalty_redeemed * 100, $points_earned, $customer_id]);
// Record transactions
if ($points_earned > 0) {
$db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'earned', ?)")
->execute([$customer_id, $transaction_id, $points_earned, "Earned from POS order #$transaction_no"]);
}
if ($loyalty_redeemed > 0) {
$db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'redeemed', ?)")
->execute([$customer_id, $transaction_id, -$loyalty_redeemed * 100, "Redeemed for POS order #$transaction_no"]);
}
// Update transaction with points earned
$db->prepare("UPDATE pos_transactions SET loyalty_points_earned = ? WHERE id = ?")->execute([$points_earned, $transaction_id]);
}
$db->commit();
echo json_encode(['success' => true, 'invoice_id' => $transaction_id, 'transaction_no' => $transaction_no]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
}
// Redirect to login if not authenticated // Redirect to login if not authenticated
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
?> ?>
@ -535,6 +650,44 @@ if (isset($_POST['add_hr_department'])) {
} }
} }
// --- POS Devices Handlers ---
if (isset($_POST['add_pos_device'])) {
$name = $_POST['device_name'] ?? '';
$type = $_POST['device_type'] ?? 'scale';
$conn = $_POST['connection_type'] ?? 'usb';
$ip = $_POST['ip_address'] ?? '';
$port = $_POST['port'] ? (int)$_POST['port'] : null;
$baud = $_POST['baud_rate'] ? (int)$_POST['baud_rate'] : null;
if ($name) {
$stmt = db()->prepare("INSERT INTO pos_devices (device_name, device_type, connection_type, ip_address, port, baud_rate) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$name, $type, $conn, $ip, $port, $baud]);
$message = "Device added successfully!";
}
}
if (isset($_POST['edit_pos_device'])) {
$id = (int)$_POST['id'];
$name = $_POST['device_name'] ?? '';
$type = $_POST['device_type'] ?? 'scale';
$conn = $_POST['connection_type'] ?? 'usb';
$ip = $_POST['ip_address'] ?? '';
$port = $_POST['port'] ? (int)$_POST['port'] : null;
$baud = $_POST['baud_rate'] ? (int)$_POST['baud_rate'] : null;
$status = $_POST['status'] ?? 'active';
if ($id && $name) {
$stmt = db()->prepare("UPDATE pos_devices SET device_name = ?, device_type = ?, connection_type = ?, ip_address = ?, port = ?, baud_rate = ?, status = ? WHERE id = ?");
$stmt->execute([$name, $type, $conn, $ip, $port, $baud, $status, $id]);
$message = "Device updated successfully!";
}
}
if (isset($_POST['delete_pos_device'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM pos_devices WHERE id = ?");
$stmt->execute([$id]);
$message = "Device deleted successfully!";
}
}
if (isset($_POST['update_profile'])) { if (isset($_POST['update_profile'])) {
$id = $_SESSION['user_id']; $id = $_SESSION['user_id'];
$username = $_POST['username'] ?? ''; $username = $_POST['username'] ?? '';
@ -606,6 +759,76 @@ if (isset($_POST['add_hr_department'])) {
} }
} }
// --- Cash Register & Session Handlers ---
if (isset($_POST['add_cash_register'])) {
$name = $_POST['name'] ?? '';
if ($name) {
$stmt = db()->prepare("INSERT INTO cash_registers (name) VALUES (?)");
$stmt->execute([$name]);
$message = "Cash Register added successfully!";
}
}
if (isset($_POST['edit_cash_register'])) {
$id = (int)$_POST['id'];
$name = $_POST['name'] ?? '';
$status = $_POST['status'] ?? 'active';
if ($id && $name) {
$stmt = db()->prepare("UPDATE cash_registers SET name = ?, status = ? WHERE id = ?");
$stmt->execute([$name, $status, $id]);
$message = "Cash Register updated successfully!";
}
}
if (isset($_POST['delete_cash_register'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM cash_registers WHERE id = ?");
$stmt->execute([$id]);
$message = "Cash Register deleted successfully!";
}
}
if (isset($_POST['open_register'])) {
$register_id = (int)$_POST['register_id'];
$user_id = $_SESSION['user_id'];
$opening_balance = (float)$_POST['opening_balance'];
// Check if user already has an open session
$check = db()->prepare("SELECT id FROM register_sessions WHERE user_id = ? AND status = 'open'");
$check->execute([$user_id]);
if ($check->fetch()) {
$message = "Error: You already have an open register session.";
} else {
$stmt = db()->prepare("INSERT INTO register_sessions (register_id, user_id, opening_balance, status) VALUES (?, ?, ?, 'open')");
$stmt->execute([$register_id, $user_id, $opening_balance]);
$_SESSION['register_session_id'] = db()->lastInsertId();
$message = "Register opened successfully!";
}
}
if (isset($_POST['close_register'])) {
$session_id = (int)$_POST['session_id'];
$cash_in_hand = (float)$_POST['cash_in_hand'];
$notes = $_POST['notes'] ?? '';
// Calculate expected closing balance
// Opening + Sum of POS Transactions (Cash) - Any cash outflows (if any)
$session = db()->prepare("SELECT opening_balance FROM register_sessions WHERE id = ?");
$session->execute([$session_id]);
$opening = (float)$session->fetchColumn();
$sales = db()->prepare("SELECT SUM(p.amount) FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed' AND p.payment_method = 'cash'");
$sales->execute([$session_id]);
$cash_sales = (float)$sales->fetchColumn();
$expected = $opening + $cash_sales;
$stmt = db()->prepare("UPDATE register_sessions SET closing_balance = ?, cash_in_hand = ?, closed_at = CURRENT_TIMESTAMP, status = 'closed', notes = ? WHERE id = ?");
$stmt->execute([$expected, $cash_in_hand, $notes, $session_id]);
unset($_SESSION['register_session_id']);
$message = "Register closed successfully!";
}
// Routing & Data Fetching // Routing & Data Fetching
$page = $_GET['page'] ?? 'dashboard'; $page = $_GET['page'] ?? 'dashboard';
@ -642,8 +865,11 @@ $page_permissions = [
'hr_payroll' => 'hr_view', 'hr_payroll' => 'hr_view',
'role_groups' => 'users_view', 'role_groups' => 'users_view',
'users' => 'users_view', 'users' => 'users_view',
'scale_devices' => 'users_view',
'backups' => 'users_view', 'backups' => 'users_view',
'logs' => 'users_view', 'logs' => 'users_view',
'cash_registers' => 'users_view',
'register_sessions' => 'pos_view',
]; ];
if (isset($page_permissions[$page]) && !can($page_permissions[$page])) { if (isset($page_permissions[$page]) && !can($page_permissions[$page])) {
@ -1189,6 +1415,30 @@ switch ($page) {
case 'devices': case 'devices':
$data['devices'] = db()->query("SELECT * FROM hr_biometric_devices ORDER BY id DESC")->fetchAll(); $data['devices'] = db()->query("SELECT * FROM hr_biometric_devices ORDER BY id DESC")->fetchAll();
break; break;
case 'scale_devices':
$data['scale_devices'] = db()->query("SELECT * FROM pos_devices ORDER BY id DESC")->fetchAll();
break;
case 'cash_registers':
$data['cash_registers'] = db()->query("SELECT * FROM cash_registers ORDER BY id DESC")->fetchAll();
break;
case 'register_sessions':
$where = ["1=1"];
$params = [];
if (!can('users_view')) {
$where[] = "s.user_id = ?";
$params[] = $_SESSION['user_id'];
}
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT s.*, r.name as register_name, u.username
FROM register_sessions s
JOIN cash_registers r ON s.register_id = r.id
JOIN users u ON s.user_id = u.id
WHERE $whereSql
ORDER BY s.id DESC");
$stmt->execute($params);
$data['sessions'] = $stmt->fetchAll();
$data['cash_registers'] = db()->query("SELECT * FROM cash_registers WHERE status = 'active'")->fetchAll();
break;
default: default:
$data['customers'] = db()->query("SELECT * FROM customers WHERE type = 'customer' ORDER BY id DESC LIMIT 5")->fetchAll(); $data['customers'] = db()->query("SELECT * FROM customers WHERE type = 'customer' ORDER BY id DESC LIMIT 5")->fetchAll();
// Dashboard stats // Dashboard stats
@ -1465,6 +1715,15 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
<a href="index.php?page=users" class="nav-link <?= $page === 'users' ? 'active' : '' ?>"> <a href="index.php?page=users" class="nav-link <?= $page === 'users' ? 'active' : '' ?>">
<i class="fas fa-users-gear"></i> <span data-en="Users" data-ar="المستخدمين">Users</span> <i class="fas fa-users-gear"></i> <span data-en="Users" data-ar="المستخدمين">Users</span>
</a> </a>
<a href="index.php?page=cash_registers" class="nav-link <?= $page === 'cash_registers' ? 'active' : '' ?>">
<i class="fas fa-cash-register"></i> <span data-en="Cash Registers" data-ar="خزائن الكاشير">Cash Registers</span>
</a>
<a href="index.php?page=register_sessions" class="nav-link <?= $page === 'register_sessions' ? 'active' : '' ?>">
<i class="fas fa-clock-rotate-left"></i> <span data-en="Register Sessions" data-ar="جلسات الكاشير">Register Sessions</span>
</a>
<a href="index.php?page=scale_devices" class="nav-link <?= $page === 'scale_devices' ? 'active' : '' ?>">
<i class="fas fa-microchip"></i> <span data-en="Devices" data-ar="الأجهزة">Devices</span>
</a>
<a href="index.php?page=backups" class="nav-link <?= $page === 'backups' ? 'active' : '' ?>"> <a href="index.php?page=backups" class="nav-link <?= $page === 'backups' ? 'active' : '' ?>">
<i class="fas fa-database"></i> <span data-en="Backups" data-ar="نسخ احتياطي">Backups</span> <i class="fas fa-database"></i> <span data-en="Backups" data-ar="نسخ احتياطي">Backups</span>
</a> </a>
@ -1521,6 +1780,9 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
'users' => ['en' => 'User Management', 'ar' => 'إدارة المستخدمين'], 'users' => ['en' => 'User Management', 'ar' => 'إدارة المستخدمين'],
'backups' => ['en' => 'Database Backups', 'ar' => 'نسخ قاعدة البيانات'], 'backups' => ['en' => 'Database Backups', 'ar' => 'نسخ قاعدة البيانات'],
'role_groups' => ['en' => 'Role Groups', 'ar' => 'مجموعات الأدوار'], 'role_groups' => ['en' => 'Role Groups', 'ar' => 'مجموعات الأدوار'],
'scale_devices' => ['en' => 'POS Devices', 'ar' => 'أجهزة نقاط البيع'],
'cash_registers' => ['en' => 'Cash Registers', 'ar' => 'خزائن الكاشير'],
'register_sessions' => ['en' => 'Register Sessions', 'ar' => 'جلسات الكاشير'],
]; ];
$currTitle = $titles[$page] ?? $titles['dashboard']; $currTitle = $titles[$page] ?? $titles['dashboard'];
?> ?>
@ -5174,6 +5436,188 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</div> </div>
</div> </div>
<?php elseif ($page === 'scale_devices'): ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold" data-en="POS Devices" data-ar="أجهزة نقاط البيع">POS Devices</h5>
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addScaleDeviceModal">
<i class="fas fa-plus me-1"></i> <span data-en="Add Device" data-ar="إضافة جهاز">Add Device</span>
</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th data-en="Device Name" data-ar="اسم الجهاز">Device Name</th>
<th data-en="Type" data-ar="النوع">Type</th>
<th data-en="Connection" data-ar="الاتصال">Connection</th>
<th data-en="Details" data-ar="التفاصيل">Details</th>
<th data-en="Status" data-ar="الحالة">Status</th>
<th class="text-end" data-en="Actions" data-ar="الإجراءات">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($data['scale_devices'] as $d): ?>
<tr>
<td>
<div class="fw-bold"><?= htmlspecialchars($d['device_name']) ?></div>
</td>
<td>
<span class="badge bg-info-subtle text-info text-capitalize"><?= $d['device_type'] ?></span>
</td>
<td>
<span class="badge bg-secondary-subtle text-secondary text-uppercase"><?= $d['connection_type'] ?></span>
</td>
<td class="small text-muted">
<?php if ($d['connection_type'] === 'network'): ?>
<?= htmlspecialchars((string)$d['ip_address']) ?>:<?= $d['port'] ?>
<?php elseif ($d['connection_type'] === 'serial'): ?>
Baud: <?= $d['baud_rate'] ?>
<?php else: ?>
USB Interface
<?php endif; ?>
</td>
<td>
<span class="badge bg-<?= $d['status'] === 'active' ? 'success' : 'danger' ?>-subtle text-<?= $d['status'] === 'active' ? 'success' : 'danger' ?> text-capitalize"><?= $d['status'] ?></span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light rounded-pill px-3" data-bs-toggle="modal" data-bs-target="#editScaleDeviceModal<?= $d['id'] ?>">
<i class="fas fa-edit me-1"></i> <span data-en="Edit" data-ar="تعديل">Edit</span>
</button>
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
<input type="hidden" name="id" value="<?= $d['id'] ?>">
<button type="submit" name="delete_pos_device" class="btn btn-sm btn-outline-danger border-0 rounded-pill">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
<!-- Edit Device Modal -->
<div class="modal fade" id="editScaleDeviceModal<?= $d['id'] ?>" tabindex="-1">
<div class="modal-dialog border-0">
<div class="modal-content shadow border-0 rounded-4">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold" data-en="Edit Device" data-ar="تعديل الجهاز">Edit Device</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<input type="hidden" name="id" value="<?= $d['id'] ?>">
<div class="modal-body">
<div class="mb-3">
<label class="form-label" data-en="Device Name" data-ar="اسم الجهاز">Device Name</label>
<input type="text" name="device_name" class="form-control" value="<?= htmlspecialchars($d['device_name']) ?>" required>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label" data-en="Device Type" data-ar="نوع الجهاز">Device Type</label>
<select name="device_type" class="form-select">
<option value="scale" <?= $d['device_type'] === 'scale' ? 'selected' : '' ?>>Weight Scale</option>
<option value="printer" <?= $d['device_type'] === 'printer' ? 'selected' : '' ?>>Receipt Printer</option>
<option value="display" <?= $d['device_type'] === 'display' ? 'selected' : '' ?>>Customer Display</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label" data-en="Connection Type" data-ar="نوع الاتصال">Connection Type</label>
<select name="connection_type" class="form-select">
<option value="usb" <?= $d['connection_type'] === 'usb' ? 'selected' : '' ?>>USB</option>
<option value="network" <?= $d['connection_type'] === 'network' ? 'selected' : '' ?>>Network (TCP/IP)</option>
<option value="serial" <?= $d['connection_type'] === 'serial' ? 'selected' : '' ?>>Serial (RS232)</option>
</select>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-8">
<label class="form-label" data-en="IP Address" data-ar="عنوان IP">IP Address</label>
<input type="text" name="ip_address" class="form-control" value="<?= htmlspecialchars((string)$d['ip_address']) ?>">
</div>
<div class="col-md-4">
<label class="form-label" data-en="Port" data-ar="المنفذ">Port</label>
<input type="number" name="port" class="form-control" value="<?= $d['port'] ?>">
</div>
</div>
<div class="mb-3">
<label class="form-label" data-en="Baud Rate" data-ar="معدل الباود">Baud Rate</label>
<input type="number" name="baud_rate" class="form-control" value="<?= $d['baud_rate'] ?>">
</div>
<div class="mb-3">
<label class="form-label" data-en="Status" data-ar="الحالة">Status</label>
<select name="status" class="form-select">
<option value="active" <?= $d['status'] === 'active' ? 'selected' : '' ?>>Active</option>
<option value="inactive" <?= $d['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
</select>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
<button type="submit" name="edit_pos_device" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
</div>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Add Scale Device Modal -->
<div class="modal fade" id="addScaleDeviceModal" tabindex="-1">
<div class="modal-dialog border-0">
<div class="modal-content shadow border-0 rounded-4">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold" data-en="Add POS Device" data-ar="إضافة جهاز">Add POS Device</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="mb-3">
<label class="form-label" data-en="Device Name" data-ar="اسم الجهاز">Device Name</label>
<input type="text" name="device_name" class="form-control" required placeholder="e.g. Counter 1 Scale">
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label" data-en="Device Type" data-ar="نوع الجهاز">Device Type</label>
<select name="device_type" class="form-select">
<option value="scale">Weight Scale</option>
<option value="printer">Receipt Printer</option>
<option value="display">Customer Display</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label" data-en="Connection Type" data-ar="نوع الاتصال">Connection Type</label>
<select name="connection_type" class="form-select">
<option value="usb">USB</option>
<option value="network">Network (TCP/IP)</option>
<option value="serial">Serial (RS232)</option>
</select>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-8">
<label class="form-label" data-en="IP Address" data-ar="عنوان IP">IP Address</label>
<input type="text" name="ip_address" class="form-control" placeholder="192.168.1.50">
</div>
<div class="col-md-4">
<label class="form-label" data-en="Port" data-ar="المنفذ">Port</label>
<input type="number" name="port" class="form-control" placeholder="9100">
</div>
</div>
<div class="mb-3">
<label class="form-label" data-en="Baud Rate" data-ar="معدل الباود">Baud Rate</label>
<input type="number" name="baud_rate" class="form-control" placeholder="9600">
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
<button type="submit" name="add_pos_device" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
</div>
</form>
</div>
</div>
</div>
<?php elseif ($page === 'my_profile'): ?> <?php elseif ($page === 'my_profile'): ?>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
@ -5660,6 +6104,407 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
</table> </table>
</div> </div>
</div> </div>
<?php elseif ($page === 'cash_registers'): ?>
<div class="card p-4 rounded-4 shadow-sm border-0">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h5 class="m-0 fw-bold text-primary" data-en="Cash Registers Management" data-ar="إدارة خزائن الكاشير">Cash Registers Management</h5>
<p class="text-muted small mb-0">Define your shop counters and registers.</p>
</div>
<button class="btn btn-primary rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#addRegisterModal">
<i class="bi bi-plus-lg me-2"></i> <span data-en="Add Register" data-ar="إضافة خزينة">Add Register</span>
</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th data-en="ID" data-ar="المعرف">ID</th>
<th data-en="Name" data-ar="الاسم">Name</th>
<th data-en="Status" data-ar="الحالة">Status</th>
<th data-en="Created At" data-ar="تاريخ الإنشاء">Created At</th>
<th class="text-end" data-en="Actions" data-ar="الإجراءات">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($data['cash_registers'] as $r): ?>
<tr>
<td>#<?= $r['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($r['name']) ?></td>
<td>
<span class="badge rounded-pill <?= $r['status'] === 'active' ? 'bg-success' : 'bg-danger' ?>">
<?= ucfirst($r['status']) ?>
</span>
</td>
<td><?= $r['created_at'] ?></td>
<td class="text-end">
<button class="btn btn-sm btn-light rounded-circle" data-bs-toggle="modal" data-bs-target="#editRegisterModal<?= $r['id'] ?>"><i class="bi bi-pencil text-primary"></i></button>
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
<input type="hidden" name="id" value="<?= $r['id'] ?>">
<button type="submit" name="delete_cash_register" class="btn btn-sm btn-light rounded-circle"><i class="bi bi-trash text-danger"></i></button>
</form>
<!-- Edit Modal -->
<div class="modal fade" id="editRegisterModal<?= $r['id'] ?>" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title">Edit Register</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body text-start">
<input type="hidden" name="id" value="<?= $r['id'] ?>">
<div class="mb-3">
<label class="form-label">Register Name</label>
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($r['name']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="active" <?= $r['status'] === 'active' ? 'selected' : '' ?>>Active</option>
<option value="inactive" <?= $r['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="edit_cash_register" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Add Modal -->
<div class="modal fade" id="addRegisterModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title">Add Cash Register</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Register Name</label>
<input type="text" name="name" class="form-control" placeholder="Counter 1" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="add_cash_register" class="btn btn-primary">Add Register</button>
</div>
</form>
</div>
</div>
</div>
<?php elseif ($page === 'register_sessions'): ?>
<div class="card p-4 rounded-4 shadow-sm border-0 mb-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h5 class="m-0 fw-bold text-primary" data-en="Register Sessions" data-ar="جلسات الكاشير">Register Sessions</h5>
<p class="text-muted small mb-0">Manage daily opening and closing of cash registers.</p>
</div>
<div>
<?php
$active_session = db()->prepare("SELECT s.*, r.name as register_name FROM register_sessions s JOIN cash_registers r ON s.register_id = r.id WHERE s.user_id = ? AND s.status = 'open'");
$active_session->execute([$_SESSION['user_id']]);
$session = $active_session->fetch();
?>
<?php if (!$session): ?>
<button class="btn btn-success rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#openRegisterModal">
<i class="bi bi-unlock me-2"></i> <span data-en="Open Register" data-ar="فتح الخزينة">Open Register</span>
</button>
<?php else: ?>
<button class="btn btn-danger rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#closeRegisterModal">
<i class="bi bi-lock me-2"></i> <span data-en="Close Register" data-ar="إغلاق الخزينة">Close Register</span>
</button>
<?php endif; ?>
</div>
</div>
<?php if ($session): ?>
<div class="alert alert-info border-0 shadow-sm d-flex justify-content-between align-items-center mb-4">
<div>
<i class="bi bi-info-circle-fill me-2"></i>
Current Open Register: <strong><?= htmlspecialchars($session['register_name']) ?></strong> |
Opened At: <strong><?= $session['opened_at'] ?></strong> |
Opening Balance: <strong>OMR <?= number_format((float)$session['opening_balance'], 3) ?></strong>
</div>
<div>
<a href="index.php?page=pos" class="btn btn-sm btn-primary rounded-pill px-3">Go to POS</a>
</div>
</div>
<?php endif; ?>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th data-en="Register" data-ar="الخزينة">Register</th>
<th data-en="Cashier" data-ar="الكاشير">Cashier</th>
<th data-en="Opened At" data-ar="وقت الفتح">Opened At</th>
<th data-en="Closed At" data-ar="وقت الإغلاق">Closed At</th>
<th data-en="Opening Bal." data-ar="رصيد الافتتاح">Opening Bal.</th>
<th data-en="Expected Bal." data-ar="الرصيد المتوقع">Expected Bal.</th>
<th data-en="Cash in Hand" data-ar="النقد الفعلي">Cash in Hand</th>
<th data-en="Difference" data-ar="الفرق">Difference</th>
<th data-en="Status" data-ar="الحالة">Status</th>
<th class="text-end" data-en="Report" data-ar="تقرير">Report</th>
</tr>
</thead>
<tbody>
<?php foreach ($data['sessions'] as $s): ?>
<tr>
<td class="fw-bold"><?= htmlspecialchars($s['register_name']) ?></td>
<td><?= htmlspecialchars($s['username']) ?></td>
<td><?= $s['opened_at'] ?></td>
<td><?= $s['closed_at'] ?? '---' ?></td>
<td>OMR <?= number_format((float)$s['opening_balance'], 3) ?></td>
<td>OMR <?= number_format((float)($s['closing_balance'] ?? 0), 3) ?></td>
<td>OMR <?= number_format((float)($s['cash_in_hand'] ?? 0), 3) ?></td>
<td>
<?php if ($s['status'] === 'closed'):
$diff = $s['cash_in_hand'] - $s['closing_balance'];
$color = $diff == 0 ? 'text-success' : ($diff > 0 ? 'text-info' : 'text-danger');
?>
<span class="<?= $color ?> fw-bold">OMR <?= number_format($diff, 3) ?></span>
<?php else: ?>---<?php endif; ?>
</td>
<td>
<span class="badge rounded-pill <?= $s['status'] === 'open' ? 'bg-success' : 'bg-secondary' ?>">
<?= ucfirst($s['status']) ?>
</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light rounded-circle" data-bs-toggle="modal" data-bs-target="#sessionReportModal<?= $s['id'] ?>"><i class="bi bi-file-earmark-text text-primary"></i></button>
<!-- Session Report Modal -->
<div class="modal fade" id="sessionReportModal<?= $s['id'] ?>" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title">Session Cashflow Report (#<?= $s['id'] ?>)</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-start">
<div class="row g-3">
<div class="col-md-6">
<p class="mb-1 text-muted small">Register</p>
<h6><?= htmlspecialchars($s['register_name']) ?></h6>
</div>
<div class="col-md-6">
<p class="mb-1 text-muted small">Cashier</p>
<h6><?= htmlspecialchars($s['username']) ?></h6>
</div>
<div class="col-md-6">
<p class="mb-1 text-muted small">Opened At</p>
<h6><?= $s['opened_at'] ?></h6>
</div>
<div class="col-md-6">
<p class="mb-1 text-muted small">Closed At</p>
<h6><?= $s['closed_at'] ?? 'Still Open' ?></h6>
</div>
</div>
<hr>
<div class="d-flex justify-content-between mb-2">
<span>Opening Balance:</span>
<span>OMR <?= number_format((float)$s['opening_balance'], 3) ?></span>
</div>
<?php
// Breakdown of sales by payment method
$breakdown = db()->prepare("SELECT p.payment_method, SUM(p.amount) as total FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed' GROUP BY p.payment_method");
$breakdown->execute([$s['id']]);
$methods = $breakdown->fetchAll();
$total_sales = 0;
foreach ($methods as $m): $total_sales += $m['total']; ?>
<div class="d-flex justify-content-between mb-2">
<span class="text-uppercase">Sales (<?= htmlspecialchars($m['payment_method']) ?>):</span>
<span>OMR <?= number_format((float)$m['total'], 3) ?></span>
</div>
<?php endforeach; ?>
<div class="d-flex justify-content-between mb-2 fw-bold border-top pt-2">
<span>Total Sales:</span>
<span>OMR <?= number_format($total_sales, 3) ?></span>
</div>
<hr>
<div class="d-flex justify-content-between mb-2 h5 text-primary">
<span>Expected Cash in Hand:</span>
<span>OMR <?= number_format((float)($s['closing_balance'] ?? 0), 3) ?></span>
</div>
<div class="d-flex justify-content-between mb-2 h5 text-success">
<span>Actual Cash in Hand:</span>
<span>OMR <?= number_format((float)($s['cash_in_hand'] ?? 0), 3) ?></span>
</div>
<div class="d-flex justify-content-between mb-2 h5 <?= ($s['cash_in_hand'] - $s['closing_balance']) >= 0 ? 'text-info' : 'text-danger' ?>">
<span>Difference:</span>
<span>OMR <?= number_format((float)(($s['cash_in_hand'] ?? 0) - ($s['closing_balance'] ?? 0)), 3) ?></span>
</div>
<hr>
<h6 class="fw-bold mb-3">Detailed Transactions by Method</h6>
<?php
foreach ($methods as $m):
$details = db()->prepare("SELECT t.transaction_no, t.created_at, p.amount FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed' AND p.payment_method = ?");
$details->execute([$s['id'], $m['payment_method']]);
$trans = $details->fetchAll();
?>
<div class="mb-4">
<div class="bg-light p-2 mb-2 d-flex justify-content-between align-items-center rounded border-start border-4 <?= strtolower($m['payment_method']) === 'cash' ? 'border-success' : 'border-primary' ?>">
<span class="text-uppercase fw-bold small"><?= htmlspecialchars($m['payment_method']) ?></span>
<span class="badge bg-white text-dark border rounded-pill">OMR <?= number_format((float)$m['total'], 3) ?></span>
</div>
<table class="table table-sm table-hover small mb-0">
<thead>
<tr class="text-muted border-bottom">
<th>TX #</th>
<th>Time</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<?php foreach ($trans as $tr): ?>
<tr>
<td><?= htmlspecialchars($tr['transaction_no']) ?></td>
<td><?= date('H:i', strtotime($tr['created_at'])) ?></td>
<td class="text-end">OMR <?= number_format((float)$tr['amount'], 3) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endforeach; ?>
<?php if ($s['notes']): ?>
<hr>
<p class="mb-1 text-muted small">Notes:</p>
<p class="mb-0 small bg-light p-2 rounded"><?= nl2br(htmlspecialchars($s['notes'])) ?></p>
<?php endif; ?>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="window.print()">Print Report</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Open Register Modal -->
<div class="modal fade" id="openRegisterModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title">Open Register Session</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Select Register / Counter</label>
<select name="register_id" class="form-select" required>
<?php foreach ($data['cash_registers'] as $reg): ?>
<option value="<?= $reg['id'] ?>"><?= htmlspecialchars($reg['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Opening Cash Balance (OMR)</label>
<input type="number" step="0.001" name="opening_balance" class="form-control" value="0.000" required>
<div class="form-text">Enter the amount of cash already in the drawer.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="open_register" class="btn btn-success">Open Register</button>
</div>
</form>
</div>
</div>
</div>
<!-- Close Register Modal -->
<?php if ($session): ?>
<div class="modal fade" id="closeRegisterModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title">Close Register Session (#<?= $session['id'] ?>)</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="session_id" value="<?= $session['id'] ?>">
<div class="alert alert-warning py-2 small border-0 shadow-sm">
<i class="bi bi-exclamation-triangle me-2"></i> Before closing, please count all cash in your register.
</div>
<?php
$curBreakdown = db()->prepare("SELECT p.payment_method, SUM(p.amount) as total FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed' GROUP BY p.payment_method");
$curBreakdown->execute([$session['id']]);
$curMethods = $curBreakdown->fetchAll();
$curCashSales = 0;
foreach($curMethods as $cm) if(strtolower($cm['payment_method']) === 'cash') $curCashSales = (float)$cm['total'];
?>
<div class="card bg-light border-0 mb-3 small">
<div class="card-body">
<h6 class="card-title fw-bold small text-uppercase mb-2">Session Summary</h6>
<div class="d-flex justify-content-between mb-1">
<span class="text-muted">Opening Cash:</span>
<span class="fw-bold">OMR <?= number_format((float)$session['opening_balance'], 3) ?></span>
</div>
<div class="d-flex justify-content-between mb-1">
<span class="text-muted">Cash Sales:</span>
<span class="fw-bold">OMR <?= number_format($curCashSales, 3) ?></span>
</div>
<hr class="my-2">
<div class="d-flex justify-content-between fw-bold text-primary mb-2">
<span>Expected Cash in Hand:</span>
<span>OMR <?= number_format((float)$session['opening_balance'] + $curCashSales, 3) ?></span>
</div>
<?php $hasOther = false; foreach($curMethods as $cm): if(strtolower($cm['payment_method']) !== 'cash'): $hasOther = true; ?>
<div class="d-flex justify-content-between mt-1 text-muted border-top pt-1">
<span><?= htmlspecialchars($cm['payment_method']) ?> Total:</span>
<span>OMR <?= number_format((float)$cm['total'], 3) ?></span>
</div>
<?php endif; endforeach; ?>
</div>
</div>
<div class="mb-3">
<label class="form-label">Total Cash in Hand (Actual Counted)</label>
<input type="number" step="0.001" name="cash_in_hand" class="form-control" required placeholder="0.000">
</div>
<div class="mb-3">
<label class="form-label">Closing Notes / Comments</label>
<textarea name="notes" class="form-control" rows="3" placeholder="Any discrepancies or remarks..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
<button type="submit" name="close_register" class="btn btn-danger">Close Register & Submit</button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<?php elseif ($page === 'logs'): ?> <?php elseif ($page === 'logs'): ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden"> <div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-0"> <div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-0">

View File

@ -90,41 +90,48 @@ class LicenseService {
* Remote API Caller * Remote API Caller
*/ */
private static function callRemoteApi($endpoint, $params) { private static function callRemoteApi($endpoint, $params) {
// In a real production environment, this would hit your licensing server.
// For this demonstration, we simulate the request logic.
$url = self::$remote_api_url . $endpoint; $url = self::$remote_api_url . $endpoint;
/* // Check if we are in local development / simulation mode
// Real implementation would look like this: // If the URL is still the default placeholder, we use simulation
if (strpos(self::$remote_api_url, 'your-domain.com') !== false) {
return self::simulateApi($endpoint, $params);
}
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$resp = curl_exec($ch); $resp = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
$data = json_decode($resp, true);
return $data;
*/
// SIMULATION: If the key starts with 'FLAT-' (case-insensitive), we treat it as valid. if ($http_code !== 200) {
$clean_key = strtoupper(trim($params['license_key'] ?? '')); return ['success' => false, 'error' => "Remote server returned error code $http_code."];
if (strpos($clean_key, 'FLAT-') === 0) {
// Check if it's a verification request
if ($endpoint === '/verify') {
return ['success' => true];
}
return [
'success' => true,
'activation_token' => hash('sha256', $params['license_key'] . $params['fingerprint'] . 'SECRET_SALT')
];
} }
return [ $data = json_decode($resp, true);
'success' => false, if (!$data) {
'error' => 'License key invalid or expired. Please contact support.' return ['success' => false, 'error' => "Invalid response from remote server."];
]; }
return $data;
}
/**
* Local Simulation for development purposes
*/
private static function simulateApi($endpoint, $params) {
$clean_key = strtoupper(trim($params['license_key'] ?? ''));
if (strpos($clean_key, 'FLAT-') === 0) {
if ($endpoint === '/verify') return ['success' => true];
return [
'success' => true,
'activation_token' => hash('sha256', $params['license_key'] . $params['fingerprint'] . 'DEBUG_SALT')
];
}
return ['success' => false, 'error' => 'License key invalid or expired (Simulation Mode).'];
} }
} }

View File

@ -0,0 +1,21 @@
<?php
// Configuration for the SEPARATE Licensing Server
// This file should be configured on the server where you host the License Manager.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'license_manager_db');
define('DB_USER', 'root');
define('DB_PASS', '');
define('SERVER_SECRET', 'CHANGE_THIS_TO_A_RANDOM_STRING_FOR_SECURITY');
function db_manager() {
static $pdo;
if (!$pdo) {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4";
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
return $pdo;
}

View File

@ -0,0 +1,26 @@
-- SQL for the License Manager Database
-- Create a new database called 'license_manager_db' and run this script.
CREATE TABLE IF NOT EXISTS licenses (
id INT AUTO_INCREMENT PRIMARY KEY,
license_key VARCHAR(255) UNIQUE NOT NULL,
max_activations INT DEFAULT 1,
status ENUM('active', 'suspended', 'expired') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS activations (
id INT AUTO_INCREMENT PRIMARY KEY,
license_id INT NOT NULL,
fingerprint VARCHAR(255) NOT NULL,
domain VARCHAR(255),
product VARCHAR(255),
activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (license_id) REFERENCES licenses(id) ON DELETE CASCADE,
UNIQUE KEY (license_id, fingerprint)
) ENGINE=InnoDB;
-- Seed some test data
INSERT INTO licenses (license_key, max_activations) VALUES ('FLAT-8822-1192-3301', 1);
INSERT INTO licenses (license_key, max_activations) VALUES ('FLAT-TEST-KEY-0001', 5);
INSERT INTO licenses (license_key, max_activations) VALUES ('FLAT-DEV-UNLIMITED', 999);

115
license_manager/index.php Normal file
View File

@ -0,0 +1,115 @@
<?php
/**
* LICENSE MANAGER SERVER (Standalone Module)
*
* This is the central authority that manages license keys and activations.
* It should be hosted on a secure, separate server.
*/
header('Content-Type: application/json');
require_once __DIR__ . '/config.php';
// Simple Router
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
$endpoint = '';
if (strpos($request_uri, '/activate') !== false) $endpoint = 'activate';
if (strpos($request_uri, '/verify') !== false) $endpoint = 'verify';
if (strpos($request_uri, '/deactivate') !== false) $endpoint = 'deactivate';
// If running as a simple script without proper URL rewriting
if (empty($endpoint)) {
$endpoint = $_GET['action'] ?? '';
}
$input = json_decode(file_get_contents('php://input'), true);
try {
$pdo = db_manager();
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => 'Database connection failed.']);
exit;
}
if ($endpoint === 'activate') {
$key = strtoupper(trim($input['license_key'] ?? ''));
$fingerprint = $input['fingerprint'] ?? '';
$domain = $input['domain'] ?? '';
$product = $input['product'] ?? '';
if (empty($key) || empty($fingerprint)) {
echo json_encode(['success' => false, 'error' => 'Missing required parameters.']);
exit;
}
// 1. Find License
$stmt = $pdo->prepare("SELECT * FROM licenses WHERE license_key = ? LIMIT 1");
$stmt->execute([$key]);
$license = $stmt->fetch();
if (!$license) {
echo json_encode(['success' => false, 'error' => 'Invalid license key.']);
exit;
}
if ($license['status'] !== 'active') {
echo json_encode(['success' => false, 'error' => 'License is ' . $license['status'] . '.']);
exit;
}
// 2. Check current activations
$stmt = $pdo->prepare("SELECT COUNT(*) FROM activations WHERE license_id = ?");
$stmt->execute([$license['id']]);
$current_activations = $stmt->fetchColumn();
// 3. Check if this machine is already activated
$stmt = $pdo->prepare("SELECT * FROM activations WHERE license_id = ? AND fingerprint = ?");
$stmt->execute([$license['id'], $fingerprint]);
$existing = $stmt->fetch();
if (!$existing) {
if ($current_activations >= $license['max_activations']) {
echo json_encode(['success' => false, 'error' => 'Maximum activation limit reached.']);
exit;
}
// Record new activation
$stmt = $pdo->prepare("INSERT INTO activations (license_id, fingerprint, domain, product) VALUES (?, ?, ?, ?)");
$stmt->execute([$license['id'], $fingerprint, $domain, $product]);
}
// Success: Return signed token
$token = hash_hmac('sha256', $key . $fingerprint, SERVER_SECRET);
echo json_encode([
'success' => true,
'activation_token' => $token
]);
exit;
}
if ($endpoint === 'verify') {
$key = strtoupper(trim($input['license_key'] ?? ''));
$fingerprint = $input['fingerprint'] ?? '';
$token = $input['token'] ?? '';
// Simple validation: re-calculate token and check DB status
$expected_token = hash_hmac('sha256', $key . $fingerprint, SERVER_SECRET);
if ($token !== $expected_token) {
echo json_encode(['success' => false, 'error' => 'Invalid activation token.']);
exit;
}
$stmt = $pdo->prepare("SELECT status FROM licenses WHERE license_key = ?");
$stmt->execute([$key]);
$status = $stmt->fetchColumn();
if ($status === 'active') {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'License is no longer active.']);
}
exit;
}
echo json_encode(['success' => false, 'error' => 'Invalid endpoint.']);

View File

@ -1,53 +0,0 @@
<?php
/**
* SAMPLE LICENSE MANAGER SERVER
* This would be hosted on your own server (e.g., https://licensing.your-domain.com/api/v1)
*/
header('Content-Type: application/json');
// 1. Mock Database of valid keys
$valid_keys = [
'FLAT-8822-1192-3301' => ['status' => 'active', 'max_activations' => 1],
'FLAT-TEST-KEY-0001' => ['status' => 'active', 'max_activations' => 3],
];
// 2. Mock Database of current activations (Key => Fingerprint)
$activations = [
// 'FLAT-8822-1192-3301' => 'hash_from_client'
];
$input = json_decode(file_get_contents('php://input'), true);
$endpoint = $_SERVER['PATH_INFO'] ?? '';
if ($endpoint === '/activate') {
$key = strtoupper(trim($input['license_key'] ?? ''));
$fingerprint = $input['fingerprint'] ?? '';
if (!isset($valid_keys[$key])) {
echo json_encode(['success' => false, 'error' => 'Invalid license key.']);
exit;
}
if ($valid_keys[$key]['status'] !== 'active') {
echo json_encode(['success' => false, 'error' => 'License is expired or suspended.']);
exit;
}
// Check if already activated on another machine
if (isset($activations[$key]) && $activations[$key] !== $fingerprint) {
echo json_encode(['success' => false, 'error' => 'License already in use on another server.']);
exit;
}
// Success: Return a signed token
echo json_encode([
'success' => true,
'activation_token' => hash_hmac('sha256', $key . $fingerprint, 'YOUR_SECRET_SERVER_SALT')
]);
}
if ($endpoint === '/verify') {
// Similar logic to check if the token is still valid or if the key was revoked
echo json_encode(['success' => true]);
}

View File

@ -9,3 +9,7 @@
2026-02-18 10:46:04 - POST: {"license_key":"Flat-8822-1192-3301","activate":""} 2026-02-18 10:46:04 - POST: {"license_key":"Flat-8822-1192-3301","activate":""}
2026-02-18 10:46:14 - POST: {"license_key":": FLAT-8822-1192-3301","activate":""} 2026-02-18 10:46:14 - POST: {"license_key":": FLAT-8822-1192-3301","activate":""}
2026-02-18 10:48:04 - POST: {"license_key":" FLAT-8822-1192-3301","activate":""} 2026-02-18 10:48:04 - POST: {"license_key":" FLAT-8822-1192-3301","activate":""}
2026-02-18 11:33:56 - POST: {"name":"Counter 1","add_cash_register":""}
2026-02-18 11:34:15 - POST: {"name":"Counter 2","add_cash_register":""}
2026-02-18 11:43:14 - POST: {"register_id":"1","opening_balance":"0.000","open_register":""}
2026-02-18 11:50:03 - POST: {"register_id":"1","opening_balance":"0.000","open_register":""}

View File

@ -1 +1 @@
user_id|i:1;username|s:5:"admin";user_role_name|s:13:"Administrator";user_permissions|s:3:"all";profile_pic|s:32:"uploads/profile_1_1771401598.png"; user_id|i:1;username|s:5:"admin";user_role_name|s:13:"Administrator";user_permissions|s:3:"all";profile_pic|s:32:"uploads/profile_1_1771401598.png";register_session_id|s:1:"1";