5907 lines
330 KiB
PHP
5907 lines
330 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
session_start();
|
|
require_once 'db/config.php';
|
|
|
|
// Handle POST Requests
|
|
$message = '';
|
|
|
|
function numberToWordsOMR($number) {
|
|
$number = number_format((float)$number, 3, '.', '');
|
|
list($rials, $baisas) = explode('.', $number);
|
|
|
|
$rialsWords = numberToWords((int)$rials);
|
|
$baisasWords = numberToWords((int)$baisas);
|
|
|
|
$result = $rialsWords . " Omani Rials";
|
|
if ((int)$baisas > 0) {
|
|
$result .= " and " . $baisasWords . " Baisas";
|
|
}
|
|
return $result . " Only";
|
|
}
|
|
|
|
function numberToWords($num) {
|
|
$num = (int)$num;
|
|
if ($num === 0) return "Zero";
|
|
$ones = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"];
|
|
$tens = ["", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
|
|
if ($num < 20) return $ones[$num];
|
|
if ($num < 100) return $tens[(int)($num / 10)] . ($num % 10 ? " " . $ones[$num % 10] : "");
|
|
if ($num < 1000) return $ones[(int)($num / 100)] . " Hundred" . ($num % 100 ? " and " . numberToWords($num % 100) : "");
|
|
if ($num < 1000000) return numberToWords((int)($num / 1000)) . " Thousand" . ($num % 1000 ? " " . numberToWords($num % 1000) : "");
|
|
return (string)$num;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action'])) {
|
|
if ($_GET['action'] === 'search_items') {
|
|
header('Content-Type: application/json');
|
|
$q = $_GET['q'] ?? '';
|
|
$stmt = db()->prepare("SELECT id, name_en, name_ar, sku, sale_price, purchase_price, stock_quantity, vat_rate FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 10");
|
|
$stmt->execute(["%$q%", "%$q%", "%$q%"]);
|
|
echo json_encode($stmt->fetchAll());
|
|
exit;
|
|
}
|
|
if ($_GET['action'] === 'get_payments') {
|
|
header('Content-Type: application/json');
|
|
$invoice_id = (int)$_GET['invoice_id'];
|
|
$stmt = db()->prepare("SELECT p.*, i.id as inv_id, i.type as inv_type, c.name as customer_name
|
|
FROM payments p
|
|
JOIN invoices i ON p.invoice_id = i.id
|
|
LEFT JOIN customers c ON i.customer_id = c.id
|
|
WHERE p.invoice_id = ? ORDER BY p.payment_date DESC, p.id DESC");
|
|
$stmt->execute([$invoice_id]);
|
|
$payments = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($payments as &$p) {
|
|
$p['amount_words'] = numberToWordsOMR($p['amount']);
|
|
}
|
|
echo json_encode($payments);
|
|
exit;
|
|
}
|
|
if ($_GET['action'] === 'get_payment_details') {
|
|
header('Content-Type: application/json');
|
|
$payment_id = (int)$_GET['payment_id'];
|
|
$stmt = db()->prepare("SELECT p.*, i.id as inv_id, i.type as inv_type, c.name as customer_name
|
|
FROM payments p
|
|
JOIN invoices i ON p.invoice_id = i.id
|
|
LEFT JOIN customers c ON i.customer_id = c.id
|
|
WHERE p.id = ?");
|
|
$stmt->execute([$payment_id]);
|
|
$payment = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($payment) {
|
|
$payment['amount_words'] = numberToWordsOMR($payment['amount']);
|
|
}
|
|
echo json_encode($payment);
|
|
exit;
|
|
}
|
|
|
|
if ($_GET['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.id DESC");
|
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
|
exit;
|
|
}
|
|
|
|
if ($_GET['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 code']);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
if ($_GET['action'] === 'get_customer_loyalty') {
|
|
header('Content-Type: application/json');
|
|
$id = (int)($_GET['customer_id'] ?? 0);
|
|
$stmt = db()->prepare("SELECT loyalty_points FROM customers WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$points = $stmt->fetchColumn();
|
|
echo json_encode(['success' => true, 'points' => (float)$points]);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
// --- Expense Categories Handlers ---
|
|
if (isset($_POST['add_expense_category'])) {
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
$name_ar = $_POST['name_ar'] ?? '';
|
|
if ($name_en && $name_ar) {
|
|
$stmt = db()->prepare("INSERT INTO expense_categories (name_en, name_ar) VALUES (?, ?)");
|
|
$stmt->execute([$name_en, $name_ar]);
|
|
$message = "Expense Category added successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['edit_expense_category'])) {
|
|
$id = (int)$_POST['id'];
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
$name_ar = $_POST['name_ar'] ?? '';
|
|
if ($id && $name_en && $name_ar) {
|
|
$stmt = db()->prepare("UPDATE expense_categories SET name_en = ?, name_ar = ? WHERE id = ?");
|
|
$stmt->execute([$name_en, $name_ar, $id]);
|
|
$message = "Expense Category updated successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['delete_expense_category'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$stmt = db()->prepare("DELETE FROM expense_categories WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Expense Category deleted successfully!";
|
|
}
|
|
}
|
|
|
|
// --- Expenses Handlers ---
|
|
if (isset($_POST['add_expense'])) {
|
|
$category_id = (int)$_POST['category_id'];
|
|
$amount = (float)$_POST['amount'];
|
|
$date = $_POST['expense_date'] ?: date('Y-m-d');
|
|
$desc = $_POST['description'] ?? '';
|
|
$ref = $_POST['reference_no'] ?? '';
|
|
|
|
if ($category_id && $amount > 0) {
|
|
$stmt = db()->prepare("INSERT INTO expenses (category_id, amount, expense_date, description, reference_no) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$category_id, $amount, $date, $desc, $ref]);
|
|
$message = "Expense recorded successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['edit_expense'])) {
|
|
$id = (int)$_POST['id'];
|
|
$category_id = (int)$_POST['category_id'];
|
|
$amount = (float)$_POST['amount'];
|
|
$date = $_POST['expense_date'] ?: date('Y-m-d');
|
|
$desc = $_POST['description'] ?? '';
|
|
$ref = $_POST['reference_no'] ?? '';
|
|
|
|
if ($id && $category_id && $amount > 0) {
|
|
$stmt = db()->prepare("UPDATE expenses SET category_id = ?, amount = ?, expense_date = ?, description = ?, reference_no = ? WHERE id = ?");
|
|
$stmt->execute([$category_id, $amount, $date, $desc, $ref, $id]);
|
|
$message = "Expense updated successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['delete_expense'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$stmt = db()->prepare("DELETE FROM expenses WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Expense deleted successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_customer'])) {
|
|
$name = $_POST['name'] ?? '';
|
|
$email = $_POST['email'] ?? '';
|
|
$phone = $_POST['phone'] ?? '';
|
|
$tax_id = $_POST['tax_id'] ?? '';
|
|
$balance = (float)($_POST['balance'] ?? 0);
|
|
$type = $_POST['type'] ?? 'customer';
|
|
if ($name) {
|
|
$stmt = db()->prepare("INSERT INTO customers (name, email, phone, tax_id, balance, type) VALUES (?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$name, $email, $phone, $tax_id, $balance, $type]);
|
|
if (isset($_POST['ajax'])) {
|
|
echo json_encode(['success' => true, 'id' => db()->lastInsertId(), 'name' => $name]);
|
|
exit;
|
|
}
|
|
$message = ucfirst($type) . " added successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['action']) && $_POST['action'] === 'save_pos_transaction') {
|
|
header('Content-Type: application/json');
|
|
try {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
|
|
$customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
|
|
$payments = json_decode($_POST['payments'] ?? '[]', true);
|
|
$total_amount = (float)$_POST['total_amount'];
|
|
$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);
|
|
$items = json_decode($_POST['items'] ?? '[]', true);
|
|
|
|
if (empty($items)) {
|
|
throw new Exception("Cart is empty");
|
|
}
|
|
|
|
$net_amount = (float)($total_amount - $discount_amount - $loyalty_redeemed);
|
|
if ($net_amount < 0) $net_amount = 0;
|
|
|
|
// Loyalty Calculation: 1 point per 1 OMR spent on net amount
|
|
$loyalty_earned = floor($net_amount);
|
|
|
|
// Check if credit is used for walk-in or exceeds limit
|
|
$credit_total = 0;
|
|
foreach ($payments as $p) {
|
|
if ($p['method'] === 'credit') {
|
|
if (!$customer_id) {
|
|
throw new Exception("Credit payment is only allowed for registered customers");
|
|
}
|
|
$credit_total += (float)$p['amount'];
|
|
}
|
|
}
|
|
|
|
if ($customer_id && $credit_total > 0) {
|
|
$stmt = $db->prepare("SELECT balance, credit_limit FROM customers WHERE id = ?");
|
|
$stmt->execute([$customer_id]);
|
|
$cust = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($cust['credit_limit'] > 0 && (abs($cust['balance'] - $credit_total) > $cust['credit_limit'])) {
|
|
throw new Exception("Credit limit exceeded. Current Debt: " . number_format(abs($cust['balance']), 3) . ", Limit: " . number_format($cust['credit_limit'], 3));
|
|
}
|
|
}
|
|
|
|
// Calculate actual paid amount (excluding credit)
|
|
$actual_paid = 0;
|
|
foreach ($payments as $p) {
|
|
if ($p['method'] !== 'credit') {
|
|
$actual_paid += (float)$p['amount'];
|
|
}
|
|
}
|
|
|
|
$status = 'paid';
|
|
if ($actual_paid <= 0) {
|
|
$status = 'unpaid';
|
|
} elseif ($actual_paid < $net_amount - 0.001) {
|
|
$status = 'partially_paid';
|
|
}
|
|
|
|
$methods_str = implode(', ', array_unique(array_map(fn($p) => $p['method'], $payments)));
|
|
|
|
// Create Invoice
|
|
$stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, total_with_vat, paid_amount, type, payment_type) VALUES (?, CURDATE(), ?, ?, ?, 'sale', ?)");
|
|
$stmt->execute([$customer_id, $status, $net_amount, $actual_paid, $methods_str]);
|
|
$invoice_id = $db->lastInsertId();
|
|
|
|
// Add POS Transaction record
|
|
$stmt = $db->prepare("INSERT INTO pos_transactions (transaction_no, customer_id, total_amount, discount_code_id, discount_amount, loyalty_points_earned, loyalty_points_redeemed, net_amount, payment_method) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
$transaction_no = 'POS-' . time() . rand(100, 999);
|
|
$stmt->execute([$transaction_no, $customer_id, $total_amount, $discount_code_id, $discount_amount, $loyalty_earned, $loyalty_redeemed, $net_amount, $methods_str]);
|
|
$pos_id = $db->lastInsertId();
|
|
|
|
foreach ($items as $item) {
|
|
$qty = (float)$item['qty'];
|
|
$price = (float)$item['price'];
|
|
$subtotal = $qty * $price;
|
|
|
|
// Add to invoice_items
|
|
$stmt = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$invoice_id, $item['id'], $qty, $price, $subtotal]);
|
|
|
|
// Add to pos_items
|
|
$stmt = $db->prepare("INSERT INTO pos_items (transaction_id, product_id, quantity, unit_price, subtotal) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$pos_id, $item['id'], $qty, $price, $subtotal]);
|
|
|
|
// Update stock
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
|
$stmt->execute([$qty, $item['id']]);
|
|
}
|
|
|
|
// Update Customer Loyalty Points and Balance
|
|
if ($customer_id) {
|
|
$credit_total = 0;
|
|
foreach ($payments as $p) {
|
|
if ($p['method'] === 'credit') {
|
|
$credit_total += (float)$p['amount'];
|
|
}
|
|
}
|
|
|
|
$stmt = $db->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? + ?, balance = balance - ? WHERE id = ?");
|
|
$stmt->execute([(float)$loyalty_redeemed, (float)$loyalty_earned, (float)$credit_total, $customer_id]);
|
|
}
|
|
|
|
// Add Payments
|
|
foreach ($payments as $p) {
|
|
if ($p['method'] === 'credit') continue;
|
|
$stmt = $db->prepare("INSERT INTO payments (invoice_id, amount, payment_date, payment_method, notes) VALUES (?, ?, CURDATE(), ?, 'POS Transaction')");
|
|
$stmt->execute([$invoice_id, (float)$p['amount'], $p['method']]);
|
|
}
|
|
|
|
$db->commit();
|
|
echo json_encode(['success' => true, 'invoice_id' => $invoice_id, 'transaction_no' => $transaction_no]);
|
|
} catch (Exception $e) {
|
|
if (isset($db)) $db->rollBack();
|
|
error_log("POS Error: " . $e->getMessage());
|
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
// New Handlers for Advanced POS Features
|
|
if (isset($_POST['action']) && $_POST['action'] === 'hold_pos_cart') {
|
|
header('Content-Type: application/json');
|
|
$name = $_POST['cart_name'] ?? 'Cart ' . date('H:i');
|
|
$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 (isset($_POST['action']) && $_POST['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 (isset($_POST['edit_customer'])) {
|
|
|
|
$id = (int)$_POST['id'];
|
|
$name = $_POST['name'] ?? '';
|
|
$email = $_POST['email'] ?? '';
|
|
$phone = $_POST['phone'] ?? '';
|
|
$tax_id = $_POST['tax_id'] ?? '';
|
|
$balance = (float)($_POST['balance'] ?? 0);
|
|
if ($id && $name) {
|
|
$stmt = db()->prepare("UPDATE customers SET name = ?, email = ?, phone = ?, tax_id = ?, balance = ? WHERE id = ?");
|
|
$stmt->execute([$name, $email, $phone, $tax_id, $balance, $id]);
|
|
$message = "Record updated successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['delete_customer'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$stmt = db()->prepare("DELETE FROM customers WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Record deleted successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_category'])) {
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
$name_ar = $_POST['name_ar'] ?? '';
|
|
if ($name_en && $name_ar) {
|
|
$stmt = db()->prepare("INSERT INTO stock_categories (name_en, name_ar) VALUES (?, ?)");
|
|
$stmt->execute([$name_en, $name_ar]);
|
|
$message = "Category added successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_unit'])) {
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
$name_ar = $_POST['name_ar'] ?? '';
|
|
$short_en = $_POST['short_en'] ?? '';
|
|
$short_ar = $_POST['short_ar'] ?? '';
|
|
if ($name_en && $name_ar) {
|
|
$stmt = db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_name_en, short_name_ar) VALUES (?, ?, ?, ?)");
|
|
$stmt->execute([$name_en, $name_ar, $short_en, $short_ar]);
|
|
$message = "Unit added successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_item'])) {
|
|
$cat_id = $_POST['category_id'] ?: null;
|
|
$unit_id = $_POST['unit_id'] ?: null;
|
|
$supplier_id = $_POST['supplier_id'] ?: null;
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
$name_ar = $_POST['name_ar'] ?? '';
|
|
$sku = $_POST['sku'] ?? '';
|
|
$p_price = (float)($_POST['purchase_price'] ?? 0);
|
|
$s_price = (float)($_POST['sale_price'] ?? 0);
|
|
$qty = (float)($_POST['stock_quantity'] ?? 0);
|
|
$min_stock = (float)($_POST['min_stock_level'] ?? 0);
|
|
$vat_rate = (float)($_POST['vat_rate'] ?? 0);
|
|
$expiry = $_POST['expiry_date'] ?: null;
|
|
|
|
$image_path = null;
|
|
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
|
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
|
|
$filename = uniqid('item_') . '.' . $ext;
|
|
$target = 'uploads/items/' . $filename;
|
|
if (move_uploaded_file($_FILES['image']['tmp_name'], $target)) {
|
|
$image_path = $target;
|
|
}
|
|
}
|
|
|
|
if ($name_en && $name_ar) {
|
|
$stmt = db()->prepare("INSERT INTO stock_items (category_id, unit_id, supplier_id, name_en, name_ar, sku, purchase_price, sale_price, stock_quantity, min_stock_level, expiry_date, image_path, vat_rate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$cat_id, $unit_id, $supplier_id, $name_en, $name_ar, $sku, $p_price, $s_price, $qty, $min_stock, $expiry, $image_path, $vat_rate]);
|
|
$message = "Item added successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['delete_item'])) {
|
|
$id = (int)$_POST['id'];
|
|
// Optional: delete image file
|
|
$item = db()->prepare("SELECT image_path FROM stock_items WHERE id = ?");
|
|
$item->execute([$id]);
|
|
$path = $item->fetchColumn();
|
|
if ($path && file_exists($path)) {
|
|
unlink($path);
|
|
}
|
|
|
|
$stmt = db()->prepare("DELETE FROM stock_items WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Item deleted successfully!";
|
|
}
|
|
|
|
if (isset($_POST['edit_item'])) {
|
|
$id = (int)$_POST['id'];
|
|
$cat_id = $_POST['category_id'] ?: null;
|
|
$unit_id = $_POST['unit_id'] ?: null;
|
|
$supplier_id = $_POST['supplier_id'] ?: null;
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
$name_ar = $_POST['name_ar'] ?? '';
|
|
$sku = $_POST['sku'] ?? '';
|
|
$p_price = (float)($_POST['purchase_price'] ?? 0);
|
|
$s_price = (float)($_POST['sale_price'] ?? 0);
|
|
$qty = (float)($_POST['stock_quantity'] ?? 0);
|
|
$min_stock = (float)($_POST['min_stock_level'] ?? 0);
|
|
$vat_rate = (float)($_POST['vat_rate'] ?? 0);
|
|
$expiry = $_POST['expiry_date'] ?: null;
|
|
|
|
$stmt = db()->prepare("SELECT image_path FROM stock_items WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$image_path = $stmt->fetchColumn();
|
|
|
|
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
|
// Delete old image
|
|
if ($image_path && file_exists($image_path)) {
|
|
unlink($image_path);
|
|
}
|
|
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
|
|
$filename = uniqid('item_') . '.' . $ext;
|
|
$target = 'uploads/items/' . $filename;
|
|
if (move_uploaded_file($_FILES['image']['tmp_name'], $target)) {
|
|
$image_path = $target;
|
|
}
|
|
}
|
|
|
|
if ($name_en && $name_ar) {
|
|
$stmt = db()->prepare("UPDATE stock_items SET category_id = ?, unit_id = ?, supplier_id = ?, name_en = ?, name_ar = ?, sku = ?, purchase_price = ?, sale_price = ?, stock_quantity = ?, min_stock_level = ?, expiry_date = ?, image_path = ?, vat_rate = ? WHERE id = ?");
|
|
$stmt->execute([$cat_id, $unit_id, $supplier_id, $name_en, $name_ar, $sku, $p_price, $s_price, $qty, $min_stock, $expiry, $image_path, $vat_rate, $id]);
|
|
$message = "Item updated successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['import_items'])) {
|
|
if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === UPLOAD_ERR_OK) {
|
|
$file = $_FILES['excel_file']['tmp_name'];
|
|
$handle = fopen($file, 'r');
|
|
$header = fgetcsv($handle); // Skip header row
|
|
|
|
$count = 0;
|
|
while (($row = fgetcsv($handle)) !== FALSE) {
|
|
// Mapping: sku, eng name, arabic name, sale price, cost price
|
|
if (count($row) < 5) continue;
|
|
|
|
$sku = trim($row[0]);
|
|
$name_en = trim($row[1]);
|
|
$name_ar = trim($row[2]);
|
|
$sale_price = (float)trim($row[3]);
|
|
$purchase_price = (float)trim($row[4]);
|
|
|
|
if ($name_en && $name_ar) {
|
|
// Check if SKU exists to update or insert
|
|
$existingId = null;
|
|
if ($sku !== "") {
|
|
$check = db()->prepare("SELECT id FROM stock_items WHERE sku = ?");
|
|
$check->execute([$sku]);
|
|
$existingId = $check->fetchColumn();
|
|
}
|
|
|
|
if ($existingId) {
|
|
$stmt = db()->prepare("UPDATE stock_items SET name_en = ?, name_ar = ?, sale_price = ?, purchase_price = ? WHERE id = ?");
|
|
$stmt->execute([$name_en, $name_ar, $sale_price, $purchase_price, $existingId]);
|
|
} else {
|
|
$stmt = db()->prepare("INSERT INTO stock_items (sku, name_en, name_ar, sale_price, purchase_price) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$sku, $name_en, $name_ar, $sale_price, $purchase_price]);
|
|
}
|
|
$count++;
|
|
}
|
|
}
|
|
fclose($handle);
|
|
$message = "$count items processed successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['import_customers']) || isset($_POST['import_suppliers'])) {
|
|
$type = isset($_POST['import_customers']) ? 'customer' : 'supplier';
|
|
if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === UPLOAD_ERR_OK) {
|
|
$file = $_FILES['excel_file']['tmp_name'];
|
|
$handle = fopen($file, 'r');
|
|
$header = fgetcsv($handle);
|
|
$count = 0;
|
|
while (($row = fgetcsv($handle)) !== FALSE) {
|
|
if (count($row) < 4) continue;
|
|
$name = trim($row[0]);
|
|
$email = trim($row[1]);
|
|
$phone = trim($row[2]);
|
|
$tax_id = isset($row[3]) ? trim($row[3]) : '';
|
|
$balance = isset($row[4]) ? (float)trim($row[4]) : (float)trim($row[3] ?? 0);
|
|
if ($name) {
|
|
$stmt = db()->prepare("INSERT INTO customers (name, email, phone, tax_id, balance, type) VALUES (?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$name, $email, $phone, $tax_id, $balance, $type]);
|
|
$count++;
|
|
}
|
|
}
|
|
fclose($handle);
|
|
$message = "$count " . ($type === 'customer' ? 'customers' : 'suppliers') . " imported successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['import_categories'])) {
|
|
if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === UPLOAD_ERR_OK) {
|
|
$file = $_FILES['excel_file']['tmp_name'];
|
|
$handle = fopen($file, 'r');
|
|
$header = fgetcsv($handle);
|
|
$count = 0;
|
|
while (($row = fgetcsv($handle)) !== FALSE) {
|
|
if (count($row) < 2) continue;
|
|
$name_en = trim($row[0]);
|
|
$name_ar = trim($row[1]);
|
|
if ($name_en && $name_ar) {
|
|
$stmt = db()->prepare("INSERT INTO stock_categories (name_en, name_ar) VALUES (?, ?)");
|
|
$stmt->execute([$name_en, $name_ar]);
|
|
$count++;
|
|
}
|
|
}
|
|
fclose($handle);
|
|
$message = "$count categories imported successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['import_units'])) {
|
|
if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === UPLOAD_ERR_OK) {
|
|
$file = $_FILES['excel_file']['tmp_name'];
|
|
$handle = fopen($file, 'r');
|
|
$header = fgetcsv($handle);
|
|
$count = 0;
|
|
while (($row = fgetcsv($handle)) !== FALSE) {
|
|
if (count($row) < 4) continue;
|
|
$name_en = trim($row[0]);
|
|
$name_ar = trim($row[1]);
|
|
$short_en = trim($row[2]);
|
|
$short_ar = trim($row[3]);
|
|
if ($name_en && $name_ar) {
|
|
$stmt = db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_name_en, short_name_ar) VALUES (?, ?, ?, ?)");
|
|
$stmt->execute([$name_en, $name_ar, $short_en, $short_ar]);
|
|
$count++;
|
|
}
|
|
}
|
|
fclose($handle);
|
|
$message = "$count units imported successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_invoice'])) {
|
|
$customer_id = $_POST['customer_id'] ?: null;
|
|
$invoice_date = $_POST['invoice_date'] ?: date('Y-m-d');
|
|
$type = $_POST['type'] ?? 'sale'; // 'sale' or 'purchase'
|
|
$payment_type = $_POST['payment_type'] ?? 'cash';
|
|
$status = $_POST['status'] ?? 'unpaid';
|
|
$item_ids = $_POST['item_ids'] ?? [];
|
|
$quantities = $_POST['quantities'] ?? [];
|
|
$prices = $_POST['prices'] ?? [];
|
|
|
|
if (!empty($item_ids)) {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
try {
|
|
$subtotal = 0;
|
|
$total_vat = 0;
|
|
|
|
$items_data = [];
|
|
foreach ($item_ids as $index => $item_id) {
|
|
$qty = (float)$quantities[$index];
|
|
$price = (float)$prices[$index];
|
|
|
|
// Fetch vat_rate for this item
|
|
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
|
$stmtVat->execute([$item_id]);
|
|
$vat_rate = (float)$stmtVat->fetchColumn();
|
|
|
|
$line_total = $qty * $price;
|
|
$line_vat = $line_total * ($vat_rate / 100);
|
|
|
|
$subtotal += $line_total;
|
|
$total_vat += $line_vat;
|
|
|
|
$items_data[] = [
|
|
'id' => $item_id,
|
|
'qty' => $qty,
|
|
'price' => $price,
|
|
'total' => $line_total
|
|
];
|
|
}
|
|
|
|
$total_with_vat = $subtotal + $total_vat;
|
|
$paid_amount = ($status === 'paid') ? $total_with_vat : (($status === 'unpaid') ? 0 : (float)($_POST['paid_amount'] ?? 0));
|
|
|
|
$stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, type, payment_type, status, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$customer_id, $invoice_date, $type, $payment_type, $status, $subtotal, $total_vat, $total_with_vat, $paid_amount]);
|
|
$invoice_id = $db->lastInsertId();
|
|
|
|
if ($paid_amount > 0) {
|
|
$stmt = $db->prepare("INSERT INTO payments (invoice_id, payment_date, amount, payment_method, notes) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$invoice_id, $invoice_date, $paid_amount, 'Cash', 'Initial payment']);
|
|
}
|
|
|
|
foreach ($items_data as $item) {
|
|
$stmt = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$invoice_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
|
|
|
// Update stock level
|
|
if ($type === 'sale') {
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
|
} else {
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
|
|
}
|
|
$stmt->execute([$item['qty'], $item['id']]);
|
|
}
|
|
|
|
$db->commit();
|
|
$message = "Invoice #$invoice_id created successfully!";
|
|
} catch (Exception $e) {
|
|
$db->rollBack();
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['delete_invoice'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
try {
|
|
// Get invoice details
|
|
$stmt = $db->prepare("SELECT type FROM invoices WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$type = $stmt->fetchColumn();
|
|
|
|
// Get items to restore stock
|
|
$stmt = $db->prepare("SELECT item_id, quantity FROM invoice_items WHERE invoice_id = ?");
|
|
$stmt->execute([$id]);
|
|
$items = $stmt->fetchAll();
|
|
|
|
foreach ($items as $item) {
|
|
if ($type === 'sale') {
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
|
|
} else {
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
|
}
|
|
$stmt->execute([$item['quantity'], $item['item_id']]);
|
|
}
|
|
|
|
$stmt = $db->prepare("DELETE FROM invoice_items WHERE invoice_id = ?");
|
|
$stmt->execute([$id]);
|
|
$stmt = $db->prepare("DELETE FROM invoices WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
|
|
$db->commit();
|
|
$message = "Invoice deleted successfully and stock restored!";
|
|
} catch (Exception $e) {
|
|
$db->rollBack();
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['edit_invoice'])) {
|
|
$invoice_id = (int)$_POST['invoice_id'];
|
|
$customer_id = $_POST['customer_id'] ?: null;
|
|
$invoice_date = $_POST['invoice_date'] ?: date('Y-m-d');
|
|
$payment_type = $_POST['payment_type'] ?? 'cash';
|
|
$status = $_POST['status'] ?? 'unpaid';
|
|
$item_ids = $_POST['item_ids'] ?? [];
|
|
$quantities = $_POST['quantities'] ?? [];
|
|
$prices = $_POST['prices'] ?? [];
|
|
|
|
if ($invoice_id && !empty($item_ids)) {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
try {
|
|
// Get old invoice type and items to revert stock
|
|
$stmt = $db->prepare("SELECT type, paid_amount FROM invoices WHERE id = ?");
|
|
$stmt->execute([$invoice_id]);
|
|
$inv_info = $stmt->fetch();
|
|
$type = $inv_info['type'];
|
|
$old_paid_amount = (float)$inv_info['paid_amount'];
|
|
|
|
$stmt = $db->prepare("SELECT item_id, quantity FROM invoice_items WHERE invoice_id = ?");
|
|
$stmt->execute([$invoice_id]);
|
|
$old_items = $stmt->fetchAll();
|
|
|
|
foreach ($old_items as $item) {
|
|
if ($type === 'sale') {
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
|
|
} else {
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
|
}
|
|
$stmt->execute([$item['quantity'], $item['item_id']]);
|
|
}
|
|
|
|
// Delete old items
|
|
$stmt = $db->prepare("DELETE FROM invoice_items WHERE invoice_id = ?");
|
|
$stmt->execute([$invoice_id]);
|
|
|
|
// Calculate new totals
|
|
$subtotal = 0;
|
|
$total_vat = 0;
|
|
$items_data = [];
|
|
foreach ($item_ids as $index => $item_id) {
|
|
$qty = (float)$quantities[$index];
|
|
$price = (float)$prices[$index];
|
|
|
|
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
|
$stmtVat->execute([$item_id]);
|
|
$vat_rate = (float)$stmtVat->fetchColumn();
|
|
|
|
$line_total = $qty * $price;
|
|
$line_vat = $line_total * ($vat_rate / 100);
|
|
|
|
$subtotal += $line_total;
|
|
$total_vat += $line_vat;
|
|
|
|
$items_data[] = [
|
|
'id' => $item_id,
|
|
'qty' => $qty,
|
|
'price' => $price,
|
|
'total' => $line_total
|
|
];
|
|
}
|
|
|
|
$total_with_vat = $subtotal + $total_vat;
|
|
$paid_amount = ($status === 'paid') ? $total_with_vat : (($status === 'unpaid') ? 0 : ($old_paid_amount > 0 ? $old_paid_amount : ($_POST['paid_amount'] ?? 0)));
|
|
|
|
if ($paid_amount > $old_paid_amount) {
|
|
$diff = $paid_amount - $old_paid_amount;
|
|
$stmt = $db->prepare("INSERT INTO payments (invoice_id, payment_date, amount, payment_method, notes) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$invoice_id, $invoice_date, $diff, 'Cash', 'Payment via edit']);
|
|
}
|
|
|
|
// Update invoice
|
|
$stmt = $db->prepare("UPDATE invoices SET customer_id = ?, invoice_date = ?, payment_type = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?");
|
|
$stmt->execute([$customer_id, $invoice_date, $payment_type, $status, $subtotal, $total_vat, $total_with_vat, $paid_amount, $invoice_id]);
|
|
|
|
// Insert new items and update stock
|
|
foreach ($items_data as $item) {
|
|
$stmt = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$invoice_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
|
|
|
if ($type === 'sale') {
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
|
} else {
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
|
|
}
|
|
$stmt->execute([$item['qty'], $item['id']]);
|
|
}
|
|
|
|
$db->commit();
|
|
$message = "Invoice #$invoice_id updated successfully!";
|
|
} catch (Exception $e) {
|
|
$db->rollBack();
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_quotation'])) {
|
|
$customer_id = $_POST['customer_id'] ?: null;
|
|
$quotation_date = $_POST['quotation_date'] ?: date('Y-m-d');
|
|
$valid_until = $_POST['valid_until'] ?: null;
|
|
$item_ids = $_POST['item_ids'] ?? [];
|
|
$quantities = $_POST['quantities'] ?? [];
|
|
$prices = $_POST['prices'] ?? [];
|
|
|
|
if (!empty($item_ids)) {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
try {
|
|
$subtotal = 0;
|
|
$total_vat = 0;
|
|
$items_data = [];
|
|
foreach ($item_ids as $index => $item_id) {
|
|
$qty = (float)$quantities[$index];
|
|
$price = (float)$prices[$index];
|
|
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
|
$stmtVat->execute([$item_id]);
|
|
$vat_rate = (float)$stmtVat->fetchColumn();
|
|
$line_total = $qty * $price;
|
|
$line_vat = $line_total * ($vat_rate / 100);
|
|
$subtotal += $line_total;
|
|
$total_vat += $line_vat;
|
|
$items_data[] = ['id' => $item_id, 'qty' => $qty, 'price' => $price, 'total' => $line_total];
|
|
}
|
|
$total_with_vat = $subtotal + $total_vat;
|
|
$stmt = $db->prepare("INSERT INTO quotations (customer_id, quotation_date, valid_until, total_amount, vat_amount, total_with_vat) VALUES (?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$customer_id, $quotation_date, $valid_until, $subtotal, $total_vat, $total_with_vat]);
|
|
$quotation_id = $db->lastInsertId();
|
|
foreach ($items_data as $item) {
|
|
$stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$quotation_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
|
}
|
|
$db->commit();
|
|
$message = "Quotation #$quotation_id created successfully!";
|
|
} catch (Exception $e) {
|
|
$db->rollBack();
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['edit_quotation'])) {
|
|
$quotation_id = (int)$_POST['quotation_id'];
|
|
$customer_id = $_POST['customer_id'] ?: null;
|
|
$quotation_date = $_POST['quotation_date'] ?: date('Y-m-d');
|
|
$valid_until = $_POST['valid_until'] ?: null;
|
|
$status = $_POST['status'] ?? 'pending';
|
|
$item_ids = $_POST['item_ids'] ?? [];
|
|
$quantities = $_POST['quantities'] ?? [];
|
|
$prices = $_POST['prices'] ?? [];
|
|
|
|
if ($quotation_id && !empty($item_ids)) {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
try {
|
|
$stmt = $db->prepare("DELETE FROM quotation_items WHERE quotation_id = ?");
|
|
$stmt->execute([$quotation_id]);
|
|
$subtotal = 0;
|
|
$total_vat = 0;
|
|
$items_data = [];
|
|
foreach ($item_ids as $index => $item_id) {
|
|
$qty = (float)$quantities[$index];
|
|
$price = (float)$prices[$index];
|
|
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
|
$stmtVat->execute([$item_id]);
|
|
$vat_rate = (float)$stmtVat->fetchColumn();
|
|
$line_total = $qty * $price;
|
|
$line_vat = $line_total * ($vat_rate / 100);
|
|
$subtotal += $line_total;
|
|
$total_vat += $line_vat;
|
|
$items_data[] = ['id' => $item_id, 'qty' => $qty, 'price' => $price, 'total' => $line_total];
|
|
}
|
|
$total_with_vat = $subtotal + $total_vat;
|
|
$stmt = $db->prepare("UPDATE quotations SET customer_id = ?, quotation_date = ?, valid_until = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ? WHERE id = ?");
|
|
$stmt->execute([$customer_id, $quotation_date, $valid_until, $status, $subtotal, $total_vat, $total_with_vat, $quotation_id]);
|
|
foreach ($items_data as $item) {
|
|
$stmt = $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$quotation_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
|
}
|
|
$db->commit();
|
|
$message = "Quotation #$quotation_id updated successfully!";
|
|
} catch (Exception $e) {
|
|
$db->rollBack();
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['delete_quotation'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$stmt = db()->prepare("DELETE FROM quotations WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Quotation deleted successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['convert_to_invoice'])) {
|
|
$quotation_id = (int)$_POST['quotation_id'];
|
|
if ($quotation_id) {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
try {
|
|
$stmt = $db->prepare("SELECT * FROM quotations WHERE id = ?");
|
|
$stmt->execute([$quotation_id]);
|
|
$q = $stmt->fetch();
|
|
if (!$q) throw new Exception("Quotation not found");
|
|
if ($q['status'] === 'converted') throw new Exception("Quotation already converted");
|
|
|
|
$stmt = $db->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
|
|
$stmt->execute([$quotation_id]);
|
|
$items = $stmt->fetchAll();
|
|
|
|
// Create Invoice
|
|
$stmt = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, type, status, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, CURDATE(), 'sale', 'unpaid', ?, ?, ?, 0)");
|
|
$stmt->execute([$q['customer_id'], $q['total_amount'], $q['vat_amount'], $q['total_with_vat']]);
|
|
$invoice_id = $db->lastInsertId();
|
|
|
|
foreach ($items as $item) {
|
|
$stmt = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$invoice_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['total_price']]);
|
|
|
|
// Update stock
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
|
$stmt->execute([$item['quantity'], $item['item_id']]);
|
|
}
|
|
|
|
// Update quotation status
|
|
$stmt = $db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?");
|
|
$stmt->execute([$quotation_id]);
|
|
|
|
$db->commit();
|
|
$message = "Quotation converted to Invoice #$invoice_id successfully!";
|
|
} catch (Exception $e) {
|
|
$db->rollBack();
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_payment_method'])) {
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
$name_ar = $_POST['name_ar'] ?? '';
|
|
if ($name_en && $name_ar) {
|
|
$stmt = db()->prepare("INSERT INTO payment_methods (name_en, name_ar) VALUES (?, ?)");
|
|
$stmt->execute([$name_en, $name_ar]);
|
|
$message = "Payment method added successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['edit_payment_method'])) {
|
|
$id = (int)$_POST['id'];
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
$name_ar = $_POST['name_ar'] ?? '';
|
|
if ($id && $name_en && $name_ar) {
|
|
$stmt = db()->prepare("UPDATE payment_methods SET name_en = ?, name_ar = ? WHERE id = ?");
|
|
$stmt->execute([$name_en, $name_ar, $id]);
|
|
$message = "Payment method updated successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['delete_payment_method'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$stmt = db()->prepare("DELETE FROM payment_methods WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Payment method deleted successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['update_settings'])) {
|
|
foreach ($_POST['settings'] as $key => $value) {
|
|
$stmt = db()->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?");
|
|
$stmt->execute([$key, $value, $value]);
|
|
}
|
|
|
|
if (isset($_FILES['company_logo']) && $_FILES['company_logo']['error'] === UPLOAD_ERR_OK) {
|
|
$ext = pathinfo($_FILES['company_logo']['name'], PATHINFO_EXTENSION);
|
|
$filename = 'logo.' . $ext;
|
|
$target = 'uploads/' . $filename;
|
|
if (!is_dir('uploads')) mkdir('uploads', 0775, true);
|
|
if (move_uploaded_file($_FILES['company_logo']['tmp_name'], $target)) {
|
|
$stmt = db()->prepare("INSERT INTO settings (`key`, `value`) VALUES ('company_logo', ?) ON DUPLICATE KEY UPDATE `value` = ?");
|
|
$stmt->execute([$target, $target]);
|
|
}
|
|
}
|
|
|
|
if (isset($_FILES['favicon']) && $_FILES['favicon']['error'] === UPLOAD_ERR_OK) {
|
|
$ext = pathinfo($_FILES['favicon']['name'], PATHINFO_EXTENSION);
|
|
$filename = 'favicon.' . $ext;
|
|
$target = 'uploads/' . $filename;
|
|
if (!is_dir('uploads')) mkdir('uploads', 0775, true);
|
|
if (move_uploaded_file($_FILES['favicon']['tmp_name'], $target)) {
|
|
$stmt = db()->prepare("INSERT INTO settings (`key`, `value`) VALUES ('favicon', ?) ON DUPLICATE KEY UPDATE `value` = ?");
|
|
$stmt->execute([$target, $target]);
|
|
}
|
|
}
|
|
|
|
if (isset($_FILES['manager_signature']) && $_FILES['manager_signature']['error'] === UPLOAD_ERR_OK) {
|
|
$ext = pathinfo($_FILES['manager_signature']['name'], PATHINFO_EXTENSION);
|
|
$filename = 'signature.' . $ext;
|
|
$target = 'uploads/' . $filename;
|
|
if (!is_dir('uploads')) mkdir('uploads', 0775, true);
|
|
if (move_uploaded_file($_FILES['manager_signature']['tmp_name'], $target)) {
|
|
$stmt = db()->prepare("INSERT INTO settings (`key`, `value`) VALUES ('manager_signature', ?) ON DUPLICATE KEY UPDATE `value` = ?");
|
|
$stmt->execute([$target, $target]);
|
|
}
|
|
}
|
|
$message = "Settings updated successfully!";
|
|
}
|
|
|
|
if (isset($_POST['record_payment'])) {
|
|
$invoice_id = (int)$_POST['invoice_id'];
|
|
$amount = (float)$_POST['amount'];
|
|
$payment_date = $_POST['payment_date'] ?: date('Y-m-d');
|
|
$payment_method = $_POST['payment_method'] ?: 'Cash';
|
|
$notes = $_POST['notes'] ?? '';
|
|
|
|
if ($invoice_id && $amount > 0) {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
try {
|
|
// Record the payment
|
|
$stmt = $db->prepare("INSERT INTO payments (invoice_id, payment_date, amount, payment_method, notes) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$invoice_id, $payment_date, $amount, $payment_method, $notes]);
|
|
$payment_id = $db->lastInsertId();
|
|
|
|
// Update invoice paid_amount and status
|
|
$stmt = $db->prepare("SELECT total_with_vat, paid_amount FROM invoices WHERE id = ?");
|
|
$stmt->execute([$invoice_id]);
|
|
$inv = $stmt->fetch();
|
|
|
|
$new_paid_amount = (float)$inv['paid_amount'] + $amount;
|
|
$total = (float)$inv['total_with_vat'];
|
|
|
|
$new_status = 'partially_paid';
|
|
if ($new_paid_amount >= $total) {
|
|
$new_status = 'paid';
|
|
}
|
|
|
|
$stmt = $db->prepare("UPDATE invoices SET paid_amount = ?, status = ? WHERE id = ?");
|
|
$stmt->execute([$new_paid_amount, $new_status, $invoice_id]);
|
|
|
|
$db->commit();
|
|
$message = "Payment of OMR " . number_format($amount, 3) . " recorded successfully! Receipt ID: $payment_id";
|
|
|
|
// For showing receipt after redirect/refresh, we can use a session or just a message.
|
|
// The user wants to "issue a receipt". I'll add a trigger to show the receipt modal.
|
|
$_SESSION['show_receipt_id'] = $payment_id;
|
|
$_SESSION['trigger_receipt_modal'] = true;
|
|
} catch (Exception $e) {
|
|
$db->rollBack();
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Routing & Data Fetching
|
|
$page = $_GET['page'] ?? 'dashboard';
|
|
$data = [];
|
|
|
|
if ($page === 'export') {
|
|
$type = $_GET['type'] ?? 'sales';
|
|
$filename = $type . "_export_" . date('Y-m-d') . ".csv";
|
|
|
|
header('Content-Type: text/csv; charset=utf-8');
|
|
header('Content-Disposition: attachment; filename=' . $filename);
|
|
$output = fopen('php://output', 'w');
|
|
|
|
// Add UTF-8 BOM for Excel
|
|
fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
|
|
|
|
if ($type === 'sales' || $type === 'purchases') {
|
|
$invType = ($type === 'sales') ? 'sale' : 'purchase';
|
|
$where = ["v.type = ?"];
|
|
$params = [$invType];
|
|
if (!empty($_GET['search'])) { $where[] = "(v.id LIKE ? OR c.name LIKE ?)"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; }
|
|
if (!empty($_GET['customer_id'])) { $where[] = "v.customer_id = ?"; $params[] = $_GET['customer_id']; }
|
|
if (!empty($_GET['start_date'])) { $where[] = "v.invoice_date >= ?"; $params[] = $_GET['start_date']; }
|
|
if (!empty($_GET['end_date'])) { $where[] = "v.invoice_date <= ?"; $params[] = $_GET['end_date']; }
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT v.id, c.name as customer_name, v.invoice_date, v.payment_type, v.status, v.total_with_vat, v.paid_amount, (v.total_with_vat - v.paid_amount) as balance
|
|
FROM invoices v LEFT JOIN customers c ON v.customer_id = c.id
|
|
WHERE $whereSql ORDER BY v.id DESC");
|
|
$stmt->execute($params);
|
|
fputcsv($output, ['Invoice ID', 'Customer/Supplier', 'Date', 'Payment', 'Status', 'Total', 'Paid', 'Balance']);
|
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row);
|
|
} elseif ($type === 'customers' || $type === 'suppliers') {
|
|
$custType = ($type === 'suppliers') ? 'supplier' : 'customer';
|
|
$where = ["type = ?"];
|
|
$params = [$custType];
|
|
if (!empty($_GET['search'])) { $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ? OR tax_id LIKE ?)"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; }
|
|
if (!empty($_GET['start_date'])) { $where[] = "DATE(created_at) >= ?"; $params[] = $_GET['start_date']; }
|
|
if (!empty($_GET['end_date'])) { $where[] = "DATE(created_at) <= ?"; $params[] = $_GET['end_date']; }
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT id, name, email, phone, tax_id, balance, created_at FROM customers WHERE $whereSql ORDER BY id DESC");
|
|
$stmt->execute($params);
|
|
fputcsv($output, ['ID', 'Name', 'Email', 'Phone', 'Tax ID', 'Balance', 'Created At']);
|
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row);
|
|
} elseif ($type === 'items') {
|
|
$where = ["1=1"];
|
|
$params = [];
|
|
if (!empty($_GET['search'])) { $where[] = "(i.name_en LIKE ? OR i.name_ar LIKE ? OR i.sku LIKE ?)"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; }
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT i.sku, i.name_en, i.name_ar, c.name_en as category, i.purchase_price, i.sale_price, i.stock_quantity, i.vat_rate
|
|
FROM stock_items i LEFT JOIN stock_categories c ON i.category_id = c.id
|
|
WHERE $whereSql ORDER BY i.id DESC");
|
|
$stmt->execute($params);
|
|
fputcsv($output, ['SKU', 'Name (EN)', 'Name (AR)', 'Category', 'Purchase Price', 'Sale Price', 'Quantity', 'VAT %']);
|
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row);
|
|
}
|
|
fclose($output);
|
|
exit;
|
|
}
|
|
|
|
// Global data for modals
|
|
$data['categories'] = db()->query("SELECT * FROM stock_categories ORDER BY name_en ASC")->fetchAll();
|
|
$data['units'] = db()->query("SELECT * FROM stock_units ORDER BY name_en ASC")->fetchAll();
|
|
$data['suppliers'] = db()->query("SELECT * FROM customers WHERE type = 'supplier' ORDER BY name ASC")->fetchAll();
|
|
|
|
$settings_raw = db()->query("SELECT * FROM settings")->fetchAll();
|
|
$data['settings'] = [];
|
|
foreach ($settings_raw as $s) {
|
|
$data['settings'][$s['key']] = $s['value'];
|
|
}
|
|
|
|
switch ($page) {
|
|
case 'suppliers':
|
|
case 'customers':
|
|
$type = ($page === 'suppliers') ? 'supplier' : 'customer';
|
|
$where = ["type = ?"];
|
|
$params = [$type];
|
|
if (!empty($_GET['search'])) {
|
|
$where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ? OR tax_id LIKE ?)";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
}
|
|
if (!empty($_GET['start_date'])) {
|
|
$where[] = "DATE(created_at) >= ?";
|
|
$params[] = $_GET['start_date'];
|
|
}
|
|
if (!empty($_GET['end_date'])) {
|
|
$where[] = "DATE(created_at) <= ?";
|
|
$params[] = $_GET['end_date'];
|
|
}
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT * FROM customers WHERE $whereSql ORDER BY id DESC");
|
|
$stmt->execute($params);
|
|
$data['customers'] = $stmt->fetchAll();
|
|
break;
|
|
case 'categories':
|
|
// Already fetched globally
|
|
break;
|
|
case 'units':
|
|
// Already fetched globally
|
|
break;
|
|
case 'items':
|
|
$where = ["1=1"];
|
|
$params = [];
|
|
if (!empty($_GET['search'])) {
|
|
$where[] = "(i.name_en LIKE ? OR i.name_ar LIKE ? OR i.sku LIKE ?)";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
}
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar, u.short_name_en as unit_en, u.short_name_ar as unit_ar, s.name as supplier_name
|
|
FROM stock_items i
|
|
LEFT JOIN stock_categories c ON i.category_id = c.id
|
|
LEFT JOIN stock_units u ON i.unit_id = u.id
|
|
LEFT JOIN customers s ON i.supplier_id = s.id
|
|
WHERE $whereSql
|
|
ORDER BY i.id DESC");
|
|
$stmt->execute($params);
|
|
$data['items'] = $stmt->fetchAll();
|
|
break;
|
|
case 'quotations':
|
|
$where = ["1=1"];
|
|
$params = [];
|
|
if (!empty($_GET['search'])) {
|
|
$where[] = "(q.id LIKE ? OR c.name LIKE ?)";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
}
|
|
if (!empty($_GET['customer_id'])) {
|
|
$where[] = "q.customer_id = ?";
|
|
$params[] = $_GET['customer_id'];
|
|
}
|
|
if (!empty($_GET['start_date'])) {
|
|
$where[] = "q.quotation_date >= ?";
|
|
$params[] = $_GET['start_date'];
|
|
}
|
|
if (!empty($_GET['end_date'])) {
|
|
$where[] = "q.quotation_date <= ?";
|
|
$params[] = $_GET['end_date'];
|
|
}
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT q.*, c.name as customer_name
|
|
FROM quotations q
|
|
LEFT JOIN customers c ON q.customer_id = c.id
|
|
WHERE $whereSql
|
|
ORDER BY q.id DESC");
|
|
$stmt->execute($params);
|
|
$data['quotations'] = $stmt->fetchAll();
|
|
$data['items_list'] = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate FROM stock_items ORDER BY name_en ASC")->fetchAll();
|
|
$data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = 'customer' ORDER BY name ASC")->fetchAll();
|
|
break;
|
|
case 'payment_methods':
|
|
$data['payment_methods'] = db()->query("SELECT * FROM payment_methods ORDER BY id DESC")->fetchAll();
|
|
break;
|
|
case 'settings':
|
|
// Already fetched globally
|
|
break;
|
|
case 'sales':
|
|
case 'purchases':
|
|
$type = ($page === 'sales') ? 'sale' : 'purchase';
|
|
|
|
$where = ["v.type = ?"];
|
|
$params = [$type];
|
|
|
|
if (!empty($_GET['search'])) {
|
|
$where[] = "(v.id LIKE ? OR c.name LIKE ?)";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
}
|
|
|
|
if (!empty($_GET['customer_id'])) {
|
|
$where[] = "v.customer_id = ?";
|
|
$params[] = $_GET['customer_id'];
|
|
}
|
|
|
|
if (!empty($_GET['start_date'])) {
|
|
$where[] = "v.invoice_date >= ?";
|
|
$params[] = $_GET['start_date'];
|
|
}
|
|
|
|
if (!empty($_GET['end_date'])) {
|
|
$where[] = "v.invoice_date <= ?";
|
|
$params[] = $_GET['end_date'];
|
|
}
|
|
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT v.*, c.name as customer_name, c.tax_id as customer_tax_id, c.phone as customer_phone
|
|
FROM invoices v
|
|
LEFT JOIN customers c ON v.customer_id = c.id
|
|
WHERE $whereSql
|
|
ORDER BY v.id DESC");
|
|
$stmt->execute($params);
|
|
$data['invoices'] = $stmt->fetchAll();
|
|
foreach ($data['invoices'] as &$inv) {
|
|
$inv['total_in_words'] = numberToWordsOMR($inv['total_with_vat']);
|
|
}
|
|
unset($inv);
|
|
|
|
$data['items_list'] = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate FROM stock_items ORDER BY name_en ASC")->fetchAll();
|
|
$data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = '" . ($type === 'sale' ? 'customer' : 'supplier') . "' ORDER BY name ASC")->fetchAll();
|
|
break;
|
|
|
|
case 'customer_statement':
|
|
case 'supplier_statement':
|
|
$type = ($page === 'customer_statement') ? 'customer' : 'supplier';
|
|
$invType = ($type === 'customer') ? 'sale' : 'purchase';
|
|
$data['entities'] = db()->query("SELECT id, name, balance FROM customers WHERE type = '$type' ORDER BY name ASC")->fetchAll();
|
|
|
|
$entity_id = (int)($_GET['entity_id'] ?? 0);
|
|
if ($entity_id) {
|
|
$data['selected_entity'] = db()->query("SELECT * FROM customers WHERE id = $entity_id")->fetch();
|
|
$start_date = $_GET['start_date'] ?? date('Y-m-01');
|
|
$end_date = $_GET['end_date'] ?? date('Y-m-d');
|
|
|
|
// Fetch Opening Balance (Balance before start_date)
|
|
// This is complex as we don't have a ledger table.
|
|
// We can calculate it: Initial Balance + Invoices(before start_date) - Payments(before start_date)
|
|
// But for now, let's just show all transactions if no date filter, or just transactions in range.
|
|
|
|
$stmt = db()->prepare("SELECT 'invoice' as trans_type, id, invoice_date as trans_date, total_with_vat as amount, status, id as ref_no
|
|
FROM invoices
|
|
WHERE customer_id = ? AND type = ? AND invoice_date BETWEEN ? AND ?");
|
|
$stmt->execute([$entity_id, $invType, $start_date, $end_date]);
|
|
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$stmt = db()->prepare("SELECT 'payment' as trans_type, p.id, p.payment_date as trans_date, p.amount, p.payment_method, p.invoice_id as ref_no
|
|
FROM payments p
|
|
JOIN invoices i ON p.invoice_id = i.id
|
|
WHERE i.customer_id = ? AND i.type = ? AND p.payment_date BETWEEN ? AND ?");
|
|
$stmt->execute([$entity_id, $invType, $start_date, $end_date]);
|
|
$payments = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$transactions = array_merge($invoices, $payments);
|
|
usort($transactions, function($a, $b) {
|
|
return strtotime($a['trans_date']) <=> strtotime($b['trans_date']);
|
|
});
|
|
|
|
$data['transactions'] = $transactions;
|
|
}
|
|
break;
|
|
case 'expense_categories':
|
|
$data['expense_categories'] = db()->query("SELECT * FROM expense_categories ORDER BY name_en ASC")->fetchAll();
|
|
break;
|
|
case 'expenses':
|
|
$where = ["1=1"];
|
|
$params = [];
|
|
if (!empty($_GET['category_id'])) {
|
|
$where[] = "e.category_id = ?";
|
|
$params[] = $_GET['category_id'];
|
|
}
|
|
if (!empty($_GET['start_date'])) {
|
|
$where[] = "e.expense_date >= ?";
|
|
$params[] = $_GET['start_date'];
|
|
}
|
|
if (!empty($_GET['end_date'])) {
|
|
$where[] = "e.expense_date <= ?";
|
|
$params[] = $_GET['end_date'];
|
|
}
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT e.*, c.name_en as cat_en, c.name_ar as cat_ar
|
|
FROM expenses e
|
|
LEFT JOIN expense_categories c ON e.category_id = c.id
|
|
WHERE $whereSql
|
|
ORDER BY e.expense_date DESC, e.id DESC");
|
|
$stmt->execute($params);
|
|
$data['expenses'] = $stmt->fetchAll();
|
|
$data['expense_categories'] = db()->query("SELECT * FROM expense_categories ORDER BY name_en ASC")->fetchAll();
|
|
break;
|
|
case 'expense_report':
|
|
$start_date = $_GET['start_date'] ?? date('Y-m-01');
|
|
$end_date = $_GET['end_date'] ?? date('Y-m-d');
|
|
|
|
$stmt = db()->prepare("SELECT c.name_en, c.name_ar, SUM(e.amount) as total
|
|
FROM expenses e
|
|
JOIN expense_categories c ON e.category_id = c.id
|
|
WHERE e.expense_date BETWEEN ? AND ?
|
|
GROUP BY c.id
|
|
ORDER BY total DESC");
|
|
$stmt->execute([$start_date, $end_date]);
|
|
$data['report_by_category'] = $stmt->fetchAll();
|
|
|
|
$stmt = db()->prepare("SELECT SUM(amount) FROM expenses WHERE expense_date BETWEEN ? AND ?");
|
|
$stmt->execute([$start_date, $end_date]);
|
|
$data['total_expenses'] = $stmt->fetchColumn() ?: 0;
|
|
break;
|
|
default:
|
|
$data['customers'] = db()->query("SELECT * FROM customers WHERE type = 'customer' ORDER BY id DESC LIMIT 5")->fetchAll();
|
|
// Dashboard stats
|
|
$data['stats'] = [
|
|
'total_customers' => db()->query("SELECT COUNT(*) FROM customers WHERE type = 'customer'")->fetchColumn(),
|
|
'total_items' => db()->query("SELECT COUNT(*) FROM stock_items")->fetchColumn(),
|
|
'total_sales' => db()->query("SELECT SUM(total_with_vat) FROM invoices WHERE type = 'sale'")->fetchColumn() ?: 0,
|
|
'total_received' => db()->query("SELECT SUM(amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.type = 'sale'")->fetchColumn() ?: 0,
|
|
'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM invoices WHERE type = 'purchase'")->fetchColumn() ?: 0,
|
|
'total_paid' => db()->query("SELECT SUM(amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.type = 'purchase'")->fetchColumn() ?: 0,
|
|
];
|
|
$data['stats']['total_receivable'] = $data['stats']['total_sales'] - $data['stats']['total_received'];
|
|
$data['stats']['total_payable'] = $data['stats']['total_purchases'] - $data['stats']['total_paid'];
|
|
break;
|
|
}
|
|
|
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en" dir="ltr">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title data-en="Accounting Admin" data-ar="لوحة التحكم المحاسبية">Accounting Admin</title>
|
|
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
|
<?php if (!empty($data['settings']['favicon'])): ?>
|
|
<link rel="icon" href="<?= htmlspecialchars($data['settings']['favicon']) ?>?v=<?= time() ?>">
|
|
<?php endif; ?>
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
|
<style>
|
|
@media print {
|
|
.sidebar, .topbar, .d-print-none, .no-print { display: none !important; }
|
|
.main-content { margin-left: 0 !important; padding: 0 !important; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="sidebar">
|
|
<div class="sidebar-header" data-en="Accounting" data-ar="المحاسبة">Accounting</div>
|
|
<nav class="mt-4">
|
|
<!-- General Section -->
|
|
<a href="index.php?page=dashboard" class="nav-link <?= !isset($_GET['page']) || $_GET['page'] === 'dashboard' ? 'active' : '' ?>">
|
|
<i class="bi bi-speedometer2"></i> <span data-en="Dashboard" data-ar="لوحة القيادة">Dashboard</span>
|
|
</a>
|
|
|
|
<!-- Operations Section -->
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['sales', 'purchases']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#ops-collapse">
|
|
<span data-en="Operations" data-ar="العمليات">Operations</span>
|
|
<i class="bi bi-chevron-down"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['sales', 'purchases', 'pos', 'quotations']) ? 'show' : '' ?>" id="ops-collapse">
|
|
<a href="index.php?page=pos" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'pos' ? 'active' : '' ?>">
|
|
<i class="bi bi-display"></i> <span data-en="Point of Sale" data-ar="نقطة البيع">Point of Sale</span>
|
|
</a>
|
|
<a href="index.php?page=sales" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'sales' ? 'active' : '' ?>">
|
|
<i class="bi bi-cart"></i> <span data-en="Sales Tax Invoices" data-ar="فواتير المبيعات الضريبية">Sales Tax Invoices</span>
|
|
</a>
|
|
<a href="index.php?page=purchases" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'purchases' ? 'active' : '' ?>">
|
|
<i class="bi bi-bag"></i> <span data-en="Purchase Tax Invoices" data-ar="فواتير المشتريات الضريبية">Purchase Tax Invoices</span>
|
|
</a>
|
|
<a href="index.php?page=quotations" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'quotations' ? 'active' : '' ?>">
|
|
<i class="bi bi-file-earmark-text"></i> <span data-en="Quotations" data-ar="العروض">Quotations</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Expenses Section -->
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['expense_categories', 'expenses', 'expense_report']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#expenses-collapse">
|
|
<span data-en="Expenses" data-ar="المصروفات">Expenses</span>
|
|
<i class="bi bi-chevron-down"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['expense_categories', 'expenses', 'expense_report']) ? 'show' : '' ?>" id="expenses-collapse">
|
|
<a href="index.php?page=expense_categories" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'expense_categories' ? 'active' : '' ?>">
|
|
<i class="bi bi-tags"></i> <span data-en="Categories" data-ar="الفئات">Categories</span>
|
|
</a>
|
|
<a href="index.php?page=expenses" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'expenses' ? 'active' : '' ?>">
|
|
<i class="bi bi-wallet2"></i> <span data-en="Expenses List" data-ar="قائمة المصروفات">Expenses List</span>
|
|
</a>
|
|
<a href="index.php?page=expense_report" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'expense_report' ? 'active' : '' ?>">
|
|
<i class="bi bi-file-earmark-bar-graph"></i> <span data-en="Expense Report" data-ar="تقرير المصروفات">Expense Report</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Inventory Section -->
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['items', 'categories', 'units']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#inventory-collapse">
|
|
<span data-en="Inventory" data-ar="المخزون">Inventory</span>
|
|
<i class="bi bi-chevron-down"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['items', 'categories', 'units']) ? 'show' : '' ?>" id="inventory-collapse">
|
|
<a href="index.php?page=items" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'items' ? 'active' : '' ?>">
|
|
<i class="bi bi-box-seam"></i> <span data-en="Items" data-ar="الأصناف">Items</span>
|
|
</a>
|
|
<a href="index.php?page=categories" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'categories' ? 'active' : '' ?>">
|
|
<i class="bi bi-tags"></i> <span data-en="Categories" data-ar="الفئات">Categories</span>
|
|
</a>
|
|
<a href="index.php?page=units" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'units' ? 'active' : '' ?>">
|
|
<i class="bi bi-calculator"></i> <span data-en="Units" data-ar="الوحدات">Units</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- People Section -->
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['customers', 'suppliers']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#relationships-collapse">
|
|
<span data-en="Relationships" data-ar="العلاقات">Relationships</span>
|
|
<i class="bi bi-chevron-down"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['customers', 'suppliers']) ? 'show' : '' ?>" id="relationships-collapse">
|
|
<a href="index.php?page=customers" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'customers' ? 'active' : '' ?>">
|
|
<i class="bi bi-people"></i> <span data-en="Customers" data-ar="العملاء">Customers</span>
|
|
</a>
|
|
<a href="index.php?page=suppliers" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'suppliers' ? 'active' : '' ?>">
|
|
<i class="bi bi-truck"></i> <span data-en="Suppliers" data-ar="الموردون">Suppliers</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Reports Section -->
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['customer_statement', 'supplier_statement']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#reports-collapse">
|
|
<span data-en="Reports" data-ar="التقارير">Reports</span>
|
|
<i class="bi bi-chevron-down"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['customer_statement', 'supplier_statement']) ? 'show' : '' ?>" id="reports-collapse">
|
|
<a href="index.php?page=customer_statement" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'customer_statement' ? 'active' : '' ?>">
|
|
<i class="bi bi-file-earmark-text"></i> <span data-en="Customer Statement" data-ar="كشف حساب عميل">Customer Statement</span>
|
|
</a>
|
|
<a href="index.php?page=supplier_statement" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'supplier_statement' ? 'active' : '' ?>">
|
|
<i class="bi bi-file-earmark-medical"></i> <span data-en="Supplier Statement" data-ar="كشف حساب مورد">Supplier Statement</span>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Configuration Section -->
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['payment_methods', 'settings']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#config-collapse">
|
|
<span data-en="Configuration" data-ar="الإعدادات">Configuration</span>
|
|
<i class="bi bi-chevron-down"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['payment_methods', 'settings']) ? 'show' : '' ?>" id="config-collapse">
|
|
<a href="index.php?page=payment_methods" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'payment_methods' ? 'active' : '' ?>">
|
|
<i class="bi bi-credit-card"></i> <span data-en="Payment Methods" data-ar="طرق الدفع">Payment Methods</span>
|
|
</a>
|
|
<a href="index.php?page=settings" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'settings' ? 'active' : '' ?>">
|
|
<i class="bi bi-building"></i> <span data-en="Company Profile" data-ar="ملف الشركة">Company Profile</span>
|
|
</a>
|
|
<a href="#" class="nav-link">
|
|
<i class="bi bi-person-badge"></i> <span data-en="HR" data-ar="الموارد البشرية">HR</span>
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<header class="topbar">
|
|
<h4 class="m-0">
|
|
<?php
|
|
$titles = [
|
|
'dashboard' => ['en' => 'Dashboard', 'ar' => 'لوحة القيادة'],
|
|
'pos' => ['en' => 'Point of Sale', 'ar' => 'نقطة البيع'],
|
|
'quotations' => ['en' => 'Quotations', 'ar' => 'العروض'],
|
|
'customers' => ['en' => 'Customers', 'ar' => 'العملاء'],
|
|
'suppliers' => ['en' => 'Suppliers', 'ar' => 'الموردون'],
|
|
'categories' => ['en' => 'Stock Categories', 'ar' => 'فئات المخزون'],
|
|
'units' => ['en' => 'Stock Units', 'ar' => 'وحدات المخزون'],
|
|
'items' => ['en' => 'Stock Items', 'ar' => 'أصناف المخزون'],
|
|
'payment_methods' => ['en' => 'Payment Methods', 'ar' => 'طرق الدفع'],
|
|
'sales' => ['en' => 'Sales Tax Invoices', 'ar' => 'فواتير المبيعات الضريبية'],
|
|
'purchases' => ['en' => 'Purchase Tax Invoices', 'ar' => 'فواتير المشتريات الضريبية'],
|
|
'customer_statement' => ['en' => 'Customer Statement', 'ar' => 'كشف حساب عميل'],
|
|
'supplier_statement' => ['en' => 'Supplier Statement', 'ar' => 'كشف حساب مورد'],
|
|
'settings' => ['en' => 'Company Profile', 'ar' => 'ملف الشركة'],
|
|
];
|
|
$currTitle = $titles[$page] ?? $titles['dashboard'];
|
|
?>
|
|
<span data-en="<?= $currTitle['en'] ?>" data-ar="<?= $currTitle['ar'] ?>"><?= $currTitle['en'] ?></span>
|
|
</h4>
|
|
<div class="d-flex align-items-center">
|
|
<button id="langToggle" class="btn btn-outline-secondary btn-sm me-3">
|
|
<i class="bi bi-translate"></i> <span data-en="العربية" data-ar="English">العربية</span>
|
|
</button>
|
|
<div class="dropdown">
|
|
<button class="btn btn-link dropdown-toggle text-dark text-decoration-none" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-person-circle"></i> Admin
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><a class="dropdown-item" href="index.php?page=settings" data-en="Company Profile" data-ar="ملف الشركة">Company Profile</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item" href="#" data-en="Logout" data-ar="تسجيل الخروج">Logout</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<?php if ($message): ?>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
let msg = <?= json_encode($message) ?>;
|
|
let type = 'success';
|
|
let title = 'Success';
|
|
|
|
if (msg.toLowerCase().includes('error') || msg.toLowerCase().includes('failed')) {
|
|
type = 'error';
|
|
title = 'Error';
|
|
}
|
|
|
|
Swal.fire({
|
|
icon: type,
|
|
title: title,
|
|
text: msg,
|
|
timer: 3000,
|
|
timerProgressBar: true,
|
|
showConfirmButton: false
|
|
});
|
|
});
|
|
</script>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($page === 'dashboard'): ?>
|
|
<div class="row g-4 mb-4">
|
|
<div class="col">
|
|
<div class="card p-3 border-start border-primary border-4 h-100">
|
|
<div class="text-muted small" data-en="Total Sales" data-ar="إجمالي المبيعات">Total Sales</div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_sales'] ?? 0), 3) ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card p-3 border-start border-warning border-4 h-100">
|
|
<div class="text-muted small" data-en="Total Received" data-ar="إجمالي المبالغ المحصلة">Total Received</div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_received'] ?? 0), 3) ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card p-3 border-start border-danger border-4 h-100">
|
|
<div class="text-muted small" data-en="Total Receivable" data-ar="إجمالي المبالغ المتبقية">Total Receivable</div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_receivable'] ?? 0), 3) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col">
|
|
<div class="card p-3 border-start border-dark border-4 h-100 bg-light">
|
|
<div class="text-muted small" data-en="Total Purchases" data-ar="إجمالي المشتريات">Total Purchases</div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_purchases'] ?? 0), 3) ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card p-3 border-start border-info border-4 h-100 bg-light">
|
|
<div class="text-muted small" data-en="Total Paid to Suppliers" data-ar="إجمالي المدفوع للموردين">Total Paid to Suppliers</div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_paid'] ?? 0), 3) ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card p-3 border-start border-secondary border-4 h-100 bg-light">
|
|
<div class="text-muted small" data-en="Total Payable" data-ar="إجمالي الذمم الدائنة">Total Payable</div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_payable'] ?? 0), 3) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col">
|
|
<div class="card p-3 border-start border-success border-4 h-100">
|
|
<div class="text-muted small" data-en="Total Customers" data-ar="إجمالي العملاء">Total Customers</div>
|
|
<div class="h4 m-0"><?= (int)($data['stats']['total_customers'] ?? 0) ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col">
|
|
<div class="card p-3 border-start border-info border-4 h-100">
|
|
<div class="text-muted small" data-en="Total Items" data-ar="إجمالي الأصناف">Total Items</div>
|
|
<div class="h4 m-0"><?= (int)($data['stats']['total_items'] ?? 0) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Recent Customers" data-ar="العملاء الحاليين">Recent Customers</h5>
|
|
<a href="index.php?page=customers" class="btn btn-outline-primary btn-sm">
|
|
<span data-en="View All" data-ar="عرض الكل">View All</span>
|
|
</a>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Name" data-ar="الاسم">Name</th>
|
|
<th data-en="Phone" data-ar="الهاتف">Phone</th>
|
|
<th data-en="Balance" data-ar="الرصيد" class="text-end">Balance</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['customers'] as $c): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($c['name']) ?></td>
|
|
<td><?= htmlspecialchars($c['phone']) ?></td>
|
|
<td class="text-end">OMR <?= number_format((float)$c['balance'], 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card p-4">
|
|
<h5 class="mb-4" data-en="Quick Links" data-ar="روابط سريعة">Quick Links</h5>
|
|
<div class="list-group list-group-flush">
|
|
<button class="list-group-item list-group-item-action border-0 px-0" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
|
|
<i class="bi bi-person-plus text-primary"></i> <span data-en="Add Customer" data-ar="إضافة عميل">Add Customer</span>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action border-0 px-0" data-bs-toggle="modal" data-bs-target="#addItemModal">
|
|
<i class="bi bi-box-seam text-success"></i> <span data-en="Add Item" data-ar="إضافة صنف">Add Item</span>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action border-0 px-0" data-bs-toggle="modal" data-bs-target="#importItemsModal">
|
|
<i class="bi bi-file-earmark-excel text-success"></i> <span data-en="Import Items" data-ar="استيراد أصناف">Import Items</span>
|
|
</button>
|
|
<a href="index.php?page=sales" class="list-group-item list-group-item-action border-0 px-0">
|
|
<i class="bi bi-cart text-primary"></i> <span data-en="Sales Tax Invoices" data-ar="فواتير المبيعات الضريبية">Sales Tax Invoices</span>
|
|
</a>
|
|
<a href="index.php?page=purchases" class="list-group-item list-group-item-action border-0 px-0">
|
|
<i class="bi bi-bag text-warning"></i> <span data-en="Purchase Tax Invoices" data-ar="فواتير المشتريات الضريبية">Purchase Tax Invoices</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($page === 'customers' || $page === 'suppliers'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="<?= $currTitle['en'] ?> Management" data-ar="إدارة <?= $currTitle['ar'] ?>"><?= $currTitle['en'] ?> Management</h5>
|
|
<div>
|
|
<button class="btn btn-outline-success me-2" data-bs-toggle="modal" data-bs-target="<?= $page === 'suppliers' ? '#importSuppliersModal' : '#importCustomersModal' ?>">
|
|
<i class="bi bi-file-earmark-excel"></i> <span data-en="Import Excel" data-ar="استيراد من اكسل">Import Excel</span>
|
|
</button>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add <?= $currTitle['en'] ?>" data-ar="إضافة <?= $currTitle['ar'] ?>">Add <?= $currTitle['en'] ?></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search & Filter Bar -->
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="row g-2 align-items-end">
|
|
<input type="hidden" name="page" value="<?= $page ?>">
|
|
<div class="col-md-4">
|
|
<label class="form-label small" data-en="Search" data-ar="بحث">Search</label>
|
|
<input type="text" name="search" class="form-control" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Name, email, or phone..." data-en="Name, email, or phone..." data-ar="الاسم، البريد، أو الهاتف...">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small" data-en="From Date" data-ar="من تاريخ">From Date</label>
|
|
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($_GET['start_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small" data-en="To Date" data-ar="إلى تاريخ">To Date</label>
|
|
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($_GET['end_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-4 d-flex gap-1">
|
|
<button type="submit" class="btn btn-primary flex-grow-1">
|
|
<i class="bi bi-filter"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
|
</button>
|
|
<a href="index.php?page=export&type=<?= $page ?>&<?= http_build_query($_GET) ?>" class="btn btn-outline-success">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</a>
|
|
<?php if (!empty($_GET['search']) || !empty($_GET['start_date']) || !empty($_GET['end_date'])): ?>
|
|
<a href="index.php?page=<?= $page ?>" class="btn btn-outline-secondary">
|
|
<i class="bi bi-x-lg"></i>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Name" data-ar="الاسم">Name</th>
|
|
<th data-en="Tax ID" data-ar="الرقم الضريبي">Tax ID</th>
|
|
<th data-en="Email" data-ar="البريد">Email</th>
|
|
<th data-en="Phone" data-ar="الهاتف">Phone</th>
|
|
<th data-en="Balance" data-ar="الرصيد" class="text-end">Balance</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['customers'] as $c): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($c['name']) ?></td>
|
|
<td><?= htmlspecialchars($c['tax_id'] ?? '---') ?></td>
|
|
<td><?= htmlspecialchars($c['email']) ?></td>
|
|
<td><?= htmlspecialchars($c['phone']) ?></td>
|
|
<td class="text-end">OMR <?= number_format((float)$c['balance'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" title="Edit" data-bs-toggle="modal" data-bs-target="#editCustomerModal<?= $c['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $c['id'] ?>">
|
|
<button type="submit" name="delete_customer" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Edit Customer Modal -->
|
|
<div class="modal fade" id="editCustomerModal<?= $c['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<span data-en="Edit <?= $currTitle['en'] ?>" data-ar="تعديل <?= $currTitle['ar'] ?>">Edit <?= $currTitle['en'] ?></span>
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $c['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name" data-ar="الاسم">Name</label>
|
|
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($c['name']) ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Email" data-ar="البريد الإلكتروني">Email</label>
|
|
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($c['email'] ?? '') ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars($c['phone'] ?? '') ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Tax ID / VAT No" data-ar="الرقم الضريبي">Tax ID / VAT No</label>
|
|
<input type="text" name="tax_id" class="form-control" value="<?= htmlspecialchars($c['tax_id'] ?? '') ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Balance" data-ar="الرصيد">Balance</label>
|
|
<input type="number" step="0.001" name="balance" class="form-control" value="<?= (float)$c['balance'] ?>">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_customer" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($page === 'categories'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Stock Categories" data-ar="فئات المخزون">Stock Categories</h5>
|
|
<div>
|
|
<button class="btn btn-outline-success me-2" data-bs-toggle="modal" data-bs-target="#importCategoriesModal">
|
|
<i class="bi bi-file-earmark-excel"></i> <span data-en="Import Excel" data-ar="استيراد من اكسل">Import Excel</span>
|
|
</button>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCategoryModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Category" data-ar="إضافة فئة">Add Category</span>
|
|
</button>
|
|
</div>
|
|
</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 (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</th>
|
|
<th data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['categories'] as $cat): ?>
|
|
<tr>
|
|
<td><?= $cat['id'] ?></td>
|
|
<td><?= htmlspecialchars($cat['name_en']) ?></td>
|
|
<td><?= htmlspecialchars($cat['name_ar']) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'units'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Stock Units" data-ar="وحدات المخزون">Stock Units</h5>
|
|
<div>
|
|
<button class="btn btn-outline-success me-2" data-bs-toggle="modal" data-bs-target="#importUnitsModal">
|
|
<i class="bi bi-file-earmark-excel"></i> <span data-en="Import Excel" data-ar="استيراد من اكسل">Import Excel</span>
|
|
</button>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addUnitModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Unit" data-ar="إضافة وحدة">Add Unit</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</th>
|
|
<th data-en="Short (EN)" data-ar="الاختصار (إنجليزي)">Short (EN)</th>
|
|
<th data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</th>
|
|
<th data-en="Short (AR)" data-ar="الاختصار (عربي)">Short (AR)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['units'] as $u): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($u['name_en']) ?></td>
|
|
<td><?= htmlspecialchars($u['short_name_en']) ?></td>
|
|
<td><?= htmlspecialchars($u['name_ar']) ?></td>
|
|
<td><?= htmlspecialchars($u['short_name_ar']) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'items'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Stock Items" data-ar="أصناف المخزون">Stock Items</h5>
|
|
<div class="d-flex align-items-center">
|
|
<button class="btn btn-dark me-2" id="bulkBarcodeBtn" style="display:none;" onclick="openAveryModal()">
|
|
<i class="bi bi-printer"></i> <span data-en="Avery Labels" data-ar="ملصقات إيفري">Avery Labels</span>
|
|
</button>
|
|
<button class="btn btn-outline-success me-2" data-bs-toggle="modal" data-bs-target="#importItemsModal">
|
|
<i class="bi bi-file-earmark-excel"></i> <span data-en="Import Excel" data-ar="استيراد من اكسل">Import Excel</span>
|
|
</button>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addItemModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Item" data-ar="إضافة صنف">Add Item</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Bar -->
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="row g-2 align-items-end">
|
|
<input type="hidden" name="page" value="items">
|
|
<div class="col-md-7">
|
|
<label class="form-label small" data-en="Search" data-ar="بحث">Search</label>
|
|
<input type="text" name="search" class="form-control" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Name or SKU..." data-en="Name or SKU..." data-ar="الاسم أو الباركود...">
|
|
</div>
|
|
<div class="col-md-5 d-flex gap-1">
|
|
<button type="submit" class="btn btn-primary flex-grow-1">
|
|
<i class="bi bi-search"></i> <span data-en="Search" data-ar="بحث">Search</span>
|
|
</button>
|
|
<a href="index.php?page=export&type=items&<?= http_build_query($_GET) ?>" class="btn btn-outline-success">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</a>
|
|
<?php if (!empty($_GET['search'])): ?>
|
|
<a href="index.php?page=items" class="btn btn-outline-secondary">
|
|
<i class="bi bi-x-lg"></i>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 40px;"><input type="checkbox" id="selectAllItems" class="form-check-input"></th>
|
|
<th data-en="Image" data-ar="الصورة">Image</th>
|
|
<th data-en="SKU" data-ar="الباركود">SKU</th>
|
|
<th data-en="Name" data-ar="الاسم">Name</th>
|
|
<th data-en="Category" data-ar="الفئة">Category</th>
|
|
<th data-en="Supplier" data-ar="المورد">Supplier</th>
|
|
<th data-en="Stock Level" data-ar="المخزون">Stock Level</th>
|
|
<th data-en="Expiry" data-ar="تاريخ الانتهاء">Expiry</th>
|
|
<th data-en="VAT" data-ar="الضريبة">VAT</th>
|
|
<th data-en="Actions" data-ar="الإجراءات">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['items'] as $item): ?>
|
|
<tr>
|
|
<td><input type="checkbox" class="form-check-input item-checkbox" data-id="<?= $item['id'] ?>" data-sku="<?= htmlspecialchars($item['sku']) ?>" data-name="<?= htmlspecialchars($item['name_en']) ?>" data-price="<?= number_format((float)$item['sale_price'], 3) ?>"></td>
|
|
<td>
|
|
<?php if ($item['image_path']): ?>
|
|
<img src="<?= htmlspecialchars($item['image_path']) ?>" alt="item" style="width: 40px; height: 40px; object-fit: cover;" class="rounded">
|
|
<?php else: ?>
|
|
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
|
<i class="bi bi-image text-muted"></i>
|
|
</div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= htmlspecialchars($item['sku']) ?></td>
|
|
<td>
|
|
<div class="fw-bold"><?= htmlspecialchars($item['name_en']) ?></div>
|
|
<div class="small text-muted"><?= htmlspecialchars($item['name_ar']) ?></div>
|
|
</td>
|
|
<td><span data-en="<?= htmlspecialchars($item['cat_en']) ?>" data-ar="<?= htmlspecialchars($item['cat_ar']) ?>"><?= htmlspecialchars($item['cat_en']) ?></span></td>
|
|
<td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td>
|
|
<td>
|
|
<div class="text-end">
|
|
<strong><?= number_format((float)$item['stock_quantity'], 3) ?></strong>
|
|
<div class="small text-muted">Min: <?= number_format((float)$item['min_stock_level'], 3) ?></div>
|
|
<?php if ($item['stock_quantity'] <= $item['min_stock_level']): ?>
|
|
<span class="badge bg-danger" data-en="Low Stock" data-ar="مخزون منخفض">Low Stock</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</td>
|
|
<td><?= $item['expiry_date'] ?: '---' ?></td>
|
|
<td><?= number_format((float)$item['vat_rate'], 3) ?>%</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info" title="View" data-bs-toggle="modal" data-bs-target="#viewItemModal<?= $item['id'] ?>"><i class="bi bi-eye"></i></button>
|
|
<button class="btn btn-outline-primary" title="Edit" data-bs-toggle="modal" data-bs-target="#editItemModal<?= $item['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<button class="btn btn-outline-dark" title="Barcode" onclick="printItemBarcode('<?= htmlspecialchars($item['sku']) ?>', '<?= htmlspecialchars($item['name_en']) ?>', '<?= number_format((float)$item['sale_price'], 3) ?>')"><i class="bi bi-upc"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $item['id'] ?>">
|
|
<button type="submit" name="delete_item" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- View Item Modal -->
|
|
<div class="modal fade" id="viewItemModal<?= $item['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><?= htmlspecialchars($item['name_en']) ?></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-center mb-3">
|
|
<?php if ($item['image_path']): ?>
|
|
<img src="<?= htmlspecialchars($item['image_path']) ?>" class="img-fluid rounded shadow-sm" style="max-height: 200px;">
|
|
<?php else: ?>
|
|
<div class="bg-light rounded d-flex align-items-center justify-content-center mx-auto" style="width: 150px; height: 150px;">
|
|
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<table class="table table-sm">
|
|
<tr><th class="text-muted">SKU</th><td><?= htmlspecialchars($item['sku']) ?></td></tr>
|
|
<tr><th class="text-muted">Category</th><td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td></tr>
|
|
<tr><th class="text-muted">Supplier</th><td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td></tr>
|
|
<tr><th class="text-muted">Sale Price</th><td>OMR <?= number_format((float)$item['sale_price'], 3) ?></td></tr>
|
|
<tr><th class="text-muted">Stock Level</th><td><?= number_format((float)$item['stock_quantity'], 3) ?></td></tr>
|
|
<tr><th class="text-muted">VAT Rate</th><td><?= number_format((float)$item['vat_rate'], 3) ?>%</td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline-dark" onclick="printItemBarcode('<?= htmlspecialchars($item['sku']) ?>', '<?= htmlspecialchars($item['name_en']) ?>', '<?= number_format((float)$item['sale_price'], 3) ?>')"><i class="bi bi-printer"></i> Print Barcode</button>
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Item Modal -->
|
|
<div class="modal fade" id="editItemModal<?= $item['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Edit Item" data-ar="تعديل صنف">Edit Item</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<input type="hidden" name="id" value="<?= $item['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" value="<?= htmlspecialchars($item['name_en']) ?>" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" value="<?= htmlspecialchars($item['name_ar']) ?>" required>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Category</label>
|
|
<select name="category_id" class="form-select">
|
|
<option value="">---</option>
|
|
<?php foreach ($data['categories'] ?? [] as $c): ?>
|
|
<option value="<?= $c['id'] ?>" <?= $c['id'] == $item['category_id'] ? 'selected' : '' ?>><?= htmlspecialchars($c['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Unit</label>
|
|
<select name="unit_id" class="form-select">
|
|
<option value="">---</option>
|
|
<?php foreach ($data['units'] ?? [] as $u): ?>
|
|
<option value="<?= $u['id'] ?>" <?= $u['id'] == $item['unit_id'] ? 'selected' : '' ?>><?= htmlspecialchars($u['short_name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Supplier</label>
|
|
<select name="supplier_id" class="form-select">
|
|
<option value="">---</option>
|
|
<?php foreach ($data['suppliers'] ?? [] as $s): ?>
|
|
<option value="<?= $s['id'] ?>" <?= $s['id'] == $item['supplier_id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Sale Price</label>
|
|
<input type="number" step="0.001" name="sale_price" class="form-control" value="<?= (float)$item['sale_price'] ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Purchase Price</label>
|
|
<input type="number" step="0.001" name="purchase_price" class="form-control" value="<?= (float)$item['purchase_price'] ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Stock Qty</label>
|
|
<input type="number" step="0.001" name="stock_quantity" class="form-control" value="<?= (float)$item['stock_quantity'] ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Min Level</label>
|
|
<input type="number" step="0.001" name="min_stock_level" class="form-control" value="<?= (float)$item['min_stock_level'] ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">SKU</label>
|
|
<input type="text" name="sku" class="form-control" value="<?= htmlspecialchars($item['sku']) ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">VAT Rate (%)</label>
|
|
<input type="number" step="0.001" name="vat_rate" class="form-control" value="<?= (float)$item['vat_rate'] ?>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Item Picture</label>
|
|
<input type="file" name="image" class="form-control" accept="image/*">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" name="edit_item" class="btn btn-primary">Update Item</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'pos'): ?>
|
|
<?php
|
|
$products = db()->query("SELECT * FROM stock_items WHERE stock_quantity > 0 ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
$customers = db()->query("SELECT * FROM customers WHERE type = 'customer' ORDER BY name ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
?>
|
|
<div class="pos-container">
|
|
<div class="pos-products">
|
|
<div class="bg-white p-3 rounded mb-3 shadow-sm d-flex gap-2">
|
|
<div class="input-group flex-grow-1">
|
|
<span class="input-group-text bg-transparent border-end-0"><i class="bi bi-search"></i></span>
|
|
<input type="text" id="productSearch" class="form-control border-start-0" placeholder="Search products by name or SKU..." data-en="Search products..." data-ar="بحث عن منتجات...">
|
|
</div>
|
|
<div class="input-group" style="width: 200px;">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-upc-scan"></i></span>
|
|
<input type="text" id="barcodeInput" class="form-control border-start-0" placeholder="Scan barcode..." data-en="Scan barcode..." data-ar="امسح الباركود..." autofocus>
|
|
</div>
|
|
<button class="btn btn-warning d-flex align-items-center gap-2" onclick="cart.openHeldCartsModal()">
|
|
<i class="bi bi-pause-btn-fill"></i>
|
|
<span class="d-none d-xl-inline" data-en="Held List" data-ar="قائمة الانتظار">Held List</span>
|
|
</button>
|
|
</div>
|
|
<div class="product-grid" id="productGrid">
|
|
<?php foreach ($products as $p): ?>
|
|
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>">
|
|
<?php if ($p['image_path']): ?>
|
|
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
|
|
<?php else: ?>
|
|
<div class="bg-light d-flex align-items-center justify-content-center rounded mb-2" style="height: 120px;">
|
|
<i class="bi bi-box-seam text-muted" style="font-size: 3rem;"></i>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="fw-bold mb-1 product-name" data-en="<?= htmlspecialchars($p['name_en']) ?>" data-ar="<?= htmlspecialchars($p['name_ar']) ?>"><?= htmlspecialchars($p['name_en']) ?></div>
|
|
<div class="small text-muted mb-2"><?= htmlspecialchars($p['sku']) ?></div>
|
|
<div class="d-flex justify-content-between align-items-center mt-auto">
|
|
<span class="price text-primary">OMR <?= number_format((float)$p['sale_price'], 3) ?></span>
|
|
<span class="badge bg-light text-dark small"><?= (float)$p['stock_quantity'] ?> left</span>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pos-cart">
|
|
<div class="p-3 border-bottom d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 fw-bold"><i class="bi bi-cart3 me-2"></i>Cart</h6>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-sm btn-outline-warning" onclick="cart.openHeldCartsModal()" title="Held List"><i class="bi bi-list-task"></i></button>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="cart.hold()" title="Hold Cart"><i class="bi bi-pause-circle"></i></button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="cart.clear()" title="Clear Cart"><i class="bi bi-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-3 bg-light border-bottom">
|
|
<div class="mb-2">
|
|
<label class="small fw-bold mb-1">Customer</label>
|
|
<div class="d-flex gap-2">
|
|
<select id="posCustomer" class="form-select form-select-sm" onchange="cart.onCustomerChange()">
|
|
<option value="">Walk-in Customer</option>
|
|
<?php foreach ($customers as $c): ?>
|
|
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addCustomerModal"><i class="bi bi-plus"></i></button>
|
|
</div>
|
|
<div id="loyaltyDisplay" class="smaller text-success mt-1" style="display:none">
|
|
Points: <span id="customerPoints">0</span>
|
|
<div class="form-check form-switch d-inline-block ms-2">
|
|
<input class="form-check-input" type="checkbox" id="redeemLoyalty" onchange="cart.render()">
|
|
<label class="form-check-label" for="redeemLoyalty">Redeem</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="small fw-bold mb-1">Discount Code</label>
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" id="discountCode" class="form-control" placeholder="Code">
|
|
<button class="btn btn-outline-primary" type="button" onclick="cart.applyDiscount()">Apply</button>
|
|
</div>
|
|
<div id="appliedDiscountInfo" class="smaller text-primary mt-1" style="display:none"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cart-items" id="cartItems">
|
|
<!-- Cart items will be injected here -->
|
|
<div class="text-center text-muted mt-5">
|
|
<i class="bi bi-cart-x" style="font-size: 3rem;"></i>
|
|
<p>Cart is empty</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cart-total">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span>Subtotal</span>
|
|
<span id="posSubtotal">OMR 0.000</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-3 fw-bold fs-5">
|
|
<span>Total</span>
|
|
<span id="posTotal" class="text-primary">OMR 0.000</span>
|
|
</div>
|
|
<button class="btn btn-primary w-100 py-2 fw-bold" id="checkoutBtn" onclick="cart.checkout()">
|
|
PLACE ORDER
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const cart = {
|
|
items: [],
|
|
discount: null,
|
|
customerPoints: 0,
|
|
selectedPaymentMethod: 'cash',
|
|
payments: [],
|
|
allCreditCustomers: <?php
|
|
$custData = [];
|
|
foreach ($customers as $c) {
|
|
$custData[] = [
|
|
'value' => (string)$c['id'],
|
|
'text' => $c['name'] . ' (' . ($c['phone'] ?? '') . ')'
|
|
];
|
|
}
|
|
echo json_encode($custData);
|
|
?>,
|
|
add(product) {
|
|
const existing = this.items.find(item => item.id === product.id);
|
|
if (existing) {
|
|
existing.qty++;
|
|
} else {
|
|
this.items.push({...product, qty: 1});
|
|
}
|
|
this.render();
|
|
},
|
|
remove(id) {
|
|
this.items = this.items.filter(item => item.id !== id);
|
|
this.render();
|
|
},
|
|
updateQty(id, delta) {
|
|
const item = this.items.find(i => i.id === id);
|
|
if (item) {
|
|
item.qty += delta;
|
|
if (item.qty <= 0) this.remove(id);
|
|
else this.render();
|
|
}
|
|
},
|
|
clear() {
|
|
this.items = [];
|
|
this.discount = null;
|
|
this.customerPoints = 0;
|
|
const discInput = document.getElementById('discountCode');
|
|
if (discInput) discInput.value = '';
|
|
const discInfo = document.getElementById('appliedDiscountInfo');
|
|
if (discInfo) discInfo.style.display = 'none';
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
if (redeemSwitch) redeemSwitch.checked = false;
|
|
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
|
|
if (loyaltyDisplay) loyaltyDisplay.style.display = 'none';
|
|
this.render();
|
|
},
|
|
async onCustomerChange() {
|
|
const id = document.getElementById('posCustomer').value;
|
|
const display = document.getElementById('loyaltyDisplay');
|
|
if (!id) {
|
|
display.style.display = 'none';
|
|
this.customerPoints = 0;
|
|
document.getElementById('redeemLoyalty').checked = false;
|
|
this.render();
|
|
return;
|
|
}
|
|
try {
|
|
const resp = await fetch(`index.php?action=get_customer_loyalty&customer_id=${id}`);
|
|
const res = await resp.json();
|
|
this.customerPoints = res.points || 0;
|
|
document.getElementById('customerPoints').innerText = this.customerPoints.toFixed(3);
|
|
display.style.display = 'block';
|
|
this.render();
|
|
} catch (err) { console.error(err); }
|
|
},
|
|
async applyDiscount() {
|
|
const code = document.getElementById('discountCode').value.trim();
|
|
if (!code) return;
|
|
try {
|
|
const resp = await fetch(`index.php?action=validate_discount&code=${code}`);
|
|
const res = await resp.json();
|
|
if (res.success) {
|
|
this.discount = res.discount;
|
|
const info = document.getElementById('appliedDiscountInfo');
|
|
info.innerText = `Applied: ${this.discount.code} (${this.discount.type === 'percentage' ? this.discount.value + '%' : 'OMR ' + parseFloat(this.discount.value).toFixed(3)})`;
|
|
info.style.display = 'block';
|
|
this.render();
|
|
} else {
|
|
Swal.fire('Error', res.error, 'error');
|
|
}
|
|
} catch (err) { console.error(err); }
|
|
},
|
|
async hold() {
|
|
if (this.items.length === 0) return;
|
|
const { value: name } = await Swal.fire({
|
|
title: 'Hold Cart',
|
|
input: 'text',
|
|
inputLabel: 'Enter a name for this cart',
|
|
inputValue: 'Cart ' + new Date().toLocaleTimeString(),
|
|
showCancelButton: true
|
|
});
|
|
if (name) {
|
|
const formData = new FormData();
|
|
formData.append('action', 'hold_pos_cart');
|
|
formData.append('cart_name', name);
|
|
formData.append('items', JSON.stringify(this.items));
|
|
formData.append('customer_id', document.getElementById('posCustomer').value);
|
|
const resp = await fetch('index.php', { method: 'POST', body: formData });
|
|
const res = await resp.json();
|
|
if (res.success) {
|
|
this.clear();
|
|
Swal.fire('Held', 'Cart has been parked', 'success');
|
|
}
|
|
}
|
|
},
|
|
async openHeldCartsModal() {
|
|
try {
|
|
const resp = await fetch('index.php?action=get_held_carts');
|
|
const text = await resp.text();
|
|
let carts;
|
|
try {
|
|
carts = JSON.parse(text);
|
|
} catch (e) {
|
|
console.error('Failed to parse held carts:', text);
|
|
throw new Error('Invalid server response');
|
|
}
|
|
const lang = document.documentElement.lang || 'en';
|
|
let html = '<div class="list-group list-group-flush shadow-sm rounded">';
|
|
if (carts.length === 0) {
|
|
html += `
|
|
<div class="text-center p-5 text-muted">
|
|
<i class="bi bi-folder2-open mb-3 d-block" style="font-size: 3rem;"></i>
|
|
<p data-en="No held carts found" data-ar="لا توجد طلبات معلقة">${lang === 'ar' ? 'لا توجد طلبات معلقة' : 'No held carts found'}</p>
|
|
</div>`;
|
|
}
|
|
carts.forEach(c => {
|
|
html += `
|
|
<div class="list-group-item d-flex justify-content-between align-items-center p-3 hover-bg-light border-start-0 border-end-0">
|
|
<div class="text-start">
|
|
<div class="fw-bold text-primary">${c.cart_name}</div>
|
|
<div class="small text-muted">
|
|
<i class="bi bi-person me-1"></i>${c.customer_name || (lang === 'ar' ? 'عميل عابر' : 'Walk-in')}
|
|
<span class="mx-2 text-silver">|</span>
|
|
<i class="bi bi-clock me-1"></i>${new Date(c.created_at).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-sm btn-primary" onclick="cart.resume(${c.id})">
|
|
<i class="bi bi-arrow-repeat me-1"></i><span data-en="Resume" data-ar="استرجاع">${lang === 'ar' ? 'استرجاع' : 'Resume'}</span>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="cart.deleteHeld(${c.id})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
Swal.fire({
|
|
title: lang === 'ar' ? 'الطلبات المعلقة' : 'Held Carts',
|
|
html: html,
|
|
showConfirmButton: false,
|
|
width: '700px',
|
|
customClass: {
|
|
container: 'held-carts-swal'
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
Swal.fire('Error', 'Failed to load held carts: ' + err.message, 'error');
|
|
}
|
|
},
|
|
async resume(id) {
|
|
try {
|
|
const resp = await fetch('index.php?action=get_held_carts');
|
|
const carts = await resp.json();
|
|
const c = carts.find(x => x.id == id);
|
|
if (c) {
|
|
this.items = JSON.parse(c.items_json);
|
|
document.getElementById('posCustomer').value = c.customer_id || '';
|
|
await this.onCustomerChange();
|
|
await this.deleteHeld(id, true);
|
|
Swal.close();
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
Swal.fire('Error', 'Failed to resume cart', 'error');
|
|
}
|
|
},
|
|
async deleteHeld(id, silent = false) {
|
|
const formData = new FormData();
|
|
formData.append('action', 'delete_held_cart');
|
|
formData.append('id', id);
|
|
await fetch('index.php', { method: 'POST', body: formData });
|
|
if (!silent) this.openHeldCartsModal();
|
|
},
|
|
render() {
|
|
const container = document.getElementById('cartItems');
|
|
const lang = document.documentElement.lang || 'en';
|
|
if (this.items.length === 0) {
|
|
container.innerHTML = `<div class="text-center text-muted mt-5"><i class="bi bi-cart-x" style="font-size: 3rem;"></i><p data-en="Cart is empty" data-ar="السلة فارغة">${lang === 'ar' ? 'السلة فارغة' : 'Cart is empty'}</p></div>`;
|
|
document.getElementById('posSubtotal').innerText = 'OMR 0.000';
|
|
document.getElementById('posTotal').innerText = 'OMR 0.000';
|
|
document.getElementById('checkoutBtn').disabled = true;
|
|
return;
|
|
}
|
|
|
|
let subtotal = 0;
|
|
container.innerHTML = this.items.map(item => {
|
|
subtotal += item.price * item.qty;
|
|
const displayName = lang === 'ar' ? item.nameAr : item.nameEn;
|
|
return `
|
|
<div class="cart-item">
|
|
<div class="flex-grow-1">
|
|
<div class="fw-bold small">${displayName}</div>
|
|
<div class="text-muted smaller">OMR ${parseFloat(item.price).toFixed(3)}</div>
|
|
</div>
|
|
<div class="qty-controls mx-3">
|
|
<button class="qty-btn" onclick="cart.updateQty(${item.id}, -1)">-</button>
|
|
<span class="small fw-bold">${item.qty}</span>
|
|
<button class="qty-btn" onclick="cart.updateQty(${item.id}, 1)">+</button>
|
|
</div>
|
|
<div class="fw-bold small">OMR ${(item.price * item.qty).toFixed(3)}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
let discountAmount = 0;
|
|
if (this.discount) {
|
|
if (this.discount.type === 'percentage') {
|
|
discountAmount = subtotal * (parseFloat(this.discount.value) / 100);
|
|
} else {
|
|
discountAmount = parseFloat(this.discount.value);
|
|
}
|
|
}
|
|
|
|
let loyaltyRedeemed = 0;
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
if (redeemSwitch && redeemSwitch.checked) {
|
|
loyaltyRedeemed = Math.min(subtotal - discountAmount, this.customerPoints);
|
|
}
|
|
|
|
const total = subtotal - discountAmount - loyaltyRedeemed;
|
|
|
|
document.getElementById('posSubtotal').innerText = 'OMR ' + subtotal.toFixed(3);
|
|
|
|
let totalHtml = '';
|
|
if (discountAmount > 0) totalHtml += `<div class="smaller text-danger">- Disc: OMR ${discountAmount.toFixed(3)}</div>`;
|
|
if (loyaltyRedeemed > 0) totalHtml += `<div class="smaller text-success">- Loyalty: OMR ${loyaltyRedeemed.toFixed(3)}</div>`;
|
|
totalHtml += 'OMR ' + total.toFixed(3);
|
|
|
|
document.getElementById('posTotal').innerHTML = totalHtml;
|
|
document.getElementById('checkoutBtn').disabled = false;
|
|
},
|
|
async checkout() {
|
|
if (this.items.length === 0) return;
|
|
|
|
const customerSelect = document.getElementById('posCustomer');
|
|
const customerName = customerSelect.options[customerSelect.selectedIndex].text;
|
|
document.getElementById('paymentCustomerName').innerText = customerName;
|
|
|
|
const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
|
|
let discountAmount = 0;
|
|
if (this.discount) {
|
|
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
|
}
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints) : 0;
|
|
const total = subtotal - discountAmount - loyaltyRedeemed;
|
|
|
|
this.payments = [];
|
|
this.renderPayments();
|
|
document.getElementById('paymentAmountDue').innerText = 'OMR ' + total.toFixed(3);
|
|
document.getElementById('partialAmount').value = total.toFixed(3);
|
|
|
|
// Sync credit customer selection if credit is default or already selected
|
|
const creditSection = document.getElementById('creditCustomerSection');
|
|
if (this.selectedPaymentMethod === 'credit') {
|
|
creditSection.style.display = 'block';
|
|
const creditSelect = $('#paymentCreditCustomer');
|
|
creditSelect.val(customerSelect.value).trigger('change');
|
|
} else {
|
|
creditSection.style.display = 'none';
|
|
}
|
|
|
|
this.updateRemaining();
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('posPaymentModal'));
|
|
modal.show();
|
|
},
|
|
selectMethod(method, btn) {
|
|
this.selectedPaymentMethod = method;
|
|
document.querySelectorAll('.payment-method-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
|
|
const creditSection = document.getElementById('creditCustomerSection');
|
|
if (method === 'credit') {
|
|
creditSection.style.display = 'block';
|
|
// Sync with main customer select
|
|
const creditSelect = $('#paymentCreditCustomer');
|
|
creditSelect.val(document.getElementById('posCustomer').value).trigger('change');
|
|
} else {
|
|
creditSection.style.display = 'none';
|
|
}
|
|
|
|
this.updateRemaining();
|
|
},
|
|
fillPartial(amount) {
|
|
const input = document.getElementById('partialAmount');
|
|
input.value = parseFloat(amount).toFixed(3);
|
|
this.updateRemaining();
|
|
},
|
|
addPaymentLine() {
|
|
const amount = parseFloat(document.getElementById('partialAmount').value) || 0;
|
|
if (amount <= 0) return;
|
|
|
|
this.payments.push({
|
|
method: this.selectedPaymentMethod,
|
|
amount: amount
|
|
});
|
|
this.renderPayments();
|
|
this.updateRemaining();
|
|
|
|
// Auto-fill remaining for next line if any
|
|
const remaining = this.getRemaining();
|
|
document.getElementById('partialAmount').value = remaining > 0 ? remaining.toFixed(3) : '0.000';
|
|
},
|
|
removePaymentLine(index) {
|
|
this.payments.splice(index, 1);
|
|
this.renderPayments();
|
|
this.updateRemaining();
|
|
},
|
|
getGrandTotal() {
|
|
const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
|
|
let discountAmount = 0;
|
|
if (this.discount) {
|
|
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
|
}
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints) : 0;
|
|
return subtotal - discountAmount - loyaltyRedeemed;
|
|
},
|
|
getRemaining() {
|
|
const total = this.getGrandTotal();
|
|
const paid = this.payments.reduce((sum, p) => sum + p.amount, 0);
|
|
return total - paid;
|
|
},
|
|
renderPayments() {
|
|
const container = document.getElementById('paymentList');
|
|
container.innerHTML = this.payments.map((p, i) => `
|
|
<div class="payment-line">
|
|
<div>
|
|
<span class="method">${p.method}</span>
|
|
<span class="ms-2 badge bg-secondary small">OMR ${p.amount.toFixed(3)}</span>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-danger border-0" onclick="cart.removePaymentLine(${i})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
},
|
|
updateRemaining() {
|
|
const remaining = this.getRemaining();
|
|
const currentInput = parseFloat(document.getElementById('partialAmount').value) || 0;
|
|
const display = document.getElementById('paymentRemaining');
|
|
display.innerText = 'OMR ' + Math.max(0, remaining).toFixed(3);
|
|
|
|
// Calculate potential change if the user types an amount > remaining
|
|
const totalPaid = this.payments.reduce((sum, p) => sum + p.amount, 0);
|
|
const grandTotal = this.getGrandTotal();
|
|
const actualChange = Math.max(0, totalPaid - grandTotal);
|
|
const potentialChange = Math.max(0, currentInput - remaining);
|
|
const displayChange = Math.max(actualChange, potentialChange);
|
|
|
|
const changeDisplay = document.getElementById('changeDue');
|
|
if (changeDisplay) {
|
|
changeDisplay.innerText = 'OMR ' + displayChange.toFixed(3);
|
|
const cashSection = document.getElementById('cashPaymentSection');
|
|
if (displayChange > 0 || this.selectedPaymentMethod === 'cash' || this.payments.some(p => p.method === 'cash')) {
|
|
cashSection.style.display = 'block';
|
|
} else {
|
|
cashSection.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
if (remaining <= 0.0001 || currentInput >= remaining - 0.0001) {
|
|
display.classList.remove('text-danger');
|
|
display.classList.add('text-success');
|
|
document.getElementById('confirmPaymentBtn').disabled = false;
|
|
} else {
|
|
display.classList.remove('text-success');
|
|
display.classList.add('text-danger');
|
|
document.getElementById('confirmPaymentBtn').disabled = true;
|
|
}
|
|
},
|
|
async completeOrder() {
|
|
if (this.items.length === 0) {
|
|
Swal.fire('Error', 'Cart is empty', 'error');
|
|
return;
|
|
}
|
|
|
|
// If there's an amount in the input and payments are not enough, add it
|
|
const remainingBefore = this.getRemaining();
|
|
const currentInput = parseFloat(document.getElementById('partialAmount').value) || 0;
|
|
|
|
if (remainingBefore > 0.0001 && currentInput >= remainingBefore - 0.0001) {
|
|
this.payments.push({
|
|
method: this.selectedPaymentMethod,
|
|
amount: currentInput
|
|
});
|
|
} else if (this.payments.length === 0) {
|
|
const total = this.getGrandTotal();
|
|
this.payments.push({
|
|
method: this.selectedPaymentMethod,
|
|
amount: total
|
|
});
|
|
}
|
|
|
|
const remaining = this.getRemaining();
|
|
if (remaining > 0.001) {
|
|
Swal.fire('Error', 'Payment is incomplete', 'error');
|
|
return;
|
|
}
|
|
|
|
const customerId = document.getElementById('posCustomer').value;
|
|
if (this.payments.some(p => p.method === 'credit') && !customerId) {
|
|
Swal.fire('Error', 'Credit payment is only allowed for registered customers', 'error');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('confirmPaymentBtn');
|
|
const originalText = btn.innerText;
|
|
btn.disabled = true;
|
|
btn.innerText = 'PROCESSING...';
|
|
|
|
const subtotal = this.items.reduce((sum, item) => sum + (parseFloat(item.price) * item.qty), 0);
|
|
let discountAmount = 0;
|
|
if (this.discount) {
|
|
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
|
}
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints) : 0;
|
|
|
|
const formData = new FormData();
|
|
formData.append('action', 'save_pos_transaction');
|
|
formData.append('customer_id', customerId);
|
|
formData.append('payments', JSON.stringify(this.payments));
|
|
formData.append('total_amount', subtotal);
|
|
formData.append('discount_code_id', this.discount ? this.discount.id : '');
|
|
formData.append('discount_amount', discountAmount);
|
|
formData.append('loyalty_redeemed', loyaltyRedeemed);
|
|
formData.append('items', JSON.stringify(this.items.map(i => ({id: i.id, qty: i.qty, price: i.price}))));
|
|
|
|
try {
|
|
const resp = await fetch('index.php', { method: 'POST', body: formData });
|
|
const text = await resp.text();
|
|
let result;
|
|
try {
|
|
result = JSON.parse(text);
|
|
} catch (e) {
|
|
console.error('Invalid JSON response:', text);
|
|
throw new Error('Server returned an invalid response');
|
|
}
|
|
|
|
if (result.success) {
|
|
const payModal = bootstrap.Modal.getInstance(document.getElementById('posPaymentModal'));
|
|
if (payModal) payModal.hide();
|
|
this.showReceipt(result.invoice_id, discountAmount, loyaltyRedeemed, result.transaction_no);
|
|
} else {
|
|
Swal.fire('Error', result.error, 'error');
|
|
btn.disabled = false;
|
|
btn.innerText = originalText;
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
Swal.fire('Error', err.message || 'Something went wrong', 'error');
|
|
btn.disabled = false;
|
|
btn.innerText = originalText;
|
|
}
|
|
},
|
|
showReceipt(invId, discountAmount, loyaltyRedeemed, transactionNo) {
|
|
const container = document.getElementById('posReceiptContent');
|
|
const customerName = document.getElementById('posCustomer').options[document.getElementById('posCustomer').selectedIndex].text;
|
|
const paymentsHtml = this.payments.map(p => `
|
|
<div class="d-flex justify-content-between small">
|
|
<span class="text-uppercase">${p.method}</span>
|
|
<span>OMR ${p.amount.toFixed(3)}</span>
|
|
</div>
|
|
`).join('');
|
|
const date = new Date().toLocaleString();
|
|
|
|
let itemsHtml = this.items.map(item => `
|
|
<tr>
|
|
<td>${item.nameEn}<br><small>${item.qty} x ${parseFloat(item.price).toFixed(3)}</small></td>
|
|
<td style="text-align: right; vertical-align: bottom;">${(item.price * item.qty).toFixed(3)}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
|
|
const total = subtotal - discountAmount - loyaltyRedeemed;
|
|
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>";
|
|
const companyPhone = "<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>";
|
|
const companyVat = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>";
|
|
|
|
container.innerHTML = `
|
|
<div class="thermal-receipt">
|
|
<div class="center">
|
|
<h5 class="mb-0 fw-bold">${companyName}</h5>
|
|
${companyPhone ? `<div>Tel: ${companyPhone}</div>` : ''}
|
|
${companyVat ? `<div>VAT: ${companyVat}</div>` : ''}
|
|
<div class="separator"></div>
|
|
<h6 class="fw-bold">TAX INVOICE</h6>
|
|
<div>Inv: ${transactionNo || 'POS-'+invId}</div>
|
|
<div>Date: ${date}</div>
|
|
<div class="separator"></div>
|
|
</div>
|
|
<div>
|
|
<strong>Customer:</strong> ${customerName}
|
|
</div>
|
|
<div class="mt-1">
|
|
<strong>Payments:</strong>
|
|
${paymentsHtml}
|
|
</div>
|
|
<div class="separator"></div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ITEM</th>
|
|
<th style="text-align: right;">TOTAL</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${itemsHtml}
|
|
</tbody>
|
|
</table>
|
|
<div class="separator"></div>
|
|
<div class="d-flex justify-content-between">
|
|
<span>Subtotal</span>
|
|
<span>OMR ${subtotal.toFixed(3)}</span>
|
|
</div>
|
|
${discountAmount > 0 ? `<div class="d-flex justify-content-between text-danger"><span>Discount</span><span>- OMR ${parseFloat(discountAmount).toFixed(3)}</span></div>` : ''}
|
|
${loyaltyRedeemed > 0 ? `<div class="d-flex justify-content-between text-success"><span>Loyalty</span><span>- OMR ${parseFloat(loyaltyRedeemed).toFixed(3)}</span></div>` : ''}
|
|
<div class="separator"></div>
|
|
<div class="d-flex justify-content-between total-row">
|
|
<span>TOTAL</span>
|
|
<span>OMR ${total.toFixed(3)}</span>
|
|
</div>
|
|
<div class="separator"></div>
|
|
<div class="center small">
|
|
Thank you for your business!<br>
|
|
Please keep the receipt.
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('posReceiptModal'));
|
|
modal.show();
|
|
|
|
this.clear();
|
|
|
|
document.getElementById('posReceiptModal').addEventListener('hidden.bs.modal', function () {
|
|
location.reload();
|
|
}, { once: true });
|
|
},
|
|
|
|
async syncCustomer(val) {
|
|
document.getElementById('posCustomer').value = val;
|
|
const customerSelect = document.getElementById('posCustomer');
|
|
const customerName = customerSelect.options[customerSelect.selectedIndex].text;
|
|
document.getElementById('paymentCustomerName').innerText = customerName;
|
|
await this.onCustomerChange();
|
|
}
|
|
};
|
|
|
|
document.querySelectorAll('.product-card').forEach(card => {
|
|
card.addEventListener('click', () => {
|
|
const product = {
|
|
id: parseInt(card.dataset.id),
|
|
nameEn: card.dataset.nameEn,
|
|
nameAr: card.dataset.nameAr,
|
|
price: parseFloat(card.dataset.price)
|
|
};
|
|
cart.add(product);
|
|
});
|
|
});
|
|
|
|
document.getElementById('productSearch').addEventListener('input', (e) => {
|
|
const q = e.target.value.toLowerCase();
|
|
document.querySelectorAll('.product-grid .product-card').forEach(card => {
|
|
const name = card.dataset.nameEn.toLowerCase() + ' ' + card.dataset.nameAr.toLowerCase();
|
|
const sku = card.dataset.sku.toLowerCase();
|
|
if (name.includes(q) || sku.includes(q)) {
|
|
card.style.display = 'flex';
|
|
} else {
|
|
card.style.display = 'none';
|
|
}
|
|
});
|
|
});
|
|
|
|
document.getElementById('barcodeInput').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
const barcode = e.target.value.trim();
|
|
if (!barcode) return;
|
|
|
|
const card = Array.from(document.querySelectorAll('.product-card')).find(c => c.dataset.sku === barcode);
|
|
if (card) {
|
|
const product = {
|
|
id: parseInt(card.dataset.id),
|
|
nameEn: card.dataset.nameEn,
|
|
nameAr: card.dataset.nameAr,
|
|
price: parseFloat(card.dataset.price)
|
|
};
|
|
cart.add(product);
|
|
e.target.value = '';
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'top-end',
|
|
icon: 'success',
|
|
title: 'Added: ' + product.nameEn,
|
|
showConfirmButton: false,
|
|
timer: 1000
|
|
});
|
|
} else {
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'top-end',
|
|
icon: 'error',
|
|
title: 'Product not found',
|
|
showConfirmButton: false,
|
|
timer: 1500
|
|
});
|
|
e.target.select();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Keep barcode input focused
|
|
document.addEventListener('click', () => {
|
|
if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT' && document.activeElement.tagName !== 'TEXTAREA') {
|
|
const bc = document.getElementById('barcodeInput');
|
|
if (bc) bc.focus();
|
|
}
|
|
});
|
|
|
|
$(document).ready(function() {
|
|
$('#posCustomer').select2({
|
|
width: '100%',
|
|
placeholder: 'Select Customer'
|
|
});
|
|
$('#paymentCreditCustomer').select2({
|
|
width: '100%',
|
|
placeholder: 'Select Customer',
|
|
dropdownParent: $('#posPaymentModal')
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<?php elseif ($page === 'quotations'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Quotations" data-ar="عروض الأسعار">Quotations</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addQuotationModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Create New Quotation" data-ar="إنشاء عرض سعر جديد">Create New Quotation</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filters Section -->
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="row g-3">
|
|
<input type="hidden" name="page" value="quotations">
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Search" data-ar="بحث">Search</label>
|
|
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Quot # or Name...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Customer" data-ar="العميل">Customer</label>
|
|
<select name="customer_id" class="form-select form-select-sm">
|
|
<option value="" data-en="All" data-ar="الكل">All</option>
|
|
<?php foreach ($data['customers_list'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>" <?= (($_GET['customer_id'] ?? '') == $c['id']) ? 'selected' : '' ?>><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold" data-en="Start Date" data-ar="من تاريخ">Start Date</label>
|
|
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['start_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold" data-en="End Date" data-ar="إلى تاريخ">End Date</label>
|
|
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['end_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end gap-1">
|
|
<button type="submit" class="btn btn-primary btn-sm flex-grow-1">
|
|
<i class="bi bi-filter"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
|
</button>
|
|
<a href="index.php?page=quotations" class="btn btn-outline-secondary btn-sm flex-grow-1">
|
|
<i class="bi bi-x-circle"></i> <span data-en="Clear" data-ar="مسح">Clear</span>
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Quotation #" data-ar="رقم العرض">Quotation #</th>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Valid Until" data-ar="صالح حتى">Valid Until</th>
|
|
<th data-en="Customer" data-ar="العميل">Customer</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Total" data-ar="الإجمالي" class="text-end">Total</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
foreach ($data['quotations'] as $q):
|
|
$items = db()->prepare("SELECT qi.*, i.name_en, i.name_ar, i.vat_rate
|
|
FROM quotation_items qi
|
|
JOIN stock_items i ON qi.item_id = i.id
|
|
WHERE qi.quotation_id = ?");
|
|
$items->execute([$q['id']]);
|
|
$q['items'] = $items->fetchAll(PDO::FETCH_ASSOC);
|
|
?>
|
|
<tr>
|
|
<td>QUO-<?= str_pad((string)$q['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= $q['quotation_date'] ?></td>
|
|
<td><?= $q['valid_until'] ?: '---' ?></td>
|
|
<td><?= htmlspecialchars($q['customer_name'] ?? '---') ?></td>
|
|
<td>
|
|
<?php
|
|
$statusClass = 'bg-secondary';
|
|
if ($q['status'] === 'converted') $statusClass = 'bg-success';
|
|
elseif ($q['status'] === 'pending') $statusClass = 'bg-warning text-dark';
|
|
elseif ($q['status'] === 'expired' || $q['status'] === 'cancelled') $statusClass = 'bg-danger';
|
|
?>
|
|
<span class="badge text-uppercase <?= $statusClass ?>"><?= htmlspecialchars($q['status']) ?></span>
|
|
</td>
|
|
<td class="text-end fw-bold">OMR <?= number_format((float)$q['total_with_vat'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info view-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" title="View"><i class="bi bi-eye"></i></button>
|
|
<button class="btn btn-outline-secondary" onclick="window.viewAndPrintQuotation(<?= htmlspecialchars(json_encode($q)) ?>)" title="Print"><i class="bi bi-printer"></i></button>
|
|
<button class="btn btn-outline-primary edit-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" data-bs-toggle="modal" data-bs-target="#editQuotationModal" title="Edit"><i class="bi bi-pencil-square"></i></button>
|
|
<?php if ($q['status'] === 'pending'): ?>
|
|
<button class="btn btn-outline-success convert-quotation-btn" data-id="<?= $q['id'] ?>" title="Convert to Invoice"><i class="bi bi-receipt"></i></button>
|
|
<?php endif; ?>
|
|
<button class="btn btn-outline-danger" onclick="if(confirm('Delete this quotation?')) { const f = document.createElement('form'); f.method='POST'; f.innerHTML='<input type=hidden name=delete_quotation><input type=hidden name=id value=<?= $q['id'] ?>>'; document.body.appendChild(f); f.submit(); }" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($data['quotations'])): ?>
|
|
<tr><td colspan="7" class="text-center py-4 text-muted" data-en="No quotations found" data-ar="لا توجد عروض أسعار">No quotations found</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'sales' || $page === 'purchases'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="<?= $currTitle['en'] ?>" data-ar="<?= $currTitle['ar'] ?>"><?= $currTitle['en'] ?></h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addInvoiceModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Create New Tax Invoice" data-ar="إنشاء فاتورة ضريبية جديدة">Create New Tax Invoice</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filters Section -->
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="row g-3">
|
|
<input type="hidden" name="page" value="<?= $page ?>">
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Search" data-ar="بحث">Search</label>
|
|
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Inv # or Name...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="<?= $page === 'sales' ? 'Customer' : 'Supplier' ?>" data-ar="<?= $page === 'sales' ? 'العميل' : 'المورد' ?>"><?= $page === 'sales' ? 'Customer' : 'Supplier' ?></label>
|
|
<select name="customer_id" class="form-select form-select-sm">
|
|
<option value="" data-en="All" data-ar="الكل">All</option>
|
|
<?php foreach ($data['customers_list'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>" <?= (($_GET['customer_id'] ?? '') == $c['id']) ? 'selected' : '' ?>><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold" data-en="Start Date" data-ar="من تاريخ">Start Date</label>
|
|
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['start_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold" data-en="End Date" data-ar="إلى تاريخ">End Date</label>
|
|
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['end_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end gap-1">
|
|
<button type="submit" class="btn btn-primary btn-sm flex-grow-1">
|
|
<i class="bi bi-filter"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
|
</button>
|
|
<a href="index.php?page=export&type=<?= $page ?>&<?= http_build_query($_GET) ?>" class="btn btn-success btn-sm">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</a>
|
|
<a href="index.php?page=<?= $page ?>" class="btn btn-outline-secondary btn-sm flex-grow-1">
|
|
<i class="bi bi-x-circle"></i> <span data-en="Clear" data-ar="مسح">Clear</span>
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Invoice #" data-ar="رقم الفاتورة">Invoice #</th>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="<?= $page === 'sales' ? 'Customer' : 'Supplier' ?>" data-ar="<?= $page === 'sales' ? 'العميل' : 'المورد' ?>"><?= $page === 'sales' ? 'Customer' : 'Supplier' ?></th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Total" data-ar="الإجمالي" class="text-end">Total</th>
|
|
<th data-en="Paid" data-ar="المدفوع" class="text-end">Paid</th>
|
|
<th data-en="Balance" data-ar="المتبقي" class="text-end">Balance</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
foreach ($data['invoices'] as $inv):
|
|
$items = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.vat_rate
|
|
FROM invoice_items ii
|
|
JOIN stock_items i ON ii.item_id = i.id
|
|
WHERE ii.invoice_id = ?");
|
|
$items->execute([$inv['id']]);
|
|
$inv['items'] = $items->fetchAll(PDO::FETCH_ASSOC);
|
|
?>
|
|
<tr>
|
|
<td>INV-<?= str_pad((string)$inv['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= $inv['invoice_date'] ?></td>
|
|
<td><?= htmlspecialchars($inv['customer_name'] ?? '---') ?></td>
|
|
<td>
|
|
<?php
|
|
$statusClass = 'bg-secondary';
|
|
if ($inv['status'] === 'paid') $statusClass = 'bg-success';
|
|
elseif ($inv['status'] === 'unpaid') $statusClass = 'bg-danger';
|
|
elseif ($inv['status'] === 'partially_paid') $statusClass = 'bg-warning text-dark';
|
|
?>
|
|
<span class="badge text-uppercase <?= $statusClass ?>"><?= htmlspecialchars(str_replace('_', ' ', $inv['status'])) ?></span>
|
|
</td>
|
|
<td class="text-end fw-bold">OMR <?= number_format((float)$inv['total_with_vat'], 3) ?></td>
|
|
<td class="text-end text-success">OMR <?= number_format((float)$inv['paid_amount'], 3) ?></td>
|
|
<td class="text-end text-danger fw-bold">OMR <?= number_format((float)($inv['total_with_vat'] - $inv['paid_amount']), 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info view-invoice-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" title="View"><i class="bi bi-eye"></i></button>
|
|
<button class="btn btn-outline-primary edit-invoice-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" data-bs-toggle="modal" data-bs-target="#editInvoiceModal" title="Edit"><i class="bi bi-pencil"></i></button>
|
|
<?php if ($inv['status'] !== 'paid'): ?>
|
|
<button class="btn btn-outline-success pay-invoice-btn" data-id="<?= $inv['id'] ?>" data-total="<?= $inv['total_with_vat'] ?>" data-paid="<?= $inv['paid_amount'] ?>" data-bs-toggle="modal" data-bs-target="#payInvoiceModal" title="Payment"><i class="bi bi-cash-coin"></i></button>
|
|
<?php endif; ?>
|
|
<button class="btn btn-outline-secondary print-a4-btn" data-json="<?= htmlspecialchars(json_encode($inv)) ?>" title="Print A4 Invoice"><i class="bi bi-printer"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this invoice?')">
|
|
<input type="hidden" name="id" value="<?= $inv['id'] ?>">
|
|
<button type="submit" name="delete_invoice" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'customer_statement' || $page === 'supplier_statement'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="<?= $currTitle['en'] ?>" data-ar="<?= $currTitle['ar'] ?>"><?= $currTitle['en'] ?></h5>
|
|
<button class="btn btn-outline-secondary d-print-none" onclick="window.print()">
|
|
<i class="bi bi-printer"></i> <span data-en="Print" data-ar="طباعة">Print</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-light p-3 rounded mb-4 d-print-none">
|
|
<form method="GET" class="row g-3 align-items-end">
|
|
<input type="hidden" name="page" value="<?= $page ?>">
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-bold" data-en="Select <?= $page === 'customer_statement' ? 'Customer' : 'Supplier' ?>" data-ar="اختر <?= $page === 'customer_statement' ? 'العميل' : 'المورد' ?>">Select <?= $page === 'customer_statement' ? 'Customer' : 'Supplier' ?></label>
|
|
<select name="entity_id" class="form-select" required>
|
|
<option value="">---</option>
|
|
<?php foreach ($data['entities'] as $e): ?>
|
|
<option value="<?= $e['id'] ?>" <?= (($_GET['entity_id'] ?? '') == $e['id']) ? 'selected' : '' ?>><?= htmlspecialchars($e['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="From Date" data-ar="من تاريخ">From Date</label>
|
|
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="To Date" data-ar="إلى تاريخ">To Date</label>
|
|
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-search"></i> <span data-en="View Report" data-ar="عرض التقرير">View Report</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if (isset($data['transactions'])): ?>
|
|
<div id="statement-print">
|
|
<div class="row mb-4">
|
|
<div class="col-6">
|
|
<h3 class="mb-0"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
|
<p class="text-muted small"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h2 class="text-uppercase text-muted" data-en="Statement of Account" data-ar="كشف حساب">Statement of Account</h2>
|
|
<p class="mb-0"><strong><?= htmlspecialchars($data['selected_entity']['name']) ?></strong></p>
|
|
<p class="text-muted small"><?= htmlspecialchars($data['selected_entity']['email']) ?> | <?= htmlspecialchars($data['selected_entity']['phone']) ?><br>Period: <?= $_GET['start_date'] ?> to <?= $_GET['end_date'] ?></p>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered table-sm">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Reference" data-ar="المرجع">Reference</th>
|
|
<th data-en="Description" data-ar="الوصف">Description</th>
|
|
<th data-en="Debit" data-ar="مدين" class="text-end">Debit</th>
|
|
<th data-en="Credit" data-ar="دائن" class="text-end">Credit</th>
|
|
<th data-en="Balance" data-ar="الرصيد" class="text-end">Balance</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$running_balance = 0;
|
|
foreach ($data['transactions'] as $t):
|
|
$debit = 0; $credit = 0;
|
|
if ($t['trans_type'] === 'invoice') {
|
|
if ($page === 'customer_statement') $debit = (float)$t['amount']; else $credit = (float)$t['amount'];
|
|
} else {
|
|
if ($page === 'customer_statement') $credit = (float)$t['amount']; else $debit = (float)$t['amount'];
|
|
}
|
|
$running_balance += ($debit - $credit);
|
|
?>
|
|
<tr>
|
|
<td><?= $t['trans_date'] ?></td>
|
|
<td><?= $t['trans_type'] === 'invoice' ? 'INV-'.str_pad((string)$t['ref_no'], 5, '0', STR_PAD_LEFT) : 'RCP-'.str_pad((string)$t['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= $t['trans_type'] === 'invoice' ? 'Tax Invoice' : 'Payment - '.$t['payment_method'] ?></td>
|
|
<td class="text-end"><?= $debit > 0 ? number_format($debit, 3) : '' ?></td>
|
|
<td class="text-end"><?= $credit > 0 ? number_format($credit, 3) : '' ?></td>
|
|
<td class="text-end fw-bold"><?= number_format($running_balance, 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
<tfoot class="bg-light fw-bold">
|
|
<tr>
|
|
<td colspan="5" class="text-end">Closing Balance</td>
|
|
<td class="text-end">OMR <?= number_format($running_balance, 3) ?></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="text-center py-5 text-muted"><p>Please select an entity and date range to generate the statement.</p></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'payment_methods'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Payment Methods" data-ar="طرق الدفع">Payment Methods</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPaymentMethodModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Payment Method" data-ar="إضافة طريقة دفع">Add Payment Method</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 (EN)" data-ar="الاسم (EN)">Name (EN)</th>
|
|
<th data-en="Name (AR)" data-ar="الاسم (AR)">Name (AR)</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['payment_methods'] as $pm): ?>
|
|
<tr>
|
|
<td><?= $pm['id'] ?></td>
|
|
<td><?= htmlspecialchars($pm['name_en'] ?? '') ?></td>
|
|
<td><?= htmlspecialchars($pm['name_ar'] ?? '') ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" title="Edit" data-bs-toggle="modal" data-bs-target="#editPaymentMethodModal<?= $pm['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $pm['id'] ?>">
|
|
<button type="submit" name="delete_payment_method" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Payment Method Modal -->
|
|
<div class="modal fade" id="addPaymentMethodModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add Payment Method" data-ar="إضافة طريقة دفع">Add Payment Method</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="Name (EN)" data-ar="الاسم (EN)">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name (AR)" data-ar="الاسم (AR)">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_payment_method" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($page === 'expense_categories'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Expense Categories" data-ar="فئات المصروفات">Expense Categories</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addExpenseCategoryModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Category" data-ar="إضافة فئة">Add Category</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 (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</th>
|
|
<th data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['expense_categories'] as $cat): ?>
|
|
<tr>
|
|
<td><?= $cat['id'] ?></td>
|
|
<td><?= htmlspecialchars($cat['name_en']) ?></td>
|
|
<td><?= htmlspecialchars($cat['name_ar']) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editExpCatModal<?= $cat['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
<button type="submit" name="delete_expense_category" class="btn btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Edit Modal -->
|
|
<div class="modal fade" id="editExpCatModal<?= $cat['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit Category</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" value="<?= htmlspecialchars($cat['name_en']) ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" value="<?= htmlspecialchars($cat['name_ar']) ?>" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" name="edit_expense_category" class="btn btn-primary">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Modal -->
|
|
<div class="modal fade" id="addExpenseCategoryModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Add Category</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">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" 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_expense_category" class="btn btn-primary">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'expenses'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Expenses List" data-ar="قائمة المصروفات">Expenses List</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addExpenseModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Expense" data-ar="إضافة مصروف">Add Expense</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="row g-3">
|
|
<input type="hidden" name="page" value="expenses">
|
|
<div class="col-md-3">
|
|
<label class="form-label small">Category</label>
|
|
<select name="category_id" class="form-select">
|
|
<option value="">All</option>
|
|
<?php foreach ($data['expense_categories'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>" <?= ($_GET['category_id'] ?? '') == $c['id'] ? 'selected' : '' ?>><?= htmlspecialchars($c['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small">Start Date</label>
|
|
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($_GET['start_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small">End Date</label>
|
|
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($_GET['end_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-3 d-flex align-items-end">
|
|
<button type="submit" class="btn btn-primary w-100">Filter</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Reference</th>
|
|
<th>Category</th>
|
|
<th>Description</th>
|
|
<th class="text-end">Amount</th>
|
|
<th class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['expenses'] as $exp): ?>
|
|
<tr>
|
|
<td><?= $exp['expense_date'] ?></td>
|
|
<td><?= htmlspecialchars($exp['reference_no'] ?: '---') ?></td>
|
|
<td><?= htmlspecialchars($exp['cat_en'] ?? 'Unknown') ?></td>
|
|
<td><?= htmlspecialchars($exp['description']) ?></td>
|
|
<td class="text-end fw-bold">OMR <?= number_format((float)$exp['amount'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editExpenseModal<?= $exp['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $exp['id'] ?>">
|
|
<button type="submit" name="delete_expense" class="btn btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Edit Modal -->
|
|
<div class="modal fade" id="editExpenseModal<?= $exp['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit Expense</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $exp['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Category</label>
|
|
<select name="category_id" class="form-select" required>
|
|
<?php foreach ($data['expense_categories'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>" <?= $c['id'] == $exp['category_id'] ? 'selected' : '' ?>><?= htmlspecialchars($c['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Date</label>
|
|
<input type="date" name="expense_date" class="form-control" value="<?= $exp['expense_date'] ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Amount</label>
|
|
<input type="number" step="0.001" name="amount" class="form-control" value="<?= (float)$exp['amount'] ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Reference No</label>
|
|
<input type="text" name="reference_no" class="form-control" value="<?= htmlspecialchars($exp['reference_no']) ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<textarea name="description" class="form-control"><?= htmlspecialchars($exp['description']) ?></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" name="edit_expense" class="btn btn-primary">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Expense Modal -->
|
|
<div class="modal fade" id="addExpenseModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Add Expense</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">Category</label>
|
|
<select name="category_id" class="form-select" required>
|
|
<option value="">Select Category</option>
|
|
<?php foreach ($data['expense_categories'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Date</label>
|
|
<input type="date" name="expense_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Amount</label>
|
|
<input type="number" step="0.001" name="amount" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Reference No</label>
|
|
<input type="text" name="reference_no" class="form-control">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<textarea name="description" class="form-control"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" name="add_expense" class="btn btn-primary">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'expense_report'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Expense Report" data-ar="تقرير المصروفات">Expense Report</h5>
|
|
<button class="btn btn-outline-secondary d-print-none" onclick="window.print()">
|
|
<i class="bi bi-printer"></i> Print
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-light p-3 rounded mb-4 d-print-none">
|
|
<form method="GET" class="row g-3">
|
|
<input type="hidden" name="page" value="expense_report">
|
|
<div class="col-md-4">
|
|
<label class="form-label small">From Date</label>
|
|
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small">To Date</label>
|
|
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?>">
|
|
</div>
|
|
<div class="col-md-4 d-flex align-items-end">
|
|
<button type="submit" class="btn btn-primary w-100">Generate Report</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-md-12">
|
|
<div class="card bg-light border-0">
|
|
<div class="card-body text-center">
|
|
<h6 class="text-muted text-uppercase mb-2">Total Expenses</h6>
|
|
<h2 class="text-danger mb-0">OMR <?= number_format((float)$data['total_expenses'], 3) ?></h2>
|
|
<small class="text-muted">Period: <?= htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?> to <?= htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th>Category</th>
|
|
<th class="text-end">Total Amount</th>
|
|
<th class="text-end">% of Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['report_by_category'])): ?>
|
|
<tr><td colspan="3" class="text-center text-muted">No expenses found for this period.</td></tr>
|
|
<?php else: ?>
|
|
<?php foreach ($data['report_by_category'] as $row):
|
|
$percent = $data['total_expenses'] > 0 ? ($row['total'] / $data['total_expenses'] * 100) : 0;
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<div><?= htmlspecialchars($row['name_en']) ?></div>
|
|
<small class="text-muted"><?= htmlspecialchars($row['name_ar']) ?></small>
|
|
</td>
|
|
<td class="text-end fw-bold">OMR <?= number_format((float)$row['total'], 3) ?></td>
|
|
<td class="text-end"><?= number_format($percent, 1) ?>%</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Add Customer Modal -->
|
|
<div class="modal fade" id="addCustomerModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<?php if ($page === 'suppliers'): ?>
|
|
<span data-en="Add New Supplier" data-ar="إضافة مورد جديد">Add New Supplier</span>
|
|
<?php else: ?>
|
|
<span data-en="Add New Customer" data-ar="إضافة عميل جديد">Add New Customer</span>
|
|
<?php endif; ?>
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="type" value="<?= $page === 'suppliers' ? 'supplier' : 'customer' ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name" data-ar="الاسم">Name</label>
|
|
<input type="text" name="name" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Email" data-ar="البريد الإلكتروني">Email</label>
|
|
<input type="email" name="email" class="form-control">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="phone" class="form-control">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Tax ID / VAT No" data-ar="الرقم الضريبي">Tax ID / VAT No</label>
|
|
<input type="text" name="tax_id" class="form-control">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Initial Balance" data-ar="الرصيد الافتتاحي">Initial Balance</label>
|
|
<input type="number" step="0.001" name="balance" class="form-control" value="0.000">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_customer" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Item Modal -->
|
|
<div class="modal fade" id="addItemModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add New Item" data-ar="إضافة صنف جديد">Add New Item</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Category" data-ar="الفئة">Category</label>
|
|
<select name="category_id" class="form-select">
|
|
<option value="">---</option>
|
|
<?php foreach ($data['categories'] ?? [] as $c): ?>
|
|
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name_en']) ?> / <?= htmlspecialchars($c['name_ar']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Unit" data-ar="الوحدة">Unit</label>
|
|
<select name="unit_id" class="form-select">
|
|
<option value="">---</option>
|
|
<?php foreach ($data['units'] ?? [] as $u): ?>
|
|
<option value="<?= $u['id'] ?>"><?= htmlspecialchars($u['short_name_en']) ?> / <?= htmlspecialchars($u['short_name_ar']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Supplier" data-ar="المورد">Supplier</label>
|
|
<select name="supplier_id" class="form-select">
|
|
<option value="">---</option>
|
|
<?php foreach ($data['suppliers'] ?? [] as $s): ?>
|
|
<option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="SKU / Barcode" data-ar="الباركود">SKU / Barcode</label>
|
|
<div class="input-group">
|
|
<input type="text" name="sku" id="skuInput" class="form-control">
|
|
<button class="btn btn-outline-secondary" type="button" id="suggestSkuBtn" data-en="Suggest" data-ar="اقتراح">Suggest</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label>
|
|
<input type="number" step="0.001" name="sale_price" class="form-control" value="0.000">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label>
|
|
<input type="number" step="0.001" name="purchase_price" class="form-control" value="0.000">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Initial Stock" data-ar="المخزون الحالي">Initial Stock</label>
|
|
<input type="number" step="0.001" name="stock_quantity" class="form-control" value="0.000">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Min Stock Level" data-ar="الحد الأدنى للمخزون">Min Stock Level</label>
|
|
<input type="number" step="0.001" name="min_stock_level" class="form-control" value="0.000">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Item Picture" data-ar="صورة الصنف">Item Picture</label>
|
|
<input type="file" name="image" class="form-control" accept="image/*">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="VAT Rate (%)" data-ar="ضريبة القيمة المضافة (%)">VAT Rate (%)</label>
|
|
<input type="number" step="0.001" name="vat_rate" class="form-control" value="15.00">
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="form-check form-switch mt-4">
|
|
<input class="form-check-input" type="checkbox" id="hasExpiryToggle">
|
|
<label class="form-check-label" for="hasExpiryToggle" data-en="Has Expiry Date?" data-ar="هل له تاريخ انتهاء؟">Has Expiry Date?</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6" id="expiryDateContainer" style="display: none;">
|
|
<label class="form-label" data-en="Expiry Date" data-ar="تاريخ الانتهاء">Expiry Date</label>
|
|
<input type="date" name="expiry_date" class="form-control">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_item" class="btn btn-primary" data-en="Save Item" data-ar="حفظ الصنف">Save Item</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Items Modal -->
|
|
<div class="modal fade" id="importItemsModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Import Items from Excel (CSV)" data-ar="استيراد الأصناف من اكسل (CSV)">Import Items from Excel (CSV)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2">
|
|
<small data-en="Please upload a CSV file with the following columns: SKU, English Name, Arabic Name, Sale Price, Cost Price." data-ar="يرجى رفع ملف CSV بالأعمدة التالية: الباركود، الاسم الإنجليزي، الاسم العربي، سعر البيع، سعر التكلفة.">
|
|
Please upload a CSV file with the following columns: SKU, English Name, Arabic Name, Sale Price, Cost Price.
|
|
</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Choose CSV File" data-ar="اختر ملف CSV">Choose CSV File</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_items" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Customers Modal -->
|
|
<div class="modal fade" id="importCustomersModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Import Customers from Excel (CSV)" data-ar="استيراد العملاء من اكسل (CSV)">Import Customers from Excel (CSV)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2">
|
|
<small data-en="Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance." data-ar="يرجى رفع ملف CSV بالأعمدة التالية: الاسم، البريد الإلكتروني، الهاتف، الرقم الضريبي، الرصيد.">
|
|
Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance.
|
|
</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Choose CSV File" data-ar="اختر ملف CSV">Choose CSV File</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_customers" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Suppliers Modal -->
|
|
<div class="modal fade" id="importSuppliersModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Import Suppliers from Excel (CSV)" data-ar="استيراد الموردين من اكسل (CSV)">Import Suppliers from Excel (CSV)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2">
|
|
<small data-en="Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance." data-ar="يرجى رفع ملف CSV بالأعمدة التالية: الاسم، البريد الإلكتروني، الهاتف، الرقم الضريبي، الرصيد.">
|
|
Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance.
|
|
</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Choose CSV File" data-ar="اختر ملف CSV">Choose CSV File</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_suppliers" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Categories Modal -->
|
|
<div class="modal fade" id="importCategoriesModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Import Categories from Excel (CSV)" data-ar="استيراد الفئات من اكسل (CSV)">Import Categories from Excel (CSV)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2">
|
|
<small data-en="Please upload a CSV file with the following columns: Name (EN), Name (AR)." data-ar="يرجى رفع ملف CSV بالأعمدة التالية: الاسم (إنجليزي)، الاسم (عربي).">
|
|
Please upload a CSV file with the following columns: Name (EN), Name (AR).
|
|
</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Choose CSV File" data-ar="اختر ملف CSV">Choose CSV File</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_categories" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Units Modal -->
|
|
<div class="modal fade" id="importUnitsModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Import Units from Excel (CSV)" data-ar="استيراد الوحدات من اكسل (CSV)">Import Units from Excel (CSV)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2">
|
|
<small data-en="Please upload a CSV file with the following columns: Name (EN), Name (AR), Short (EN), Short (AR)." data-ar="يرجى رفع ملف CSV بالأعمدة التالية: الاسم (إنجليزي)، الاسم (عربي)، الاختصار (إنجليزي)، الاختصار (عربي).">
|
|
Please upload a CSV file with the following columns: Name (EN), Name (AR), Short (EN), Short (AR).
|
|
</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Choose CSV File" data-ar="اختر ملف CSV">Choose CSV File</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_units" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Category Modal -->
|
|
<div class="modal fade" id="addCategoryModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add Category" data-ar="إضافة فئة">Add Category</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="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_category" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Unit Modal -->
|
|
<div class="modal fade" id="addUnitModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add Unit" data-ar="إضافة وحدة">Add Unit</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Short Name (EN)" data-ar="الاختصار (إنجليزي)">Short (EN)</label>
|
|
<input type="text" name="short_en" class="form-control" required placeholder="e.g. Kg">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Short Name (AR)" data-ar="الاختصار (عربي)">Short (AR)</label>
|
|
<input type="text" name="short_ar" class="form-control" required placeholder="مثلاً: كجم">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_unit" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const hasExpiryToggle = document.getElementById('hasExpiryToggle');
|
|
const expiryDateContainer = document.getElementById('expiryDateContainer');
|
|
const suggestSkuBtn = document.getElementById('suggestSkuBtn');
|
|
const skuInput = document.getElementById('skuInput');
|
|
|
|
if (suggestSkuBtn && skuInput) {
|
|
suggestSkuBtn.addEventListener('click', function() {
|
|
const sku = Math.floor(100000000000 + Math.random() * 900000000000).toString();
|
|
skuInput.value = sku;
|
|
});
|
|
}
|
|
|
|
// Toggle Expiry Date visibility
|
|
if (hasExpiryToggle && expiryDateContainer) {
|
|
hasExpiryToggle.addEventListener('change', function() {
|
|
expiryDateContainer.style.display = this.checked ? 'block' : 'none';
|
|
if (!this.checked) {
|
|
expiryDateContainer.querySelector('input').value = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Status change logic for Paid Amount field
|
|
const togglePaidAmount = (statusId, containerId) => {
|
|
const statusEl = document.getElementById(statusId);
|
|
const containerEl = document.getElementById(containerId);
|
|
if (statusEl && containerEl) {
|
|
statusEl.addEventListener('change', function() {
|
|
if (this.value === 'partially_paid') {
|
|
containerEl.style.display = 'block';
|
|
} else {
|
|
containerEl.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
};
|
|
togglePaidAmount('add_status', 'addPaidAmountContainer');
|
|
togglePaidAmount('edit_status', 'editPaidAmountContainer');
|
|
|
|
// Pay Invoice Logic
|
|
document.querySelectorAll('.pay-invoice-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const id = this.getAttribute('data-id');
|
|
const total = parseFloat(this.getAttribute('data-total'));
|
|
const paid = parseFloat(this.getAttribute('data-paid') || 0);
|
|
const remaining = total - paid;
|
|
|
|
document.getElementById('pay_invoice_id').value = id;
|
|
document.getElementById('pay_invoice_total').value = `OMR ${total.toFixed(3)}`;
|
|
document.getElementById('pay_remaining_amount').value = `OMR ${remaining.toFixed(3)}`;
|
|
document.getElementById('pay_amount').value = remaining.toFixed(3);
|
|
document.getElementById('pay_amount').max = remaining.toFixed(3);
|
|
});
|
|
});
|
|
|
|
// Show receipt modal if needed
|
|
<?php if (isset($_SESSION['trigger_receipt_modal'])):
|
|
$rid = (int)$_SESSION['show_receipt_id'];
|
|
unset($_SESSION['trigger_receipt_modal']);
|
|
?>
|
|
showReceipt(<?= $rid ?>);
|
|
<?php endif; ?>
|
|
|
|
window.showReceipt = function(paymentId) {
|
|
fetch(`index.php?action=get_payment_details&payment_id=${paymentId}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (!data) return;
|
|
document.getElementById('receiptNo').textContent = 'RCP-' + data.id.toString().padStart(5, '0');
|
|
document.getElementById('receiptDate').textContent = data.payment_date;
|
|
document.getElementById('receiptCustomer').textContent = data.customer_name || '---';
|
|
document.getElementById('receiptInvNo').textContent = 'INV-' + data.inv_id.toString().padStart(5, '0');
|
|
document.getElementById('receiptMethod').textContent = data.payment_method;
|
|
document.getElementById('receiptAmount').textContent = 'OMR ' + parseFloat(data.amount).toFixed(3);
|
|
document.getElementById('receiptAmountWords').textContent = data.amount_words;
|
|
|
|
// Update labels for Purchase vs Sale
|
|
const partyLabel = document.getElementById('receiptPartyLabel');
|
|
const againstLabel = document.getElementById('receiptAgainstLabel');
|
|
const receiptTitle = document.querySelector('#receiptModal .modal-title');
|
|
const receiptH4 = document.querySelector('.receipt-container h4.letter-spacing-2');
|
|
|
|
if (data.inv_type === 'purchase') {
|
|
partyLabel.textContent = 'Paid To';
|
|
partyLabel.setAttribute('data-en', 'Paid To');
|
|
partyLabel.setAttribute('data-ar', 'صرف إلى');
|
|
|
|
againstLabel.textContent = 'Against Purchase';
|
|
againstLabel.setAttribute('data-en', 'Against Purchase');
|
|
againstLabel.setAttribute('data-ar', 'مقابل شراء');
|
|
|
|
receiptTitle.textContent = 'Payment Voucher';
|
|
receiptTitle.setAttribute('data-en', 'Payment Voucher');
|
|
receiptTitle.setAttribute('data-ar', 'سند صرف');
|
|
|
|
receiptH4.textContent = 'PAYMENT VOUCHER';
|
|
receiptH4.setAttribute('data-en', 'PAYMENT VOUCHER');
|
|
receiptH4.setAttribute('data-ar', 'سند صرف');
|
|
} else {
|
|
partyLabel.textContent = 'Received From';
|
|
partyLabel.setAttribute('data-en', 'Received From');
|
|
partyLabel.setAttribute('data-ar', 'وصلنا من');
|
|
|
|
againstLabel.textContent = 'Against Invoice';
|
|
againstLabel.setAttribute('data-en', 'Against Invoice');
|
|
againstLabel.setAttribute('data-ar', 'مقابل فاتورة');
|
|
|
|
receiptTitle.textContent = 'Payment Receipt';
|
|
receiptTitle.setAttribute('data-en', 'Payment Receipt');
|
|
receiptTitle.setAttribute('data-ar', 'سند قبض');
|
|
|
|
receiptH4.textContent = 'PAYMENT RECEIPT';
|
|
receiptH4.setAttribute('data-en', 'PAYMENT RECEIPT');
|
|
receiptH4.setAttribute('data-ar', 'سند قبض');
|
|
}
|
|
|
|
const notesContainer = document.getElementById('receiptNotesContainer');
|
|
if (data.notes) {
|
|
document.getElementById('receiptNotes').textContent = data.notes;
|
|
notesContainer.style.display = 'block';
|
|
} else {
|
|
notesContainer.style.display = 'none';
|
|
}
|
|
|
|
const receiptModal = new bootstrap.Modal(document.getElementById('receiptModal'));
|
|
receiptModal.show();
|
|
});
|
|
};
|
|
|
|
document.querySelectorAll('.view-payments-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const invoiceId = this.getAttribute('data-id');
|
|
const tbody = document.getElementById('paymentsTableBody');
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center">Loading...</td></tr>';
|
|
|
|
fetch(`index.php?action=get_payments&invoice_id=${invoiceId}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
tbody.innerHTML = '';
|
|
if (data.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center">No payments found.</td></tr>';
|
|
return;
|
|
}
|
|
data.forEach(p => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>RCP-${p.id.toString().padStart(5, '0')}</td>
|
|
<td>${p.payment_date}</td>
|
|
<td>${p.payment_method}</td>
|
|
<td class="text-end fw-bold">OMR ${parseFloat(p.amount).toFixed(3)}</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="showReceipt(${p.id})">
|
|
<i class="bi bi-printer"></i>
|
|
</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
window.printReceipt = function() {
|
|
const content = document.getElementById('printableReceipt').innerHTML;
|
|
const originalContent = document.body.innerHTML;
|
|
document.body.innerHTML = content;
|
|
window.print();
|
|
document.body.innerHTML = originalContent;
|
|
window.location.reload();
|
|
};
|
|
|
|
// Handle Expiry toggle in Edit Modals
|
|
document.querySelectorAll('.hasExpiryToggleEdit').forEach(toggle => {
|
|
toggle.addEventListener('change', function() {
|
|
const container = this.closest('.row').querySelector('.expiryDateContainerEdit');
|
|
if (container) {
|
|
container.style.display = this.checked ? 'block' : 'none';
|
|
if (!this.checked) {
|
|
container.querySelector('input').value = '';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Invoice Form Logic
|
|
const initInvoiceForm = (searchInputId, suggestionsId, tableBodyId, grandTotalId, subtotalId, totalVatId) => {
|
|
const searchInput = document.getElementById(searchInputId);
|
|
const suggestions = document.getElementById(suggestionsId);
|
|
const tableBody = document.getElementById(tableBodyId);
|
|
const grandTotalEl = document.getElementById(grandTotalId);
|
|
const subtotalEl = document.getElementById(subtotalId);
|
|
const totalVatEl = document.getElementById(totalVatId);
|
|
|
|
if (!searchInput || !tableBody) return;
|
|
|
|
let timeout = null;
|
|
searchInput.addEventListener('input', function() {
|
|
clearTimeout(timeout);
|
|
const q = this.value.trim();
|
|
if (q.length < 1) {
|
|
suggestions.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
timeout = setTimeout(() => {
|
|
fetch(`index.php?action=search_items&q=${encodeURIComponent(q)}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
suggestions.innerHTML = '';
|
|
if (data.length > 0) {
|
|
data.forEach(item => {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-group-item list-group-item-action';
|
|
btn.innerHTML = `
|
|
<div class="d-flex justify-content-between">
|
|
<span><strong>${item.sku}</strong> - ${item.name_en} / ${item.name_ar}</span>
|
|
<span class="text-muted small">Stock: ${item.stock_quantity}</span>
|
|
</div>
|
|
`;
|
|
btn.onclick = () => addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl);
|
|
suggestions.appendChild(btn);
|
|
});
|
|
suggestions.style.display = 'block';
|
|
} else {
|
|
suggestions.style.display = 'none';
|
|
}
|
|
});
|
|
}, 300);
|
|
});
|
|
|
|
// Close suggestions when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (!searchInput.contains(e.target) && !suggestions.contains(e.target)) {
|
|
suggestions.style.display = 'none';
|
|
}
|
|
});
|
|
};
|
|
|
|
function addItemToTable(item, tableBody, searchInput, suggestions, grandTotalEl, subtotalEl, totalVatEl, customData = null) {
|
|
if (suggestions) suggestions.style.display = 'none';
|
|
if (searchInput) searchInput.value = '';
|
|
|
|
const existingRow = Array.from(tableBody.querySelectorAll('.item-id-input')).find(input => input.value == item.id);
|
|
if (existingRow && !customData) {
|
|
const row = existingRow.closest('tr');
|
|
const qtyInput = row.querySelector('.item-qty');
|
|
qtyInput.value = parseFloat(qtyInput.value) + 1;
|
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
return;
|
|
}
|
|
|
|
const row = document.createElement('tr');
|
|
row.className = 'item-row';
|
|
const price = customData ? customData.unit_price : (invoiceType === 'sale' ? item.sale_price : item.purchase_price);
|
|
const qty = customData ? customData.quantity : 1;
|
|
const vatRate = item.vat_rate || 0;
|
|
|
|
row.innerHTML = `
|
|
<td>
|
|
<input type="hidden" name="item_ids[]" class="item-id-input" value="${item.id}">
|
|
<input type="hidden" class="item-vat-rate" value="${vatRate}">
|
|
<div><strong>${item.name_en}</strong></div>
|
|
<div class="small text-muted">${item.name_ar} (${item.sku})</div>
|
|
</td>
|
|
<td><input type="number" step="0.001" name="quantities[]" class="form-control item-qty" value="${qty}" required></td>
|
|
<td><input type="number" step="0.001" name="prices[]" class="form-control item-price" value="${price}" required></td>
|
|
<td><input type="text" class="form-control bg-light" value="${vatRate}%" readonly></td>
|
|
<td><input type="number" step="0.001" class="form-control item-total" value="${(qty * price).toFixed(3)}" readonly></td>
|
|
<td><button type="button" class="btn btn-outline-danger btn-sm remove-row"><i class="bi bi-trash"></i></button></td>
|
|
`;
|
|
|
|
tableBody.appendChild(row);
|
|
attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
}
|
|
|
|
function recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
|
let subtotal = 0;
|
|
let totalVat = 0;
|
|
tableBody.querySelectorAll('.item-row').forEach(row => {
|
|
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
|
|
const price = parseFloat(row.querySelector('.item-price').value) || 0;
|
|
const vatRate = parseFloat(row.querySelector('.item-vat-rate').value) || 0;
|
|
|
|
const total = qty * price;
|
|
const vatAmount = total * (vatRate / 100);
|
|
|
|
row.querySelector('.item-total').value = total.toFixed(3);
|
|
subtotal += total;
|
|
totalVat += vatAmount;
|
|
});
|
|
const grandTotal = subtotal + totalVat;
|
|
|
|
if (subtotalEl) subtotalEl.textContent = 'OMR ' + subtotal.toFixed(3);
|
|
if (totalVatEl) totalVatEl.textContent = 'OMR ' + totalVat.toFixed(3);
|
|
if (grandTotalEl) grandTotalEl.textContent = 'OMR ' + grandTotal.toFixed(3);
|
|
}
|
|
|
|
function attachRowListeners(row, tableBody, grandTotalEl, subtotalEl, totalVatEl) {
|
|
row.querySelector('.item-qty').addEventListener('input', () => recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl));
|
|
row.querySelector('.item-price').addEventListener('input', () => recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl));
|
|
row.querySelector('.remove-row').addEventListener('click', function() {
|
|
row.remove();
|
|
recalculate(tableBody, grandTotalEl, subtotalEl, totalVatEl);
|
|
});
|
|
}
|
|
|
|
const companySettings = <?= json_encode($data['settings']) ?>;
|
|
const invoiceType = '<?= in_array($page, ["sales", "quotations"]) ? "sale" : ($page === "purchases" ? "purchase" : "") ?>';
|
|
initInvoiceForm('productSearchInput', 'searchSuggestions', 'invoiceItemsTableBody', 'grandTotal', 'subtotal', 'totalVat');
|
|
initInvoiceForm('editProductSearchInput', 'editSearchSuggestions', 'editInvoiceItemsTableBody', 'edit_grandTotal', 'edit_subtotal', 'edit_totalVat');
|
|
|
|
// Quotation Form Logic
|
|
initInvoiceForm('quotProductSearchInput', 'quotSearchSuggestions', 'quotItemsTableBody', 'quot_grand_display', 'quot_subtotal_display', 'quot_vat_display');
|
|
initInvoiceForm('editQuotProductSearchInput', 'editQuotSearchSuggestions', 'editQuotItemsTableBody', 'edit_quot_grand_display', 'edit_quot_subtotal_display', 'edit_quot_vat_display');
|
|
|
|
document.querySelectorAll('.edit-quotation-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const data = JSON.parse(this.dataset.json);
|
|
document.getElementById('edit_quotation_id').value = data.id;
|
|
document.getElementById('edit_quot_customer_id').value = data.customer_id;
|
|
document.getElementById('edit_quot_date').value = data.quotation_date;
|
|
document.getElementById('edit_quot_valid').value = data.valid_until || '';
|
|
document.getElementById('edit_quot_status').value = data.status || 'pending';
|
|
|
|
const tableBody = document.getElementById('editQuotItemsTableBody');
|
|
tableBody.innerHTML = '';
|
|
|
|
data.items.forEach(item => {
|
|
addItemToTable({
|
|
id: item.item_id,
|
|
name_en: item.name_en,
|
|
name_ar: item.name_ar,
|
|
sku: '',
|
|
vat_rate: item.vat_rate || 0
|
|
}, tableBody, null, null,
|
|
document.getElementById('edit_quot_grand_display'),
|
|
document.getElementById('edit_quot_subtotal_display'),
|
|
document.getElementById('edit_quot_vat_display'),
|
|
{ quantity: item.quantity, unit_price: item.unit_price });
|
|
});
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.convert-quotation-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
if (confirm('Convert this quotation to an invoice? This will reduce stock.')) {
|
|
const f = document.createElement('form');
|
|
f.method = 'POST';
|
|
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${this.dataset.id}">`;
|
|
document.body.appendChild(f);
|
|
f.submit();
|
|
}
|
|
});
|
|
});
|
|
|
|
// View Quotation Logic
|
|
window.viewAndPrintQuotation = function(data, autoPrint = false) {
|
|
const modal = new bootstrap.Modal(document.getElementById('viewQuotationModal'));
|
|
const content = document.getElementById('quotationPrintableArea');
|
|
|
|
let itemsHtml = '';
|
|
data.items.forEach((item, index) => {
|
|
itemsHtml += `
|
|
<tr>
|
|
<td>${index + 1}</td>
|
|
<td>${item.name_en}<br><small>${item.name_ar}</small></td>
|
|
<td class="text-center">${item.quantity}</td>
|
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
|
<td class="text-center">${item.vat_rate}%</td>
|
|
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
// Company Logo and Header Construction
|
|
const logoUrl = companySettings.company_logo || '';
|
|
const logoImg = logoUrl ? `<img src="${logoUrl}" alt="Logo" class="invoice-logo mb-3">` : '';
|
|
const companyName = companySettings.company_name || 'Accounting System';
|
|
const companyAddress = (companySettings.company_address || '').replace(/\n/g, '<br>');
|
|
const companyVat = companySettings.vat_number ? `<p class="text-muted small mb-0">VAT: ${companySettings.vat_number}</p>` : '';
|
|
const companyPhone = companySettings.company_phone ? `<p class="text-muted small mb-0">Tel: ${companySettings.company_phone}</p>` : '';
|
|
|
|
// Quotation Header Construction
|
|
const quotDate = data.quotation_date;
|
|
const quotValid = data.valid_until || 'N/A';
|
|
const quotNo = 'QUO-' + data.id.toString().padStart(5, '0');
|
|
const customerName = data.customer_name || 'Walk-in Customer';
|
|
const statusBadge = `<span class="badge ${data.status === 'converted' ? 'bg-success' : 'bg-secondary'}">${data.status.toUpperCase()}</span>`;
|
|
|
|
content.innerHTML = `
|
|
<div class="p-5">
|
|
<div class="invoice-header mb-4">
|
|
<div class="row align-items-center">
|
|
<div class="col-6">
|
|
${logoImg}
|
|
<h3 class="mb-1 fw-bold">${companyName}</h3>
|
|
<p class="text-muted small mb-0">${companyAddress}</p>
|
|
${companyVat}
|
|
${companyPhone}
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h1 class="invoice-title fw-bold mb-0 text-uppercase">Quotation</h1>
|
|
<div class="mt-2">${statusBadge}</div>
|
|
<div class="mt-3">
|
|
<p class="mb-0 fs-5">No: <strong class="text-primary">${quotNo}</strong></p>
|
|
<p class="mb-0">Date: <span class="fw-bold">${quotDate}</span></p>
|
|
<p class="mb-0">Valid Until: <span class="fw-bold">${quotValid}</span></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4 g-3">
|
|
<div class="col-6">
|
|
<div class="invoice-info-card">
|
|
<p class="text-muted small text-uppercase fw-bold mb-2 border-bottom pb-1">To</p>
|
|
<h5 class="mb-1 fw-bold">${customerName}</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="table table-bordered table-formal">
|
|
<thead class="bg-dark text-white">
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Item Description</th>
|
|
<th class="text-center">Qty</th>
|
|
<th class="text-end">Unit Price</th>
|
|
<th class="text-center">VAT</th>
|
|
<th class="text-end">Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>${itemsHtml}</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<th colspan="5" class="text-end">Subtotal</th>
|
|
<td class="text-end fw-bold">${parseFloat(data.total_amount).toFixed(3)}</td>
|
|
</tr>
|
|
<tr>
|
|
<th colspan="5" class="text-end">VAT Amount</th>
|
|
<td class="text-end fw-bold">${parseFloat(data.vat_amount).toFixed(3)}</td>
|
|
</tr>
|
|
<tr class="table-primary">
|
|
<th colspan="5" class="text-end h5">Grand Total (OMR)</th>
|
|
<td class="text-end h5 fw-bold">${parseFloat(data.total_with_vat).toFixed(3)}</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
<div class="mt-5 pt-3 border-top">
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<p class="small text-muted text-uppercase fw-bold">Terms & Conditions:</p>
|
|
<ul class="small text-muted">
|
|
<li>Quotation is valid until the date mentioned above.</li>
|
|
<li>Prices are inclusive of VAT where applicable.</li>
|
|
</ul>
|
|
</div>
|
|
<div class="col-6 text-end pt-4">
|
|
<div class="border-top d-inline-block px-5">Authorized Signature</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-center">
|
|
<p class="text-muted x-small mb-0">Generated by ${companyName}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const actionButtons = document.getElementById('quotationActionButtons');
|
|
actionButtons.innerHTML = '';
|
|
if (data.status === 'pending') {
|
|
const convertBtn = document.createElement('button');
|
|
convertBtn.className = 'btn btn-success me-2';
|
|
convertBtn.innerHTML = '<i class="bi bi-receipt"></i> Convert to Invoice';
|
|
convertBtn.onclick = function() {
|
|
if (confirm('Convert this quotation to an invoice?')) {
|
|
const f = document.createElement('form');
|
|
f.method = 'POST';
|
|
f.innerHTML = `<input type="hidden" name="convert_to_invoice" value="1"><input type="hidden" name="quotation_id" value="${data.id}">`;
|
|
document.body.appendChild(f);
|
|
f.submit();
|
|
}
|
|
};
|
|
actionButtons.appendChild(convertBtn);
|
|
|
|
const editBtn = document.createElement('button');
|
|
editBtn.className = 'btn btn-primary';
|
|
editBtn.innerHTML = '<i class="bi bi-pencil-square"></i> Edit';
|
|
editBtn.onclick = function() {
|
|
const editModal = new bootstrap.Modal(document.getElementById('editQuotationModal'));
|
|
modal.hide();
|
|
const originalEditBtn = document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id},']`) ||
|
|
document.querySelector(`.edit-quotation-btn[data-json*='"id":${data.id}']`);
|
|
if (originalEditBtn) originalEditBtn.click();
|
|
};
|
|
actionButtons.appendChild(editBtn);
|
|
}
|
|
|
|
modal.show();
|
|
if (autoPrint) {
|
|
setTimeout(() => { window.print(); }, 500);
|
|
}
|
|
};
|
|
|
|
document.querySelectorAll('.view-quotation-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const data = JSON.parse(this.dataset.json);
|
|
window.viewAndPrintQuotation(data, false);
|
|
});
|
|
});
|
|
|
|
// Edit Invoice Logic
|
|
document.querySelectorAll('.edit-invoice-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const data = JSON.parse(this.dataset.json);
|
|
document.getElementById('edit_invoice_id').value = data.id;
|
|
document.getElementById('edit_customer_id').value = data.customer_id;
|
|
document.getElementById('edit_invoice_date').value = data.invoice_date;
|
|
document.getElementById('edit_payment_type').value = data.payment_type || 'cash';
|
|
document.getElementById('edit_status').value = data.status || 'unpaid';
|
|
document.getElementById('edit_paid_amount').value = parseFloat(data.paid_amount || 0).toFixed(3);
|
|
|
|
if (data.status === 'partially_paid') {
|
|
document.getElementById('editPaidAmountContainer').style.display = 'block';
|
|
} else {
|
|
document.getElementById('editPaidAmountContainer').style.display = 'none';
|
|
}
|
|
|
|
const tableBody = document.getElementById('editInvoiceItemsTableBody');
|
|
tableBody.innerHTML = '';
|
|
|
|
data.items.forEach(item => {
|
|
// We need more data than what's in invoice_items (like SKU and names, but we have them from the join in PHP)
|
|
// The dataset-json already contains name_en, name_ar etc because of the PHP logic at line 1093
|
|
const itemMeta = {
|
|
id: item.item_id,
|
|
name_en: item.name_en,
|
|
name_ar: item.name_ar,
|
|
sku: '', // Optional, or fetch if needed
|
|
vat_rate: 0 // Will be handled if we have it in the join
|
|
};
|
|
|
|
// Fetch current item details to get VAT rate if possible, or use stored if available
|
|
// For simplicity, let's assume we want to use the item's current VAT rate or store it.
|
|
// Looking at the join at line 1093, it doesn't fetch vat_rate. Let's fix that in PHP too.
|
|
|
|
addItemToTable({
|
|
id: item.item_id,
|
|
name_en: item.name_en,
|
|
name_ar: item.name_ar,
|
|
sku: '',
|
|
vat_rate: item.vat_rate || 0 // We'll add this to PHP join
|
|
}, tableBody, null, null,
|
|
document.getElementById('edit_grandTotal'),
|
|
document.getElementById('edit_subtotal'),
|
|
document.getElementById('edit_totalVat'),
|
|
{ quantity: item.quantity, unit_price: item.unit_price });
|
|
});
|
|
});
|
|
});
|
|
|
|
// View and Print Invoice Logic
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.closest('.view-invoice-btn')) {
|
|
const btn = e.target.closest('.view-invoice-btn');
|
|
const data = JSON.parse(btn.dataset.json);
|
|
if (window.viewAndPrintA4Invoice) {
|
|
window.viewAndPrintA4Invoice(data, false);
|
|
}
|
|
}
|
|
if (e.target.closest('.print-a4-btn')) {
|
|
const btn = e.target.closest('.print-a4-btn');
|
|
const data = JSON.parse(btn.dataset.json);
|
|
if (window.viewAndPrintA4Invoice) {
|
|
window.viewAndPrintA4Invoice(data, true);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
|
|
|
<!-- View Invoice Modal -->
|
|
<!-- Add Invoice Modal -->
|
|
<div class="modal fade" id="addInvoiceModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title" data-en="Create New Tax Invoice" data-ar="إنشاء فاتورة ضريبية جديدة">Create New Tax Invoice</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="type" value="<?= $page === 'sales' ? 'sale' : 'purchase' ?>">
|
|
<div class="modal-body p-4">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-bold" data-en="<?= $page === 'sales' ? 'Customer' : 'Supplier' ?>" data-ar="<?= $page === 'sales' ? 'العميل' : 'المورد' ?>"><?= $page === 'sales' ? 'Customer' : 'Supplier' ?></label>
|
|
<select name="customer_id" class="form-select" required>
|
|
<option value="">---</option>
|
|
<?php foreach ($data['customers_list'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
|
|
<input type="date" name="invoice_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label fw-bold" data-en="Payment Type" data-ar="طريقة الدفع">Payment Type</label>
|
|
<select name="payment_type" class="form-select">
|
|
<option value="cash">Cash</option>
|
|
<option value="card">Card</option>
|
|
<option value="bank_transfer">Bank Transfer</option>
|
|
<option value="credit">Credit</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label fw-bold" data-en="Status" data-ar="الحالة">Status</label>
|
|
<select name="status" id="add_status" class="form-select">
|
|
<option value="unpaid">Unpaid</option>
|
|
<option value="partially_paid">Partially Paid</option>
|
|
<option value="paid">Paid</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2" id="addPaidAmountContainer" style="display: none;">
|
|
<label class="form-label fw-bold" data-en="Paid Amount" data-ar="المبلغ المدفوع">Paid Amount</label>
|
|
<input type="number" step="0.001" name="paid_amount" class="form-control" value="0.000">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-body bg-light">
|
|
<label class="form-label fw-bold" data-en="Search Items" data-ar="بحث عن أصناف">Search Items</label>
|
|
<div class="position-relative">
|
|
<input type="text" id="productSearchInput" class="form-control" placeholder="Search by name or SKU..." autocomplete="off">
|
|
<div id="searchSuggestions" class="list-group position-absolute w-100 shadow-sm" style="display: none; z-index: 1000;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered align-middle">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th style="width: 40%;" data-en="Item Details" data-ar="تفاصيل الصنف">Item Details</th>
|
|
<th style="width: 15%;" data-en="Qty" data-ar="الكمية">Qty</th>
|
|
<th style="width: 15%;" data-en="Unit Price" data-ar="سعر الوحدة">Unit Price</th>
|
|
<th style="width: 10%;" data-en="VAT" data-ar="الضريبة">VAT</th>
|
|
<th style="width: 15%;" data-en="Total" data-ar="الإجمالي">Total</th>
|
|
<th style="width: 5%;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="invoiceItemsTableBody"></tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="4" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
|
<td class="text-end fw-bold" id="subtotal">OMR 0.000</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="4" class="text-end fw-bold" data-en="Total VAT" data-ar="إجمالي الضريبة">Total VAT</td>
|
|
<td class="text-end fw-bold" id="totalVat">OMR 0.000</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr class="table-primary">
|
|
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
|
<td class="text-end fw-bold h5" id="grandTotal">OMR 0.000</td>
|
|
<td></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_invoice" class="btn btn-primary" data-en="Create Invoice" data-ar="إنشاء الفاتورة">Create Invoice</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Invoice Modal -->
|
|
<div class="modal fade" id="editInvoiceModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title" data-en="Edit Invoice" data-ar="تعديل الفاتورة">Edit Invoice</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="invoice_id" id="edit_invoice_id">
|
|
<div class="modal-body p-4">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-bold" data-en="<?= $page === 'sales' ? 'Customer' : 'Supplier' ?>" data-ar="<?= $page === 'sales' ? 'العميل' : 'المورد' ?>"><?= $page === 'sales' ? 'Customer' : 'Supplier' ?></label>
|
|
<select name="customer_id" id="edit_customer_id" class="form-select" required>
|
|
<option value="">---</option>
|
|
<?php foreach ($data['customers_list'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
|
|
<input type="date" name="invoice_date" id="edit_invoice_date" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label fw-bold" data-en="Payment Type" data-ar="طريقة الدفع">Payment Type</label>
|
|
<select name="payment_type" id="edit_payment_type" class="form-select">
|
|
<option value="cash">Cash</option>
|
|
<option value="card">Card</option>
|
|
<option value="bank_transfer">Bank Transfer</option>
|
|
<option value="credit">Credit</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label fw-bold" data-en="Status" data-ar="الحالة">Status</label>
|
|
<select name="status" id="edit_status" class="form-select">
|
|
<option value="unpaid">Unpaid</option>
|
|
<option value="partially_paid">Partially Paid</option>
|
|
<option value="paid">Paid</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2" id="editPaidAmountContainer" style="display: none;">
|
|
<label class="form-label fw-bold" data-en="Paid Amount" data-ar="المبلغ المدفوع">Paid Amount</label>
|
|
<input type="number" step="0.001" name="paid_amount" id="edit_paid_amount" class="form-control">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-body bg-light">
|
|
<label class="form-label fw-bold" data-en="Search Items" data-ar="بحث عن أصناف">Search Items</label>
|
|
<div class="position-relative">
|
|
<input type="text" id="editProductSearchInput" class="form-control" placeholder="Search by name or SKU..." autocomplete="off">
|
|
<div id="editSearchSuggestions" class="list-group position-absolute w-100 shadow-sm" style="display: none; z-index: 1000;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered align-middle">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th style="width: 40%;" data-en="Item Details" data-ar="تفاصيل الصنف">Item Details</th>
|
|
<th style="width: 15%;" data-en="Qty" data-ar="الكمية">Qty</th>
|
|
<th style="width: 15%;" data-en="Unit Price" data-ar="سعر الوحدة">Unit Price</th>
|
|
<th style="width: 10%;" data-en="VAT" data-ar="الضريبة">VAT</th>
|
|
<th style="width: 15%;" data-en="Total" data-ar="الإجمالي">Total</th>
|
|
<th style="width: 5%;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="editInvoiceItemsTableBody"></tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="4" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
|
<td class="text-end fw-bold" id="edit_subtotal">OMR 0.000</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="4" class="text-end fw-bold" data-en="Total VAT" data-ar="إجمالي الضريبة">Total VAT</td>
|
|
<td class="text-end fw-bold" id="edit_totalVat">OMR 0.000</td>
|
|
<td></td>
|
|
</tr>
|
|
<tr class="table-primary">
|
|
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
|
<td class="text-end fw-bold h5" id="edit_grandTotal">OMR 0.000</td>
|
|
<td></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_invoice" class="btn btn-primary" data-en="Update Invoice" data-ar="تحديث الفاتورة">Update Invoice</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Quotation Modal -->
|
|
<div class="modal fade" id="addQuotationModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title" data-en="Create New Quotation" data-ar="إنشاء عرض سعر جديد">Create New Quotation</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body p-4">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-bold" data-en="Customer" data-ar="العميل">Customer</label>
|
|
<select name="customer_id" class="form-select" required>
|
|
<option value="">---</option>
|
|
<?php foreach ($data['customers_list'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
|
|
<input type="date" name="quotation_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-bold" data-en="Valid Until" data-ar="صالح حتى">Valid Until</label>
|
|
<input type="date" name="valid_until" class="form-control">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-body bg-light">
|
|
<label class="form-label fw-bold" data-en="Search Items" data-ar="بحث عن أصناف">Search Items</label>
|
|
<div class="position-relative">
|
|
<input type="text" id="quotProductSearchInput" class="form-control" placeholder="Search by name or SKU..." autocomplete="off">
|
|
<div id="quotSearchSuggestions" class="list-group position-absolute w-100 shadow-sm" style="display: none; z-index: 1000;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered align-middle">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th style="width: 40%;" data-en="Item Details" data-ar="تفاصيل الصنف">Item Details</th>
|
|
<th style="width: 15%;" data-en="Qty" data-ar="الكمية">Qty</th>
|
|
<th style="width: 15%;" data-en="Unit Price" data-ar="سعر الوحدة">Unit Price</th>
|
|
<th style="width: 10%;" data-en="VAT" data-ar="الضريبة">VAT</th>
|
|
<th style="width: 15%;" data-en="Total" data-ar="الإجمالي">Total</th>
|
|
<th style="width: 5%;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="quotItemsTableBody"></tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="4" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
|
<td class="text-end fw-bold" id="quot_subtotal_display">OMR 0.000</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="4" class="text-end fw-bold" data-en="Total VAT" data-ar="إجمالي الضريبة">Total VAT</td>
|
|
<td class="text-end fw-bold" id="quot_vat_display">OMR 0.000</td>
|
|
</tr>
|
|
<tr class="table-primary">
|
|
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
|
<td class="text-end fw-bold h5" id="quot_grand_display">OMR 0.000</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_quotation" class="btn btn-primary" data-en="Create Quotation" data-ar="إنشاء عرض السعر">Create Quotation</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Quotation Modal -->
|
|
<div class="modal fade" id="editQuotationModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title" data-en="Edit Quotation" data-ar="تعديل عرض السعر">Edit Quotation</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="quotation_id" id="edit_quotation_id">
|
|
<div class="modal-body p-4">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-bold" data-en="Customer" data-ar="العميل">Customer</label>
|
|
<select name="customer_id" id="edit_quot_customer_id" class="form-select" required>
|
|
<option value="">---</option>
|
|
<?php foreach ($data['customers_list'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-bold" data-en="Date" data-ar="التاريخ">Date</label>
|
|
<input type="date" name="quotation_date" id="edit_quot_date" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-bold" data-en="Valid Until" data-ar="صالح حتى">Valid Until</label>
|
|
<input type="date" name="valid_until" id="edit_quot_valid" class="form-control">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-bold" data-en="Status" data-ar="الحالة">Status</label>
|
|
<select name="status" id="edit_quot_status" class="form-select">
|
|
<option value="pending">Pending</option>
|
|
<option value="converted">Converted</option>
|
|
<option value="expired">Expired</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-body bg-light">
|
|
<label class="form-label fw-bold" data-en="Search Items" data-ar="بحث عن أصناف">Search Items</label>
|
|
<div class="position-relative">
|
|
<input type="text" id="editQuotProductSearchInput" class="form-control" placeholder="Search by name or SKU..." autocomplete="off">
|
|
<div id="editQuotSearchSuggestions" class="list-group position-absolute w-100 shadow-sm" style="display: none; z-index: 1000;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered align-middle">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th style="width: 40%;" data-en="Item Details" data-ar="تفاصيل الصنف">Item Details</th>
|
|
<th style="width: 15%;" data-en="Qty" data-ar="الكمية">Qty</th>
|
|
<th style="width: 15%;" data-en="Unit Price" data-ar="سعر الوحدة">Unit Price</th>
|
|
<th style="width: 10%;" data-en="VAT" data-ar="الضريبة">VAT</th>
|
|
<th style="width: 15%;" data-en="Total" data-ar="الإجمالي">Total</th>
|
|
<th style="width: 5%;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="editQuotItemsTableBody"></tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="4" class="text-end fw-bold" data-en="Subtotal" data-ar="الإجمالي الفرعي">Subtotal</td>
|
|
<td class="text-end fw-bold" id="edit_quot_subtotal_display">OMR 0.000</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="4" class="text-end fw-bold" data-en="Total VAT" data-ar="إجمالي الضريبة">Total VAT</td>
|
|
<td class="text-end fw-bold" id="edit_quot_vat_display">OMR 0.000</td>
|
|
</tr>
|
|
<tr class="table-primary">
|
|
<td colspan="4" class="text-end fw-bold h5" data-en="Grand Total" data-ar="الإجمالي النهائي">Grand Total</td>
|
|
<td class="text-end fw-bold h5" id="edit_quot_grand_display">OMR 0.000</td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_quotation" class="btn btn-primary" data-en="Update Quotation" data-ar="تحديث عرض السعر">Update Quotation</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- View Quotation Modal -->
|
|
<div class="modal fade" id="viewQuotationModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header bg-info text-white d-print-none">
|
|
<h5 class="modal-title" data-en="View Quotation" data-ar="عرض سعر">View Quotation</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0" id="quotationPrintableArea">
|
|
<!-- Dynamic content -->
|
|
</div>
|
|
<div class="modal-footer d-print-none">
|
|
<div id="quotationActionButtons" class="me-auto"></div>
|
|
<button type="button" class="btn btn-secondary" onclick="window.print()"><i class="bi bi-printer"></i> Print</button>
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@media print {
|
|
.no-print, .sidebar, .topbar, .card, .btn, .modal-header, .modal-footer, .d-print-none, .table-responsive,
|
|
table:not(.table-formal), .bg-light:not(.invoice-info-card):not(.p-3), .modal-backdrop { display: none !important; }
|
|
body { background: white !important; margin: 0 !important; padding: 0 !important; }
|
|
.main-content { margin: 0 !important; padding: 0 !important; background: white !important; }
|
|
|
|
/* Hide all modals by default */
|
|
.modal { display: none !important; }
|
|
|
|
/* Show ONLY the active modal */
|
|
.modal.show {
|
|
position: absolute !important;
|
|
left: 0 !important;
|
|
top: 0 !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
overflow: visible !important;
|
|
display: block !important;
|
|
visibility: visible !important;
|
|
background: white !important;
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
}
|
|
|
|
.modal.show .modal-dialog { max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 !important; }
|
|
.modal.show .modal-content { border: none !important; box-shadow: none !important; background: white !important; }
|
|
.modal.show .modal-body { padding: 0 !important; margin: 0 !important; background: white !important; }
|
|
|
|
.table-bordered th, .table-bordered td { border: 1px solid #dee2e6 !important; }
|
|
.bg-light { background-color: #f8f9fa !important; -webkit-print-color-adjust: exact; }
|
|
.text-primary { color: #0d6efd !important; -webkit-print-color-adjust: exact; }
|
|
.badge { border: 1px solid #000; color: #000 !important; }
|
|
|
|
/* Ensure the modal is the only thing visible */
|
|
body > *:not(.main-content):not(.modal) { display: none !important; }
|
|
.main-content > *:not(.modal) { display: none !important; }
|
|
}
|
|
.invoice-logo { max-height: 80px; width: auto; }
|
|
.invoice-header { border-bottom: 2px solid #333; padding-bottom: 20px; }
|
|
.invoice-title { font-size: 2.5rem; color: #333; letter-spacing: 2px; }
|
|
.invoice-info-card { background: #f8f9fa; border-radius: 8px; padding: 15px; height: 100%; }
|
|
.table-formal thead th { background: #333; color: #fff; border: none; text-transform: uppercase; font-size: 0.85rem; }
|
|
</style>
|
|
|
|
<div class="modal fade" id="viewInvoiceModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header d-print-none">
|
|
<h5 class="modal-title" data-en="View Invoice" data-ar="عرض الفاتورة">View Invoice</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0" id="invoicePrintableArea">
|
|
<div class="p-5">
|
|
<div class="invoice-header mb-4">
|
|
<div class="row align-items-center">
|
|
<div class="col-6">
|
|
<?php
|
|
$logo = $data['settings']['company_logo'] ?? $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|
if ($logo):
|
|
?>
|
|
<img src="<?= htmlspecialchars($logo) ?>" alt="Logo" class="invoice-logo mb-3">
|
|
<?php endif; ?>
|
|
<h3 class="mb-1 fw-bold"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
|
<p class="text-muted small mb-0"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
|
<p class="text-muted small mb-0">VAT: <?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?></p>
|
|
<?php if (!empty($data['settings']['company_phone'])): ?>
|
|
<p class="text-muted small mb-0">Tel: <?= htmlspecialchars($data['settings']['company_phone']) ?></p>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h1 class="invoice-title fw-bold mb-0 text-uppercase">Tax Invoice</h1>
|
|
<div class="mt-2"><span id="invoiceTypeLabel" class="badge"></span></div>
|
|
<div class="mt-3">
|
|
<p class="mb-0 fs-5">Invoice No: <strong id="invNumber" class="text-primary"></strong></p>
|
|
<p class="mb-0">Date: <span id="invDate" class="fw-bold"></span></p>
|
|
<p class="mb-0 small">Status: <span id="invoiceStatusLabel"></span></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4 g-3">
|
|
<div class="col-6">
|
|
<div class="invoice-info-card">
|
|
<p class="text-muted small text-uppercase fw-bold mb-2 border-bottom pb-1" id="invPartyLabel">Bill To</p>
|
|
<h5 class="mb-1 fw-bold"><span id="invCustomerName"></span></h5>
|
|
<p class="small text-muted mb-0" id="invCustomerTaxIdContainer">VAT: <span id="invCustomerTaxId"></span></p>
|
|
<p class="small text-muted mb-0" id="invCustomerPhoneContainer">Phone: <span id="invCustomerPhone"></span></p>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="invoice-info-card text-end">
|
|
<p class="text-muted small text-uppercase fw-bold mb-2 border-bottom pb-1">Payment Details</p>
|
|
<p class="mb-1">Method: <strong id="invPaymentType"></strong></p>
|
|
<p class="mb-0 small text-muted">Currency: <strong>OMR</strong></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="table table-bordered table-formal">
|
|
<thead>
|
|
<tr>
|
|
<th>Item Description</th>
|
|
<th class="text-center" style="width: 80px;">Qty</th>
|
|
<th class="text-end" style="width: 120px;">Unit Price</th>
|
|
<th class="text-end" style="width: 100px;">VAT %</th>
|
|
<th class="text-end" style="width: 150px;">Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="invItemsBody"></tbody>
|
|
</table>
|
|
|
|
<div class="row mt-4">
|
|
<div class="col-7">
|
|
<div class="p-3 bg-light rounded" style="min-height: 100px;">
|
|
<p class="text-muted small text-uppercase fw-bold mb-1">Amount in Words</p>
|
|
<p id="invAmountInWords" class="small fw-bold mb-0"></p>
|
|
</div>
|
|
<div class="mt-4">
|
|
<p class="text-muted small text-uppercase fw-bold mb-1">Terms & Conditions</p>
|
|
<ul class="small text-muted ps-3">
|
|
<li>Goods once sold will not be taken back or exchanged.</li>
|
|
<li>Payment is due within the agreed credit period.</li>
|
|
<li>This is a computer-generated invoice and does not require a physical signature.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="col-5">
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span class="text-muted">Subtotal</span>
|
|
<span id="invSubtotal" class="fw-bold"></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span class="text-muted">VAT Amount</span>
|
|
<span id="invVatAmount" class="fw-bold"></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-3 border-top pt-2">
|
|
<span class="h4 fw-bold">Grand Total</span>
|
|
<span id="invGrandTotal" class="h4 fw-bold text-primary"></span>
|
|
</div>
|
|
|
|
<div id="invPaymentsSection" class="mt-4 border-top pt-3">
|
|
<p class="text-muted small text-uppercase fw-bold mb-2">Payment Tracking</p>
|
|
<table class="table table-sm table-bordered small mb-3">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Method</th>
|
|
<th class="text-end">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="invPaymentsBody"></tbody>
|
|
</table>
|
|
<div class="bg-light p-3 rounded">
|
|
<div class="d-flex justify-content-between small mb-1">
|
|
<span>Paid Amount</span>
|
|
<span id="invPaidInfo" class="text-success fw-bold"></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between small fw-bold">
|
|
<span>Balance Due</span>
|
|
<span id="invBalanceInfo" class="text-danger"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-5 pt-4 border-top text-center">
|
|
<div class="row">
|
|
<div class="col-4">
|
|
<div class="border-top pt-2 mx-auto" style="width: 150px;">
|
|
<p class="small text-muted">Customer Signature</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div id="invQrCode" class="mb-2 d-flex justify-content-center"></div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="border-top pt-2 mx-auto" style="width: 150px;">
|
|
<p class="small text-muted">Authorized Signatory</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-muted x-small mt-4 mb-0">Generated by <?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?> | Visit us at <?= $_SERVER['HTTP_HOST'] ?></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer d-print-none">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" onclick="window.print()"><i class="bi bi-printer me-2"></i>Print Invoice</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pay Invoice Modal -->
|
|
<div class="modal fade" id="payInvoiceModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title" data-en="Record Payment" data-ar="تسجيل دفعة">Record Payment</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="invoice_id" id="pay_invoice_id">
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold" data-en="Total Amount" data-ar="المبلغ الإجمالي">Total Amount</label>
|
|
<input type="text" id="pay_invoice_total" class="form-control" readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold" data-en="Remaining Amount" data-ar="المبلغ المتبقي">Remaining Amount</label>
|
|
<input type="text" id="pay_remaining_amount" class="form-control" readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold" data-en="Amount to Pay" data-ar="المبلغ المراد دفعه">Amount to Pay</label>
|
|
<input type="number" step="0.001" name="amount" id="pay_amount" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold" data-en="Payment Date" data-ar="تاريخ الدفع">Payment Date</label>
|
|
<input type="date" name="payment_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold" data-en="Payment Method" data-ar="طريقة الدفع">Payment Method</label>
|
|
<select name="payment_method" class="form-select" required>
|
|
<option value="Cash">Cash</option>
|
|
<option value="Card">Card</option>
|
|
<option value="Bank Transfer">Bank Transfer</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold" data-en="Notes" data-ar="ملاحظات">Notes</label>
|
|
<textarea name="notes" class="form-control" rows="2"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="record_payment" class="btn btn-success" data-en="Save Payment" data-ar="حفظ الدفعة">Save Payment</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Payment Receipt Modal -->
|
|
<div class="modal fade" id="receiptModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header border-0 pb-0 d-print-none">
|
|
<h5 class="modal-title">Payment Receipt</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0" id="printableReceipt">
|
|
<div class="receipt-container p-4">
|
|
<div class="text-center mb-4">
|
|
<h3 class="mb-1 fw-bold"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
|
<p class="text-muted small mb-0"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
|
<hr class="my-4">
|
|
<h4 class="letter-spacing-2 fw-bold text-uppercase">Payment Receipt</h4>
|
|
</div>
|
|
<div class="row mb-4">
|
|
<div class="col-6">
|
|
<p class="mb-1 text-muted small text-uppercase fw-bold">Receipt No</p>
|
|
<p class="fw-bold h5 text-primary" id="receiptNo"></p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<p class="mb-1 text-muted small text-uppercase fw-bold">Date</p>
|
|
<p class="fw-bold" id="receiptDate"></p>
|
|
</div>
|
|
</div>
|
|
<div class="mb-4 p-3 bg-light rounded">
|
|
<div class="row mb-2">
|
|
<div class="col-4 text-muted small text-uppercase fw-bold" id="receiptPartyLabel">Received From</div>
|
|
<div class="col-8 fw-bold" id="receiptCustomer"></div>
|
|
</div>
|
|
<div class="row mb-2">
|
|
<div class="col-4 text-muted small text-uppercase fw-bold" id="receiptAgainstLabel">Against Invoice</div>
|
|
<div class="col-8 fw-bold" id="receiptInvNo"></div>
|
|
</div>
|
|
<div class="row mb-2">
|
|
<div class="col-4 text-muted small text-uppercase fw-bold">Payment Method</div>
|
|
<div class="col-8 fw-bold" id="receiptMethod"></div>
|
|
</div>
|
|
</div>
|
|
<div class="text-center my-4 py-4 border-top border-bottom">
|
|
<p class="mb-1 text-muted small text-uppercase fw-bold">Amount Paid</p>
|
|
<h1 class="display-5 fw-bold text-primary mb-1" id="receiptAmount"></h1>
|
|
<p class="text-muted small font-italic" id="receiptAmountWords"></p>
|
|
</div>
|
|
<div id="receiptNotesContainer" class="mb-4" style="display: none;">
|
|
<p class="mb-1 text-muted small text-uppercase fw-bold">Notes</p>
|
|
<p class="small p-2 bg-light rounded" id="receiptNotes"></p>
|
|
</div>
|
|
<div class="row mt-5 pt-4">
|
|
<div class="col-6">
|
|
<div class="border-top pt-2 text-center small text-muted">Receiver's Signature</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="border-top pt-2 text-center small text-muted">Authorized Signatory</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 d-print-none">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" onclick="window.printReceipt()"><i class="bi bi-printer me-2"></i>Print Receipt</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- POS Payment Modal -->
|
|
<div class="modal fade" id="posPaymentModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title fw-bold">Payment</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3 p-3 border rounded bg-light shadow-sm">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<span class="small text-muted d-block">Customer</span>
|
|
<span class="h6 fw-bold m-0 text-primary" id="paymentCustomerName">Walk-in Customer</span>
|
|
</div>
|
|
<i class="bi bi-person-circle fs-3 text-secondary"></i>
|
|
</div>
|
|
<div id="creditCustomerSection" class="mt-2 pt-2 border-top" style="display:none;">
|
|
<label class="form-label smaller fw-bold mb-1">Select Credit Customer</label>
|
|
<select id="paymentCreditCustomer" class="form-select form-select-sm" onchange="cart.syncCustomer(this.value)">
|
|
<option value="">--- Select Customer ---</option>
|
|
<?php foreach ($customers as $c): ?>
|
|
<option value="<?= $c['id'] ?>" data-search="<?= htmlspecialchars(strtolower($c['name'] . ' ' . ($c['phone'] ?? ''))) ?>"><?= htmlspecialchars($c['name']) ?> (<?= htmlspecialchars($c['phone'] ?? '') ?>)</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="amount-due-box mb-3">
|
|
<div class="d-flex justify-content-between px-3">
|
|
<div class="text-start">
|
|
<div class="label">Amount Due</div>
|
|
<div class="value" id="paymentAmountDue">OMR 0.000</div>
|
|
</div>
|
|
<div class="text-end">
|
|
<div class="label text-danger">Remaining</div>
|
|
<div class="value text-danger" id="paymentRemaining">OMR 0.000</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="paymentList" class="mb-3">
|
|
<!-- Added payments will appear here -->
|
|
</div>
|
|
|
|
<div class="mb-3 p-3 border rounded bg-light">
|
|
<label class="form-label small fw-bold">Add Payment Method</label>
|
|
<div class="payment-methods-grid mb-2">
|
|
<div class="payment-method-btn active" data-method="cash" onclick="cart.selectMethod('cash', this)">
|
|
<i class="bi bi-cash-stack"></i>
|
|
<span class="small fw-bold">Cash</span>
|
|
</div>
|
|
<div class="payment-method-btn" data-method="card" onclick="cart.selectMethod('card', this)">
|
|
<i class="bi bi-credit-card"></i>
|
|
<span class="small fw-bold">Card</span>
|
|
</div>
|
|
<div class="payment-method-btn" data-method="credit" onclick="cart.selectMethod('credit', this)">
|
|
<i class="bi bi-person-badge"></i>
|
|
<span class="small fw-bold">Credit</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col">
|
|
<label class="form-label smaller fw-bold mb-1">Amount</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text p-1 small">OMR</span>
|
|
<input type="number" step="0.001" id="partialAmount" class="form-control" placeholder="0.000" oninput="cart.updateRemaining()">
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button type="button" class="btn btn-primary" onclick="cart.addPaymentLine()">
|
|
<i class="bi bi-plus-lg"></i> ADD
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="quick-pay-grid mt-2">
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(1)">1</div>
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(5)">5</div>
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(10)">10</div>
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(20)">20</div>
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(50)">50</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="cashPaymentSection" style="display: none;">
|
|
<div class="d-flex justify-content-between align-items-center p-3 bg-primary-subtle rounded border border-primary-subtle">
|
|
<span class="fw-bold">Total Tendered (Cash)</span>
|
|
<span class="h4 m-0 fw-bold text-primary" id="changeDue">OMR 0.000</span>
|
|
</div>
|
|
<div class="small text-muted mt-1">* Change is calculated based on cash payments only.</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary px-4" id="confirmPaymentBtn" onclick="cart.completeOrder()">
|
|
PAY & COMPLETE
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- POS Receipt Modal -->
|
|
<div class="modal fade" id="posReceiptModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content border-0">
|
|
<div class="modal-header border-0 pb-0">
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body pt-0">
|
|
<div id="posReceiptContent">
|
|
<!-- Receipt content will be generated here -->
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0">
|
|
<button type="button" class="btn btn-primary w-100" onclick="printPosReceipt()">
|
|
<i class="bi bi-printer me-2"></i>PRINT RECEIPT
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="posPrintArea" class="d-none d-print-block"></div>
|
|
|
|
<!-- Barcode Print Modal -->
|
|
<div class="modal fade" id="barcodePrintModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Print Barcode Label</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<div id="barcodeContainer" class="p-4 bg-white border mb-3 mx-auto" style="width: fit-content;">
|
|
<div id="barcodeLabelName" class="fw-bold small mb-1"></div>
|
|
<svg id="barcodeSvg"></svg>
|
|
<div id="barcodeLabelPrice" class="fw-bold mt-1"></div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small">Number of Labels</label>
|
|
<input type="number" id="barcodeQty" class="form-control form-control-sm mx-auto" value="1" min="1" style="width: 80px;">
|
|
</div>
|
|
<div class="row mb-3 mx-auto" style="max-width: 200px;">
|
|
<div class="col-6">
|
|
<label class="form-label small">Width (mm)</label>
|
|
<input type="number" id="barcodeWidth" class="form-control form-control-sm" value="40" min="10">
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small">Height (mm)</label>
|
|
<input type="number" id="barcodeHeight" class="form-control form-control-sm" value="25" min="10">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" onclick="executeBarcodePrint()"><i class="bi bi-printer me-2"></i>Print Now</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Avery Labels Modal -->
|
|
<div class="modal fade" id="averyLabelsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header d-print-none">
|
|
<h5 class="modal-title">Avery Barcode Labels (A4)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row mb-4 d-print-none">
|
|
<div class="col-md-4">
|
|
<label class="form-label small">Label Layout</label>
|
|
<select id="averyLayout" class="form-select form-select-sm" onchange="updateAveryPreview()">
|
|
<option value="3x7">3 x 7 (21 Labels per sheet)</option>
|
|
<option value="3x8">3 x 8 (24 Labels per sheet)</option>
|
|
<option value="4x10">4 x 10 (40 Labels per sheet)</option>
|
|
<option value="L7651">L7651 (5 x 13 - 65 Labels)</option>
|
|
<option value="L4736">L4736 (2 x 7 - 14 Labels)</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small">Copies (Set All)</label>
|
|
<input type="number" id="averyCopies" class="form-control form-control-sm" value="1" min="1" oninput="updateAllItemQuantities()" onchange="updateAllItemQuantities()">
|
|
</div>
|
|
<div class="col-md-4 d-flex align-items-end">
|
|
<button class="btn btn-primary btn-sm w-100" onclick="window.print()"><i class="bi bi-printer me-2"></i>Print A4 Sheet</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3 d-print-none">
|
|
<div class="col-12">
|
|
<label class="form-label small fw-bold" data-en="Quantities per Item" data-ar="الكميات لكل صنف">Quantities per Item</label>
|
|
<div id="averyItemQuantities" class="border rounded p-2 bg-light" style="max-height: 150px; overflow-y: auto;">
|
|
<small class="text-muted">Select items to adjust quantities.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="averyPrintArea" class="avery-container">
|
|
<!-- Labels will be generated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Avery Label Printing */
|
|
.avery-container {
|
|
background: white;
|
|
width: 210mm; /* A4 Width */
|
|
min-height: 297mm; /* A4 Height */
|
|
padding: 10mm 5mm;
|
|
margin: 0 auto;
|
|
display: grid;
|
|
gap: 2mm;
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
|
}
|
|
.avery-layout-3x7 { grid-template-columns: repeat(3, 1fr); grid-auto-rows: 38mm; }
|
|
.avery-layout-3x8 { grid-template-columns: repeat(3, 1fr); grid-auto-rows: 34mm; }
|
|
.avery-layout-4x10 { grid-template-columns: repeat(4, 1fr); grid-auto-rows: 27mm; }
|
|
.avery-layout-L7651 { grid-template-columns: repeat(5, 1fr); grid-auto-rows: 21mm; }
|
|
.avery-layout-L4736 { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 38mm; }
|
|
|
|
.avery-layout-L7651 .avery-label { padding: 2mm; }
|
|
.avery-layout-L7651 .avery-label svg { height: 25px; }
|
|
.avery-layout-L7651 .avery-label div { font-size: 8px !important; }
|
|
|
|
.avery-label {
|
|
border: 1px dashed #eee;
|
|
padding: 5mm;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
overflow: hidden;
|
|
background: white;
|
|
}
|
|
|
|
.avery-label svg {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
@media print {
|
|
body.printing-avery .sidebar,
|
|
body.printing-avery .topbar,
|
|
body.printing-avery .modal-header,
|
|
body.printing-avery .d-print-none,
|
|
body.printing-avery .modal-backdrop {
|
|
display: none !important;
|
|
}
|
|
body.printing-avery .modal {
|
|
position: absolute !important;
|
|
left: 0 !important;
|
|
top: 0 !important;
|
|
width: 100% !important;
|
|
display: block !important;
|
|
visibility: visible !important;
|
|
background: white !important;
|
|
}
|
|
body.printing-avery .modal-dialog {
|
|
max-width: 100% !important;
|
|
width: 100% !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
}
|
|
body.printing-avery .modal-content {
|
|
border: none !important;
|
|
}
|
|
body.printing-avery .avery-label {
|
|
border: none !important;
|
|
}
|
|
.avery-container {
|
|
margin: 0 !important;
|
|
padding: 10mm 5mm !important;
|
|
box-shadow: none !important;
|
|
border: none !important;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// Avery Label Logic
|
|
const selectAllItems = document.getElementById('selectAllItems');
|
|
const bulkBarcodeBtn = document.getElementById('bulkBarcodeBtn');
|
|
|
|
if (selectAllItems) {
|
|
selectAllItems.addEventListener('change', function() {
|
|
document.querySelectorAll('.item-checkbox').forEach(cb => {
|
|
cb.checked = this.checked;
|
|
});
|
|
toggleBulkBtn();
|
|
});
|
|
}
|
|
|
|
document.addEventListener('change', function(e) {
|
|
if (e.target.classList.contains('item-checkbox')) {
|
|
toggleBulkBtn();
|
|
}
|
|
});
|
|
|
|
function toggleBulkBtn() {
|
|
const checked = document.querySelectorAll('.item-checkbox:checked').length;
|
|
if (bulkBarcodeBtn) {
|
|
bulkBarcodeBtn.style.display = checked > 0 ? 'inline-block' : 'none';
|
|
}
|
|
}
|
|
|
|
window.openAveryModal = function() {
|
|
const modal = new bootstrap.Modal(document.getElementById('averyLabelsModal'));
|
|
const checkedItems = document.querySelectorAll('.item-checkbox:checked');
|
|
const container = document.getElementById('averyItemQuantities');
|
|
const defaultCopies = parseInt(document.getElementById('averyCopies').value) || 1;
|
|
|
|
if (container) {
|
|
container.innerHTML = '';
|
|
if (checkedItems.length === 0) {
|
|
container.innerHTML = '<small class="text-muted">No items selected.</small>';
|
|
} else {
|
|
const table = document.createElement('table');
|
|
table.className = 'table table-sm table-borderless mb-0';
|
|
const tbody = document.createElement('tbody');
|
|
|
|
checkedItems.forEach(cb => {
|
|
const sku = cb.dataset.sku;
|
|
const name = cb.dataset.name;
|
|
const id = cb.dataset.id;
|
|
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td class="align-middle" style="width: 70%; font-size: 0.9em;">${name} <span class="text-muted">(${sku})</span></td>
|
|
<td class="align-middle" style="width: 30%;">
|
|
<input type="number" class="form-control form-control-sm item-qty-input"
|
|
data-id="${id}" value="${defaultCopies}" min="0" onchange="updateAveryPreview()" onkeyup="updateAveryPreview()">
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
table.appendChild(tbody);
|
|
container.appendChild(table);
|
|
}
|
|
}
|
|
|
|
modal.show();
|
|
updateAveryPreview();
|
|
};
|
|
|
|
window.updateAllItemQuantities = function() {
|
|
const globalQty = document.getElementById('averyCopies').value;
|
|
const itemInputs = document.querySelectorAll('.item-qty-input');
|
|
itemInputs.forEach(input => {
|
|
input.value = globalQty;
|
|
});
|
|
updateAveryPreview();
|
|
};
|
|
|
|
window.updateAveryPreview = function() {
|
|
const layout = document.getElementById('averyLayout').value;
|
|
const container = document.getElementById('averyPrintArea');
|
|
const checkedItems = document.querySelectorAll('.item-checkbox:checked');
|
|
|
|
container.className = 'avery-container avery-layout-' + layout;
|
|
container.innerHTML = '';
|
|
|
|
checkedItems.forEach(cb => {
|
|
const sku = cb.dataset.sku;
|
|
const name = cb.dataset.name;
|
|
const price = cb.dataset.price;
|
|
const id = cb.dataset.id;
|
|
|
|
// Find specific quantity input
|
|
const qtyInput = document.querySelector(`.item-qty-input[data-id="${id}"]`);
|
|
let copies = 1;
|
|
if (qtyInput) {
|
|
copies = parseInt(qtyInput.value) || 0;
|
|
} else {
|
|
copies = parseInt(document.getElementById('averyCopies').value) || 1;
|
|
}
|
|
|
|
for (let i = 0; i < copies; i++) {
|
|
const label = document.createElement('div');
|
|
label.className = 'avery-label';
|
|
const uniqueId = Math.random().toString(36).substr(2, 9);
|
|
const svgId = `bc-${sku}-${uniqueId}`;
|
|
label.innerHTML = `
|
|
<div style="font-size: 10px; font-weight: bold; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;">${name}</div>
|
|
<svg id="${svgId}"></svg>
|
|
<div style="font-size: 11px; font-weight: bold; margin-top: 2px;">OMR ${price}</div>
|
|
`;
|
|
container.appendChild(label);
|
|
|
|
setTimeout(() => {
|
|
if (document.getElementById(svgId)) {
|
|
const bcHeight = layout === 'L7651' ? 20 : 35;
|
|
JsBarcode(`#${svgId}`, sku, {
|
|
format: "CODE128",
|
|
width: layout === 'L7651' ? 1.0 : 1.2,
|
|
height: bcHeight,
|
|
displayValue: true,
|
|
fontSize: layout === 'L7651' ? 8 : 10,
|
|
margin: 0
|
|
});
|
|
}
|
|
}, 0);
|
|
}
|
|
});
|
|
};
|
|
|
|
window.addEventListener('beforeprint', () => {
|
|
if (document.getElementById('averyLabelsModal') && document.getElementById('averyLabelsModal').classList.contains('show')) {
|
|
document.body.classList.add('printing-avery');
|
|
}
|
|
});
|
|
window.addEventListener('afterprint', () => {
|
|
document.body.classList.remove('printing-avery');
|
|
});
|
|
</script>
|
|
|
|
|
|
|
|
<script>
|
|
window.printItemBarcode = function(sku, name, price) {
|
|
if (!sku) {
|
|
Swal.fire('Error', 'This item has no SKU/Barcode assigned.', 'error');
|
|
return;
|
|
}
|
|
document.getElementById('barcodeLabelName').textContent = name;
|
|
document.getElementById('barcodeLabelPrice').textContent = 'OMR ' + price;
|
|
|
|
JsBarcode("#barcodeSvg", sku, {
|
|
format: "CODE128",
|
|
lineColor: "#000",
|
|
width: 2,
|
|
height: 50,
|
|
displayValue: true
|
|
});
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('barcodePrintModal'));
|
|
modal.show();
|
|
};
|
|
|
|
window.executeBarcodePrint = function() {
|
|
const qty = parseInt(document.getElementById('barcodeQty').value) || 1;
|
|
const width = parseInt(document.getElementById('barcodeWidth').value) || 40;
|
|
const height = parseInt(document.getElementById('barcodeHeight').value) || 25;
|
|
|
|
// Get content
|
|
const name = document.getElementById('barcodeLabelName').innerText;
|
|
const price = document.getElementById('barcodeLabelPrice').innerText;
|
|
const svg = document.getElementById('barcodeSvg').outerHTML;
|
|
|
|
// Create a hidden iframe
|
|
const iframe = document.createElement('iframe');
|
|
iframe.style.position = 'absolute';
|
|
iframe.style.width = '0px';
|
|
iframe.style.height = '0px';
|
|
iframe.style.border = 'none';
|
|
document.body.appendChild(iframe);
|
|
|
|
const doc = iframe.contentWindow.document;
|
|
|
|
let labelsHtml = '';
|
|
for (let i = 0; i < qty; i++) {
|
|
labelsHtml += `
|
|
<div class="label-container">
|
|
<div class="label-name">${name}</div>
|
|
${svg}
|
|
<div class="label-price">${price}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
doc.open();
|
|
doc.write(`
|
|
<html>
|
|
<head>
|
|
<style>
|
|
@page { size: ${width}mm ${height}mm; margin: 0; }
|
|
body { margin: 0; padding: 0; font-family: sans-serif; }
|
|
.label-container {
|
|
width: ${width}mm;
|
|
height: ${height}mm;
|
|
page-break-after: always;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
text-align: center;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
padding: 1mm;
|
|
}
|
|
.label-container:last-child { page-break-after: avoid; }
|
|
.label-name { font-size: 10px; font-weight: bold; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
.label-price { font-size: 12px; font-weight: bold; margin-top: 2px; }
|
|
svg { max-width: 100%; height: auto; max-height: 70%; display: block; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
${labelsHtml}
|
|
</body>
|
|
</html>
|
|
`);
|
|
doc.close();
|
|
|
|
iframe.contentWindow.focus();
|
|
setTimeout(() => {
|
|
iframe.contentWindow.print();
|
|
setTimeout(() => {
|
|
document.body.removeChild(iframe);
|
|
}, 2000);
|
|
}, 500);
|
|
};
|
|
|
|
window.viewAndPrintA4Invoice = function(data, autoPrint = true) {
|
|
if (!data) return;
|
|
// Reuse view logic
|
|
document.getElementById('invNumber').textContent = 'INV-' + data.id.toString().padStart(5, '0');
|
|
document.getElementById('invDate').textContent = data.invoice_date;
|
|
document.getElementById('invPaymentType').textContent = data.payment_type ? data.payment_type.toUpperCase() : 'CASH';
|
|
document.getElementById('invCustomerName').textContent = data.customer_name || '---';
|
|
|
|
const phoneEl = document.getElementById('invCustomerPhone');
|
|
const phoneContainer = document.getElementById('invCustomerPhoneContainer');
|
|
if (data.customer_phone) {
|
|
phoneEl.textContent = data.customer_phone;
|
|
phoneContainer.style.display = 'block';
|
|
} else {
|
|
phoneContainer.style.display = 'none';
|
|
}
|
|
|
|
const taxIdEl = document.getElementById('invCustomerTaxId');
|
|
const taxIdContainer = document.getElementById('invCustomerTaxIdContainer');
|
|
if (data.customer_tax_id) {
|
|
taxIdEl.textContent = data.customer_tax_id;
|
|
taxIdContainer.style.display = 'block';
|
|
} else {
|
|
taxIdContainer.style.display = 'none';
|
|
}
|
|
|
|
document.getElementById('invAmountInWords').textContent = data.total_in_words || '';
|
|
|
|
document.getElementById('invPartyLabel').textContent = data.type === 'sale' ? 'Bill To' : 'Bill From';
|
|
document.getElementById('invPartyLabel').setAttribute('data-en', data.type === 'sale' ? 'Bill To' : 'Bill From');
|
|
document.getElementById('invPartyLabel').setAttribute('data-ar', data.type === 'sale' ? 'فاتورة إلى' : 'فاتورة من');
|
|
document.getElementById('invoiceTypeLabel').textContent = data.type;
|
|
document.getElementById('invoiceTypeLabel').className = 'badge text-uppercase ' + (data.type === 'sale' ? 'bg-success' : 'bg-warning');
|
|
|
|
const statusLabel = document.getElementById('invoiceStatusLabel');
|
|
let statusClass = 'bg-secondary';
|
|
let statusEn = data.status ? (data.status.charAt(0).toUpperCase() + data.status.slice(1)) : '---';
|
|
if (data.status === 'paid') statusClass = 'bg-success';
|
|
else if (data.status === 'unpaid') statusClass = 'bg-danger';
|
|
else if (data.status === 'partially_paid') {
|
|
statusClass = 'bg-warning text-dark';
|
|
statusEn = 'Partially Paid';
|
|
}
|
|
|
|
statusLabel.textContent = statusEn;
|
|
statusLabel.className = 'badge text-uppercase ' + statusClass;
|
|
|
|
const body = document.getElementById('invItemsBody');
|
|
body.innerHTML = '';
|
|
if (data.items) {
|
|
data.items.forEach(item => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${item.name_en} / ${item.name_ar}</td>
|
|
<td class="text-center">${item.quantity}</td>
|
|
<td class="text-end">OMR ${parseFloat(item.unit_price).toFixed(3)}</td>
|
|
<td class="text-end">${parseFloat(item.vat_rate || 0).toFixed(3)}%</td>
|
|
<td class="text-end">OMR ${parseFloat(item.total_price).toFixed(3)}</td>
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
}
|
|
if (document.getElementById('invSubtotal')) document.getElementById('invSubtotal').textContent = 'OMR ' + parseFloat(data.total_amount).toFixed(3);
|
|
if (document.getElementById('invVatAmount')) document.getElementById('invVatAmount').textContent = 'OMR ' + (parseFloat(data.vat_amount) || 0).toFixed(3);
|
|
const grandTotalValue = (parseFloat(data.total_with_vat) || parseFloat(data.total_amount));
|
|
if (document.getElementById('invGrandTotal')) document.getElementById('invGrandTotal').textContent = 'OMR ' + grandTotalValue.toFixed(3);
|
|
|
|
if (document.getElementById('invPaidInfo')) document.getElementById('invPaidInfo').textContent = 'OMR ' + parseFloat(data.paid_amount || 0).toFixed(3);
|
|
const balance = grandTotalValue - parseFloat(data.paid_amount || 0);
|
|
if (document.getElementById('invBalanceInfo')) document.getElementById('invBalanceInfo').textContent = 'OMR ' + balance.toFixed(3);
|
|
|
|
// Generate QR Code for Zakat, Tax and Customs Authority (ZATCA) style or simple formal
|
|
const companyName = <?= json_encode($data['settings']['company_name'] ?? 'Accounting System') ?>;
|
|
const vatNo = <?= json_encode($data['settings']['vat_number'] ?? '') ?>;
|
|
const qrData = `Seller: ${companyName}\nVAT: ${vatNo}\nInvoice: INV-${data.id.toString().padStart(5, '0')}\nDate: ${data.invoice_date}\nTotal: ${grandTotalValue.toFixed(3)}`;
|
|
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(qrData)}`;
|
|
if (document.getElementById('invQrCode')) {
|
|
document.getElementById('invQrCode').innerHTML = `<img src="${qrUrl}" alt="QR Code" style="width: 100px; height: 100px;" class="border p-1 bg-white">`;
|
|
}
|
|
|
|
const viewModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('viewInvoiceModal'));
|
|
viewModal.show();
|
|
|
|
if (autoPrint) {
|
|
setTimeout(() => { window.print(); }, 1000);
|
|
}
|
|
|
|
fetch(`index.php?action=get_payments&invoice_id=${data.id}`)
|
|
.then(res => res.json())
|
|
.then(payments => {
|
|
const paymentsBody = document.getElementById('invPaymentsBody');
|
|
const paymentsSection = document.getElementById('invPaymentsSection');
|
|
if (paymentsBody) paymentsBody.innerHTML = '';
|
|
if (payments && payments.length > 0) {
|
|
if (paymentsBody) {
|
|
payments.forEach(p => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `<td>${p.payment_date}</td><td>${p.payment_method}</td><td class="text-end fw-bold">OMR ${parseFloat(p.amount).toFixed(3)}</td>`;
|
|
paymentsBody.appendChild(tr);
|
|
});
|
|
}
|
|
if (paymentsSection) paymentsSection.style.display = 'block';
|
|
} else {
|
|
if (paymentsSection) paymentsSection.style.display = 'none';
|
|
}
|
|
}).catch(err => console.error('Error fetching payments:', err));
|
|
};
|
|
|
|
window.printPosReceiptFromInvoice = function(inv) {
|
|
const container = document.getElementById('posReceiptContent');
|
|
const itemsHtml = inv.items.map(item => `
|
|
<tr>
|
|
<td>${item.name_en}<br><small>${item.quantity} x ${parseFloat(item.unit_price).toFixed(3)}</small></td>
|
|
<td style="text-align: right; vertical-align: bottom;">${(item.unit_price * item.quantity).toFixed(3)}</td>
|
|
</tr>
|
|
`).join('');
|
|
|
|
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>";
|
|
const companyPhone = "<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>";
|
|
const companyVat = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>";
|
|
|
|
container.innerHTML = `
|
|
<div class="thermal-receipt">
|
|
<div class="center">
|
|
<h5 class="mb-0 fw-bold">${companyName}</h5>
|
|
${companyPhone ? `<div>Tel: ${companyPhone}</div>` : ''}
|
|
${companyVat ? `<div>VAT: ${companyVat}</div>` : ''}
|
|
<div class="separator"></div>
|
|
<h6 class="fw-bold">TAX INVOICE</h6>
|
|
<div>Inv: INV-${inv.id.toString().padStart(5, '0')}</div>
|
|
<div>Date: ${inv.invoice_date}</div>
|
|
<div class="separator"></div>
|
|
</div>
|
|
<div>
|
|
<strong>Customer:</strong> ${inv.customer_name || 'Walk-in'}
|
|
</div>
|
|
<div class="separator"></div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ITEM</th>
|
|
<th style="text-align: right;">TOTAL</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${itemsHtml}
|
|
</tbody>
|
|
</table>
|
|
<div class="separator"></div>
|
|
<div class="total-row d-flex justify-content-between">
|
|
<span>TOTAL</span>
|
|
<span>OMR ${parseFloat(inv.total_with_vat).toFixed(3)}</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between small">
|
|
<span>PAID</span>
|
|
<span>OMR ${parseFloat(inv.paid_amount).toFixed(3)}</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between small fw-bold">
|
|
<span>BALANCE</span>
|
|
<span>OMR ${(inv.total_with_vat - inv.paid_amount).toFixed(3)}</span>
|
|
</div>
|
|
<div class="separator"></div>
|
|
<div class="center small">
|
|
<p>Thank You for your business!</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const posModal = new bootstrap.Modal(document.getElementById('posReceiptModal'));
|
|
posModal.show();
|
|
};
|
|
|
|
function printPosReceipt() {
|
|
const content = document.getElementById('posReceiptContent').innerHTML;
|
|
const printArea = document.getElementById('posPrintArea');
|
|
printArea.innerHTML = `<div class="thermal-receipt thermal-receipt-print">${content}</div>`;
|
|
|
|
document.body.classList.add('printing-receipt');
|
|
window.print();
|
|
document.body.classList.remove('printing-receipt');
|
|
location.reload();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|