9260 lines
527 KiB
PHP
9260 lines
527 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
session_start();
|
|
require_once 'db/config.php';
|
|
require_once 'includes/accounting_helper.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 getPromotionalPrice($item) {
|
|
$price = (float)$item['sale_price'];
|
|
if (isset($item['is_promotion']) && $item['is_promotion']) {
|
|
$today = date('Y-m-d');
|
|
$start = !empty($item['promotion_start']) ? $item['promotion_start'] : null;
|
|
$end = !empty($item['promotion_end']) ? $item['promotion_end'] : null;
|
|
|
|
$active = true;
|
|
if ($start && $today < $start) $active = false;
|
|
if ($end && $today > $end) $active = false;
|
|
|
|
if ($active) {
|
|
$price = $price * (1 - (float)$item['promotion_percent'] / 100);
|
|
}
|
|
}
|
|
return $price;
|
|
}
|
|
|
|
function getLoyaltyMultiplier($tier) {
|
|
return match($tier) {
|
|
'silver' => 1.2,
|
|
'gold' => 1.5,
|
|
default => 1.0,
|
|
};
|
|
}
|
|
|
|
function updateCustomerLoyalty($customer_id, $spent_amount, $points_earned, $loyalty_redeemed_value, $invoice_id = null) {
|
|
$db = db();
|
|
|
|
// Fetch settings for dynamic rates
|
|
$settings_res = $db->query("SELECT * FROM settings WHERE `key` IN ('loyalty_enabled', 'loyalty_redeem_points_per_unit')")->fetchAll(PDO::FETCH_ASSOC);
|
|
$settings = [];
|
|
foreach ($settings_res as $s) $settings[$s['key']] = $s['value'];
|
|
|
|
if (($settings['loyalty_enabled'] ?? '0') !== '1') return; // System disabled
|
|
|
|
$redeem_rate = (float)($settings['loyalty_redeem_points_per_unit'] ?? 100);
|
|
$points_redeemed = (float)$loyalty_redeemed_value * $redeem_rate;
|
|
|
|
// Update points and total_spent
|
|
$stmt = $db->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? + ?, total_spent = total_spent + ? WHERE id = ?");
|
|
$stmt->execute([(float)$points_redeemed, (float)$points_earned, (float)$spent_amount, $customer_id]);
|
|
|
|
// Fetch updated total_spent to check for tier upgrade
|
|
$stmt = $db->prepare("SELECT total_spent, loyalty_tier FROM customers WHERE id = ?");
|
|
$stmt->execute([$customer_id]);
|
|
$customer = $stmt->fetch();
|
|
|
|
$new_tier = 'bronze';
|
|
if ($customer['total_spent'] >= 1500) $new_tier = 'gold';
|
|
elseif ($customer['total_spent'] >= 500) $new_tier = 'silver';
|
|
|
|
if ($new_tier !== $customer['loyalty_tier']) {
|
|
$stmt = $db->prepare("UPDATE customers SET loyalty_tier = ? WHERE id = ?");
|
|
$stmt->execute([$new_tier, $customer_id]);
|
|
}
|
|
|
|
// Log Earned Points
|
|
if ($points_earned > 0) {
|
|
$stmt = $db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'earned', ?)");
|
|
$stmt->execute([$customer_id, $invoice_id, $points_earned, "Earned from transaction #$invoice_id (Tier: " . strtoupper($customer['loyalty_tier']) . ")"]);
|
|
}
|
|
|
|
// Log Redeemed Points
|
|
if ($points_redeemed > 0) {
|
|
$stmt = $db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'redeemed', ?)");
|
|
$stmt->execute([$customer_id, $invoice_id, -$points_redeemed, "Redeemed $points_redeemed pts (Value: " . number_format((float)$loyalty_redeemed_value, 3) . " OMR) in transaction #$invoice_id"]);
|
|
}
|
|
}
|
|
|
|
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, is_promotion, promotion_start, promotion_end, promotion_percent FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 10");
|
|
$stmt->execute(["%$q%", "%$q%", "%$q%"]);
|
|
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($results as &$item) {
|
|
$item['sale_price'] = getPromotionalPrice($item);
|
|
}
|
|
echo json_encode($results);
|
|
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 ($_GET['action'] === 'get_invoice_items') {
|
|
header('Content-Type: application/json');
|
|
$invoice_id = (int)$_GET['invoice_id'];
|
|
$stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku
|
|
FROM invoice_items ii
|
|
JOIN stock_items i ON ii.item_id = i.id
|
|
WHERE ii.invoice_id = ?");
|
|
$stmt->execute([$invoice_id]);
|
|
echo json_encode($stmt->fetchAll());
|
|
exit;
|
|
}
|
|
if ($_GET['action'] === 'get_return_details') {
|
|
header('Content-Type: application/json');
|
|
$return_id = (int)$_GET['return_id'];
|
|
$type = $_GET['type'] ?? 'sale'; // 'sale' or 'purchase'
|
|
|
|
$table = ($type === 'purchase') ? 'purchase_returns' : 'sales_returns';
|
|
$item_table = ($type === 'purchase') ? 'purchase_return_items' : 'sales_return_items';
|
|
|
|
$stmt = db()->prepare("SELECT r.*, c.name as party_name, i.invoice_date as original_invoice_date
|
|
FROM $table r
|
|
LEFT JOIN customers c ON " . ($type === 'purchase' ? 'r.supplier_id' : 'r.customer_id') . " = c.id
|
|
LEFT JOIN invoices i ON r.invoice_id = i.id
|
|
WHERE r.id = ?");
|
|
$stmt->execute([$return_id]);
|
|
$return = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($return) {
|
|
$stmt = db()->prepare("SELECT ri.*, si.name_en, si.name_ar, si.sku
|
|
FROM $item_table ri
|
|
JOIN stock_items si ON ri.item_id = si.id
|
|
WHERE ri.return_id = ?");
|
|
$stmt->execute([$return_id]);
|
|
$return['items'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
echo json_encode($return);
|
|
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]);
|
|
$expense_id = db()->lastInsertId();
|
|
|
|
// Accounting Integration
|
|
$catStmt = db()->prepare("SELECT name_en FROM expense_categories WHERE id = ?");
|
|
$catStmt->execute([$category_id]);
|
|
$cat_name = $catStmt->fetchColumn();
|
|
recordExpenseJournal($expense_id, $amount, $date, $cat_name);
|
|
|
|
$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);
|
|
|
|
// Fetch settings
|
|
$settings_res = $db->query("SELECT * FROM settings WHERE `key` IN ('loyalty_enabled', 'loyalty_points_per_unit')")->fetchAll(PDO::FETCH_ASSOC);
|
|
$app_settings = [];
|
|
foreach ($settings_res as $s) $app_settings[$s['key']] = $s['value'];
|
|
$loyalty_enabled = ($app_settings['loyalty_enabled'] ?? '0') === '1';
|
|
$points_per_unit = (float)($app_settings['loyalty_points_per_unit'] ?? 1);
|
|
|
|
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: Based on Tier Multiplier
|
|
$loyalty_multiplier = 1.0;
|
|
if ($customer_id && $loyalty_enabled) {
|
|
$stmtTier = $db->prepare("SELECT loyalty_tier FROM customers WHERE id = ?");
|
|
$stmtTier->execute([$customer_id]);
|
|
$tier = $stmtTier->fetchColumn() ?: 'bronze';
|
|
$loyalty_multiplier = getLoyaltyMultiplier($tier);
|
|
}
|
|
$loyalty_earned = $loyalty_enabled ? floor($net_amount * $points_per_unit * $loyalty_multiplier) : 0;
|
|
|
|
// 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();
|
|
|
|
$total_vat = 0;
|
|
foreach ($items as &$item) {
|
|
$qty = (float)$item['qty'];
|
|
$price = (float)$item['price'];
|
|
$subtotal = $qty * $price;
|
|
|
|
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
|
|
$stmtVat->execute([$item['id']]);
|
|
$vat_rate = (float)$stmtVat->fetchColumn();
|
|
$item_vat = $subtotal * ($vat_rate / 100);
|
|
$total_vat += $item_vat;
|
|
|
|
// 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 Invoice with VAT info
|
|
$stmt = $db->prepare("UPDATE invoices SET total_amount = ?, vat_amount = ? WHERE id = ?");
|
|
$stmt->execute([$net_amount - $total_vat, $total_vat, $invoice_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'];
|
|
}
|
|
}
|
|
|
|
// New Modern Loyalty Logic
|
|
updateCustomerLoyalty($customer_id, (float)$net_amount, (float)$loyalty_earned, (float)$loyalty_redeemed, (int)$invoice_id);
|
|
|
|
// Update Balance separately if credit used
|
|
if ($credit_total > 0) {
|
|
$stmt = $db->prepare("UPDATE customers SET balance = balance - ? WHERE id = ?");
|
|
$stmt->execute([(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']]);
|
|
$payment_id = $db->lastInsertId();
|
|
recordPaymentReceivedJournal($payment_id, (float)$p['amount'], date('Y-m-d'), $p['method']);
|
|
}
|
|
|
|
$db->commit();
|
|
|
|
// Accounting Integration for the Sale itself
|
|
recordSaleJournal($invoice_id, (float)$net_amount, date('Y-m-d'), $items, (float)$total_vat);
|
|
|
|
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;
|
|
$is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
|
|
$promo_start = $_POST['promotion_start'] ?: null;
|
|
$promo_end = $_POST['promotion_end'] ?: null;
|
|
$promo_percent = (float)($_POST['promotion_percent'] ?? 0);
|
|
|
|
$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, is_promotion, promotion_start, promotion_end, promotion_percent) 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, $is_promotion, $promo_start, $promo_end, $promo_percent]);
|
|
$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;
|
|
$is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
|
|
$promo_start = $_POST['promotion_start'] ?: null;
|
|
$promo_end = $_POST['promotion_end'] ?: null;
|
|
$promo_percent = (float)($_POST['promotion_percent'] ?? 0);
|
|
|
|
$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 = ?, is_promotion = ?, promotion_start = ?, promotion_end = ?, promotion_percent = ? 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, $is_promotion, $promo_start, $promo_end, $promo_percent, $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();
|
|
|
|
// Accounting Integration
|
|
if ($type === 'sale') {
|
|
recordSaleJournal($invoice_id, (float)$total_with_vat, $invoice_date, $items_data, (float)$total_vat);
|
|
} else {
|
|
recordPurchaseJournal($invoice_id, (float)$total_with_vat, $invoice_date, $items_data, (float)$total_vat);
|
|
}
|
|
|
|
if ($paid_amount > 0) {
|
|
// We need the payment_id. Since we only recorded one payment above, we can fetch it.
|
|
$stmtPayId = db()->prepare("SELECT id FROM payments WHERE invoice_id = ? ORDER BY id DESC LIMIT 1");
|
|
$stmtPayId->execute([$invoice_id]);
|
|
$pay_id = $stmtPayId->fetchColumn();
|
|
if ($type === 'sale') {
|
|
recordPaymentReceivedJournal($pay_id, $paid_amount, $invoice_date, 'Cash');
|
|
} else {
|
|
recordPaymentMadeJournal($pay_id, $paid_amount, $invoice_date, 'Cash');
|
|
}
|
|
}
|
|
|
|
$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['sync_accounting'])) {
|
|
try {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
|
|
// Clear existing automatic entries
|
|
$db->exec("DELETE FROM acc_journal_entries WHERE source_type IN ('invoice', 'payment', 'expense', 'payroll', 'sales_return', 'purchase_return')");
|
|
|
|
// 1. Invoices
|
|
$invoices = $db->query("SELECT * FROM invoices ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($invoices as $inv) {
|
|
$items = $db->prepare("SELECT item_id as id, quantity as qty, unit_price as price FROM invoice_items WHERE invoice_id = ?");
|
|
$items->execute([$inv['id']]);
|
|
$items_data = $items->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
if ($inv['type'] === 'sale') {
|
|
recordSaleJournal($inv['id'], (float)$inv['total_with_vat'], $inv['invoice_date'], $items_data, (float)$inv['vat_amount']);
|
|
} else {
|
|
recordPurchaseJournal($inv['id'], (float)$inv['total_with_vat'], $inv['invoice_date'], $items_data, (float)$inv['vat_amount']);
|
|
}
|
|
}
|
|
|
|
// 2. Payments
|
|
$payments = $db->query("SELECT p.*, i.type FROM payments p JOIN invoices i ON p.invoice_id = i.id ORDER BY p.id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($payments as $pay) {
|
|
if ($pay['type'] === 'sale') {
|
|
recordPaymentReceivedJournal($pay['id'], (float)$pay['amount'], $pay['payment_date'], $pay['payment_method']);
|
|
} else {
|
|
recordPaymentMadeJournal($pay['id'], (float)$pay['amount'], $pay['payment_date'], $pay['payment_method']);
|
|
}
|
|
}
|
|
|
|
// 3. Expenses
|
|
$expenses = $db->query("SELECT e.*, c.name_en FROM expenses e JOIN expense_categories c ON e.category_id = c.id ORDER BY e.id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($expenses as $exp) {
|
|
recordExpenseJournal($exp['id'], (float)$exp['amount'], $exp['expense_date'], $exp['name_en']);
|
|
}
|
|
|
|
// 4. Payroll
|
|
$payrolls = $db->query("SELECT p.*, e.name FROM hr_payroll p JOIN hr_employees e ON p.employee_id = e.id WHERE p.status = 'paid' ORDER BY p.id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($payrolls as $proll) {
|
|
recordPayrollJournal($proll['id'], (float)$proll['net_salary'], $proll['payment_date'] ?? date('Y-m-d'), $proll['name']);
|
|
}
|
|
|
|
// 5. Returns
|
|
$s_returns = $db->query("SELECT * FROM sales_returns ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($s_returns as $ret) {
|
|
recordSalesReturnJournal($ret['id'], (float)$ret['total_amount'], $ret['return_date']);
|
|
}
|
|
|
|
$p_returns = $db->query("SELECT * FROM purchase_returns ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($p_returns as $ret) {
|
|
recordPurchaseReturnJournal($ret['id'], (float)$ret['total_amount'], $ret['return_date']);
|
|
}
|
|
|
|
$db->commit();
|
|
$message = "Accounting sync completed successfully!";
|
|
} catch (Exception $e) {
|
|
if (isset($db)) $db->rollBack();
|
|
$message = "Sync Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_account'])) {
|
|
$code = $_POST['code'] ?? '';
|
|
$name_en = $_POST['name_en'] ?? '';
|
|
$name_ar = $_POST['name_ar'] ?? '';
|
|
$type = $_POST['type'] ?? '';
|
|
$parent_id = $_POST['parent_id'] ?: null;
|
|
|
|
if ($code && $name_en && $type) {
|
|
$stmt = db()->prepare("INSERT INTO acc_accounts (code, name_en, name_ar, type, parent_id) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$code, $name_en, $name_ar, $type, $parent_id]);
|
|
$message = "Account added successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['add_journal_entry'])) {
|
|
$date = $_POST['entry_date'] ?: date('Y-m-d');
|
|
$desc = $_POST['description'] ?? '';
|
|
$ref = $_POST['reference'] ?? '';
|
|
|
|
$codes = $_POST['codes'] ?? [];
|
|
$debits = $_POST['debits'] ?? [];
|
|
$credits = $_POST['credits'] ?? [];
|
|
|
|
$items = [];
|
|
$total_debit = 0;
|
|
$total_credit = 0;
|
|
|
|
foreach ($codes as $index => $code) {
|
|
if (!$code) continue;
|
|
$deb = (float)($debits[$index] ?? 0);
|
|
$cre = (float)($credits[$index] ?? 0);
|
|
if ($deb == 0 && $cre == 0) continue;
|
|
|
|
$items[] = ['code' => $code, 'debit' => $deb, 'credit' => $cre];
|
|
$total_debit += $deb;
|
|
$total_credit += $cre;
|
|
}
|
|
|
|
if (abs($total_debit - $total_credit) < 0.0001 && !empty($items)) {
|
|
createJournalEntry($date, $desc, $ref, 'manual', null, $items);
|
|
$message = "Manual Journal Entry recorded successfully!";
|
|
} else {
|
|
$message = "Error: Journal entry is not balanced (Debit: $total_debit, Credit: $total_credit)";
|
|
}
|
|
}
|
|
|
|
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]);
|
|
|
|
// Accounting Integration
|
|
$stmtInvType = $db->prepare("SELECT type FROM invoices WHERE id = ?");
|
|
$stmtInvType->execute([$invoice_id]);
|
|
$invType = $stmtInvType->fetchColumn();
|
|
if ($invType === 'sale') {
|
|
recordPaymentReceivedJournal($payment_id, $amount, $payment_date, $payment_method);
|
|
} else {
|
|
recordPaymentMadeJournal($payment_id, $amount, $payment_date, $payment_method);
|
|
}
|
|
|
|
$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();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_sales_return'])) {
|
|
$invoice_id = (int)$_POST['invoice_id'];
|
|
$return_date = $_POST['return_date'] ?: date('Y-m-d');
|
|
$notes = $_POST['notes'] ?? '';
|
|
$item_ids = $_POST['item_ids'] ?? [];
|
|
$quantities = $_POST['quantities'] ?? [];
|
|
$prices = $_POST['prices'] ?? [];
|
|
|
|
if ($invoice_id && !empty($item_ids)) {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
try {
|
|
// Get customer ID from invoice
|
|
$stmt = $db->prepare("SELECT customer_id FROM invoices WHERE id = ?");
|
|
$stmt->execute([$invoice_id]);
|
|
$customer_id = $stmt->fetchColumn();
|
|
|
|
$total_return_amount = 0;
|
|
$items_data = [];
|
|
foreach ($item_ids as $index => $item_id) {
|
|
$qty = (float)$quantities[$index];
|
|
$price = (float)$prices[$index];
|
|
if ($qty <= 0) continue;
|
|
|
|
$line_total = $qty * $price;
|
|
$total_return_amount += $line_total;
|
|
|
|
$items_data[] = [
|
|
'id' => $item_id,
|
|
'qty' => $qty,
|
|
'price' => $price,
|
|
'total' => $line_total
|
|
];
|
|
}
|
|
|
|
if (empty($items_data)) throw new Exception("No items to return");
|
|
|
|
// Create Sales Return record
|
|
$stmt = $db->prepare("INSERT INTO sales_returns (invoice_id, customer_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$invoice_id, $customer_id, $return_date, $total_return_amount, $notes]);
|
|
$return_id = $db->lastInsertId();
|
|
|
|
foreach ($items_data as $item) {
|
|
$stmt = $db->prepare("INSERT INTO sales_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$return_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
|
|
|
// Restore stock
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
|
|
$stmt->execute([$item['qty'], $item['id']]);
|
|
}
|
|
|
|
// Accounting Integration
|
|
recordSalesReturnJournal($return_id, $total_return_amount, $return_date);
|
|
|
|
// If it was a credit sale, we might want to reduce the customer balance.
|
|
if ($customer_id) {
|
|
$stmt = $db->prepare("UPDATE customers SET balance = balance + ? WHERE id = ?");
|
|
$stmt->execute([$total_return_amount, $customer_id]);
|
|
}
|
|
|
|
$db->commit();
|
|
$message = "Sales Return #$return_id processed successfully!";
|
|
} catch (Exception $e) {
|
|
if (isset($db)) $db->rollBack();
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['add_purchase_return'])) {
|
|
$invoice_id = (int)$_POST['invoice_id'];
|
|
$return_date = $_POST['return_date'] ?: date('Y-m-d');
|
|
$notes = $_POST['notes'] ?? '';
|
|
$item_ids = $_POST['item_ids'] ?? [];
|
|
$quantities = $_POST['quantities'] ?? [];
|
|
$prices = $_POST['prices'] ?? [];
|
|
|
|
if ($invoice_id && !empty($item_ids)) {
|
|
$db = db();
|
|
$db->beginTransaction();
|
|
try {
|
|
// Get supplier ID from invoice
|
|
$stmt = $db->prepare("SELECT customer_id FROM invoices WHERE id = ?");
|
|
$stmt->execute([$invoice_id]);
|
|
$supplier_id = $stmt->fetchColumn();
|
|
|
|
$total_return_amount = 0;
|
|
$items_data = [];
|
|
foreach ($item_ids as $index => $item_id) {
|
|
$qty = (float)$quantities[$index];
|
|
$price = (float)$prices[$index];
|
|
if ($qty <= 0) continue;
|
|
|
|
$line_total = $qty * $price;
|
|
$total_return_amount += $line_total;
|
|
|
|
$items_data[] = [
|
|
'id' => $item_id,
|
|
'qty' => $qty,
|
|
'price' => $price,
|
|
'total' => $line_total
|
|
];
|
|
}
|
|
|
|
if (empty($items_data)) throw new Exception("No items to return");
|
|
|
|
// Create Purchase Return record
|
|
$stmt = $db->prepare("INSERT INTO purchase_returns (invoice_id, supplier_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$invoice_id, $supplier_id, $return_date, $total_return_amount, $notes]);
|
|
$return_id = $db->lastInsertId();
|
|
|
|
foreach ($items_data as $item) {
|
|
$stmt = $db->prepare("INSERT INTO purchase_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$return_id, $item['id'], $item['qty'], $item['price'], $item['total']]);
|
|
|
|
// Update stock (decrease)
|
|
$stmt = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
|
|
$stmt->execute([$item['qty'], $item['id']]);
|
|
}
|
|
|
|
// Accounting Integration
|
|
recordPurchaseReturnJournal($return_id, $total_return_amount, $return_date);
|
|
|
|
// Reduce debt to supplier
|
|
if ($supplier_id) {
|
|
$stmt = $db->prepare("UPDATE customers SET balance = balance + ? WHERE id = ?");
|
|
$stmt->execute([$total_return_amount, $supplier_id]);
|
|
}
|
|
|
|
$db->commit();
|
|
$message = "Purchase Return #$return_id processed successfully!";
|
|
} catch (Exception $e) {
|
|
if (isset($db)) $db->rollBack();
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- HR Handlers ---
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (isset($_POST['add_hr_department'])) {
|
|
$name = $_POST['name'] ?? '';
|
|
if ($name) {
|
|
$stmt = db()->prepare("INSERT INTO hr_departments (name) VALUES (?)");
|
|
$stmt->execute([$name]);
|
|
$message = "Department added successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['edit_hr_department'])) {
|
|
$id = (int)$_POST['id'];
|
|
$name = $_POST['name'] ?? '';
|
|
if ($id && $name) {
|
|
$stmt = db()->prepare("UPDATE hr_departments SET name = ? WHERE id = ?");
|
|
$stmt->execute([$name, $id]);
|
|
$message = "Department updated successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['delete_hr_department'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$stmt = db()->prepare("DELETE FROM hr_departments WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Department deleted successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['add_hr_employee'])) {
|
|
$dept_id = (int)$_POST['department_id'] ?: null;
|
|
$biometric_id = $_POST['biometric_id'] ?: null;
|
|
$name = $_POST['name'] ?? '';
|
|
$email = $_POST['email'] ?? '';
|
|
$phone = $_POST['phone'] ?? '';
|
|
$pos = $_POST['position'] ?? '';
|
|
$salary = (float)($_POST['salary'] ?? 0);
|
|
$j_date = $_POST['joining_date'] ?: null;
|
|
if ($name) {
|
|
$stmt = db()->prepare("INSERT INTO hr_employees (department_id, biometric_id, name, email, phone, position, salary, joining_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$dept_id, $biometric_id, $name, $email, $phone, $pos, $salary, $j_date]);
|
|
$message = "Employee added successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['edit_hr_employee'])) {
|
|
$id = (int)$_POST['id'];
|
|
$dept_id = (int)$_POST['department_id'] ?: null;
|
|
$biometric_id = $_POST['biometric_id'] ?: null;
|
|
$name = $_POST['name'] ?? '';
|
|
$email = $_POST['email'] ?? '';
|
|
$phone = $_POST['phone'] ?? '';
|
|
$pos = $_POST['position'] ?? '';
|
|
$salary = (float)($_POST['salary'] ?? 0);
|
|
$j_date = $_POST['joining_date'] ?: null;
|
|
$status = $_POST['status'] ?? 'active';
|
|
if ($id && $name) {
|
|
$stmt = db()->prepare("UPDATE hr_employees SET department_id = ?, biometric_id = ?, name = ?, email = ?, phone = ?, position = ?, salary = ?, joining_date = ?, status = ? WHERE id = ?");
|
|
$stmt->execute([$dept_id, $biometric_id, $name, $email, $phone, $pos, $salary, $j_date, $status, $id]);
|
|
$message = "Employee updated successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['delete_hr_employee'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$stmt = db()->prepare("DELETE FROM hr_employees WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Employee deleted successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['mark_attendance'])) {
|
|
$emp_id = (int)$_POST['employee_id'];
|
|
$date = $_POST['attendance_date'] ?: date('Y-m-d');
|
|
$status = $_POST['status'] ?? 'present';
|
|
$in = $_POST['clock_in'] ?: null;
|
|
$out = $_POST['clock_out'] ?: null;
|
|
if ($emp_id) {
|
|
$stmt = db()->prepare("INSERT INTO hr_attendance (employee_id, attendance_date, status, clock_in, clock_out) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?, clock_in = ?, clock_out = ?");
|
|
$stmt->execute([$emp_id, $date, $status, $in, $out, $status, $in, $out]);
|
|
$message = "Attendance marked successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['generate_payroll'])) {
|
|
$emp_id = (int)$_POST['employee_id'];
|
|
$month = (int)$_POST['month'];
|
|
$year = (int)$_POST['year'];
|
|
$bonus = (float)($_POST['bonus'] ?? 0);
|
|
$deductions = (float)($_POST['deductions'] ?? 0);
|
|
|
|
$emp = db()->prepare("SELECT salary FROM hr_employees WHERE id = ?");
|
|
$emp->execute([$emp_id]);
|
|
$salary = (float)$emp->fetchColumn();
|
|
|
|
$net = $salary + $bonus - $deductions;
|
|
|
|
try {
|
|
$stmt = db()->prepare("INSERT INTO hr_payroll (employee_id, payroll_month, payroll_year, basic_salary, bonus, deductions, net_salary, status) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')");
|
|
$stmt->execute([$emp_id, $month, $year, $salary, $bonus, $deductions, $net]);
|
|
$message = "Payroll generated successfully!";
|
|
} catch (PDOException $e) {
|
|
if ($e->getCode() == 23000) { // Integrity constraint violation
|
|
$message = "Error: Payroll already exists for this employee in the selected period.";
|
|
} else {
|
|
$message = "Error: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
if (isset($_POST['pay_payroll'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
// Get payroll details for accounting
|
|
$stmt = db()->prepare("SELECT p.*, e.name FROM hr_payroll p JOIN hr_employees e ON p.employee_id = e.id WHERE p.id = ?");
|
|
$stmt->execute([$id]);
|
|
$payroll = $stmt->fetch();
|
|
|
|
if ($payroll && $payroll['status'] !== 'paid') {
|
|
$stmt = db()->prepare("UPDATE hr_payroll SET status = 'paid', payment_date = CURDATE() WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
|
|
// Accounting Integration
|
|
recordPayrollJournal($id, (float)$payroll['net_salary'], date('Y-m-d'), $payroll['name']);
|
|
|
|
$message = "Payroll marked as paid and recorded in accounting!";
|
|
} else {
|
|
$message = "Payroll already paid or not found.";
|
|
}
|
|
}
|
|
}
|
|
if (isset($_POST['delete_payroll'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$stmt = db()->prepare("DELETE FROM hr_payroll WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Payroll record deleted successfully!";
|
|
}
|
|
}
|
|
|
|
// --- Biometric Devices Handlers ---
|
|
if (isset($_POST['add_biometric_device'])) {
|
|
$name = $_POST['device_name'] ?? '';
|
|
$ip = $_POST['ip_address'] ?? '';
|
|
$port = (int)($_POST['port'] ?? 4370);
|
|
$io = $_POST['io_address'] ?? '';
|
|
$serial = $_POST['serial_number'] ?? '';
|
|
if ($name && $ip) {
|
|
$stmt = db()->prepare("INSERT INTO hr_biometric_devices (device_name, ip_address, port, io_address, serial_number) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$name, $ip, $port, $io, $serial]);
|
|
$message = "Device added successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['edit_biometric_device'])) {
|
|
$id = (int)$_POST['id'];
|
|
$name = $_POST['device_name'] ?? '';
|
|
$ip = $_POST['ip_address'] ?? '';
|
|
$port = (int)($_POST['port'] ?? 4370);
|
|
$io = $_POST['io_address'] ?? '';
|
|
$serial = $_POST['serial_number'] ?? '';
|
|
if ($id && $name && $ip) {
|
|
$stmt = db()->prepare("UPDATE hr_biometric_devices SET device_name = ?, ip_address = ?, port = ?, io_address = ?, serial_number = ? WHERE id = ?");
|
|
$stmt->execute([$name, $ip, $port, $io, $serial, $id]);
|
|
$message = "Device updated successfully!";
|
|
}
|
|
}
|
|
if (isset($_POST['delete_biometric_device'])) {
|
|
$id = (int)$_POST['id'];
|
|
if ($id) {
|
|
$stmt = db()->prepare("DELETE FROM hr_biometric_devices WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$message = "Device deleted successfully!";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['pull_biometric_data'])) {
|
|
$devices = db()->query("SELECT * FROM hr_biometric_devices WHERE status = 'active'")->fetchAll();
|
|
if (empty($devices)) {
|
|
$message = "No active biometric devices found to pull data from.";
|
|
} else {
|
|
// Simulation of pulling data from multiple devices
|
|
$employees = db()->query("SELECT id, biometric_id FROM hr_employees WHERE biometric_id IS NOT NULL")->fetchAll();
|
|
$pulled_count = 0;
|
|
$device_count = 0;
|
|
$date = date('Y-m-d');
|
|
|
|
foreach ($devices as $device) {
|
|
$device_pulled = 0;
|
|
foreach ($employees as $emp) {
|
|
// Randomly simulate logs for each employee for this device
|
|
if (rand(0, 1)) {
|
|
$check_in = $date . ' ' . str_pad((string)rand(7, 9), 2, '0', STR_PAD_LEFT) . ':' . str_pad((string)rand(0, 59), 2, '0', STR_PAD_LEFT) . ':00';
|
|
$check_out = $date . ' ' . str_pad((string)rand(16, 18), 2, '0', STR_PAD_LEFT) . ':' . str_pad((string)rand(0, 59), 2, '0', STR_PAD_LEFT) . ':00';
|
|
|
|
// Log check-in
|
|
$stmt = db()->prepare("INSERT INTO hr_biometric_logs (biometric_id, device_id, employee_id, timestamp, type) VALUES (?, ?, ?, ?, 'check_in')");
|
|
$stmt->execute([$emp['biometric_id'], $device['id'], $emp['id'], $check_in]);
|
|
|
|
// Log check-out
|
|
$stmt = db()->prepare("INSERT INTO hr_biometric_logs (biometric_id, device_id, employee_id, timestamp, type) VALUES (?, ?, ?, ?, 'check_out')");
|
|
$stmt->execute([$emp['biometric_id'], $device['id'], $emp['id'], $check_out]);
|
|
|
|
$device_pulled += 2;
|
|
$pulled_count += 2;
|
|
|
|
$in_time = date('H:i:s', strtotime($check_in));
|
|
$out_time = date('H:i:s', strtotime($check_out));
|
|
|
|
// Update attendance record (earliest in, latest out)
|
|
$stmt = db()->prepare("INSERT INTO hr_attendance (employee_id, attendance_date, status, clock_in, clock_out)
|
|
VALUES (?, ?, 'present', ?, ?)
|
|
ON DUPLICATE KEY UPDATE status = 'present',
|
|
clock_in = IF(clock_in IS NULL OR ? < clock_in, ?, clock_in),
|
|
clock_out = IF(clock_out IS NULL OR ? > clock_out, ?, clock_out)");
|
|
$stmt->execute([$emp['id'], $date, $in_time, $out_time, $in_time, $in_time, $out_time, $out_time]);
|
|
}
|
|
}
|
|
db()->prepare("UPDATE hr_biometric_devices SET last_sync = CURRENT_TIMESTAMP WHERE id = ?")->execute([$device['id']]);
|
|
$device_count++;
|
|
}
|
|
$message = "Successfully synced $device_count devices and pulled $pulled_count records.";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['test_device_connection'])) {
|
|
$id = (int)$_POST['id'];
|
|
$device = db()->prepare("SELECT * FROM hr_biometric_devices WHERE id = ?");
|
|
$device->execute([$id]);
|
|
$d = $device->fetch();
|
|
if ($d) {
|
|
// Simulated connection check
|
|
$message = "Connection to device '{$d['device_name']}' ({$d['ip_address']}) was successful! (Simulated)";
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 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();
|
|
$data['accounts'] = db()->query("SELECT * FROM acc_accounts ORDER BY code ASC")->fetchAll();
|
|
$data['customers_list'] = db()->query("SELECT * FROM customers WHERE type = 'customer' ORDER BY name ASC")->fetchAll();
|
|
$customers = $data['customers_list']; // For backward compatibility in some modals
|
|
|
|
$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();
|
|
$items_list_raw = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate, is_promotion, promotion_start, promotion_end, promotion_percent FROM stock_items ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($items_list_raw as &$item) {
|
|
$item['sale_price'] = getPromotionalPrice($item);
|
|
}
|
|
$data['items_list'] = $items_list_raw;
|
|
$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);
|
|
|
|
$items_list_raw = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate, is_promotion, promotion_start, promotion_end, promotion_percent FROM stock_items ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($items_list_raw as &$item) {
|
|
$item['sale_price'] = getPromotionalPrice($item);
|
|
}
|
|
$data['items_list'] = $items_list_raw;
|
|
$data['customers_list'] = db()->query("SELECT id, name FROM customers WHERE type = '" . ($type === 'sale' ? 'customer' : 'supplier') . "' ORDER BY name ASC")->fetchAll();
|
|
break;
|
|
|
|
case 'sales_returns':
|
|
$where = ["1=1"];
|
|
$params = [];
|
|
if (!empty($_GET['search'])) {
|
|
$where[] = "(sr.id LIKE ? OR c.name LIKE ? OR sr.invoice_id LIKE ?)";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
}
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT sr.*, c.name as customer_name, i.total_with_vat as invoice_total
|
|
FROM sales_returns sr
|
|
LEFT JOIN customers c ON sr.customer_id = c.id
|
|
LEFT JOIN invoices i ON sr.invoice_id = i.id
|
|
WHERE $whereSql
|
|
ORDER BY sr.id DESC");
|
|
$stmt->execute($params);
|
|
$data['returns'] = $stmt->fetchAll();
|
|
$data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'sale' ORDER BY id DESC")->fetchAll();
|
|
break;
|
|
|
|
case 'purchase_returns':
|
|
$where = ["1=1"];
|
|
$params = [];
|
|
if (!empty($_GET['search'])) {
|
|
$where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.invoice_id LIKE ?)";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
$params[] = "%{$_GET['search']}%";
|
|
}
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT pr.*, c.name as supplier_name, i.total_with_vat as invoice_total
|
|
FROM purchase_returns pr
|
|
LEFT JOIN customers c ON pr.supplier_id = c.id
|
|
LEFT JOIN invoices i ON pr.invoice_id = i.id
|
|
WHERE $whereSql
|
|
ORDER BY pr.id DESC");
|
|
$stmt->execute($params);
|
|
$data['returns'] = $stmt->fetchAll();
|
|
$data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices WHERE type = 'purchase' ORDER BY id DESC")->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 'accounting':
|
|
$data['journal_entries'] = db()->query("SELECT je.*,
|
|
(SELECT SUM(debit) FROM acc_ledger WHERE journal_entry_id = je.id) as total_debit
|
|
FROM acc_journal_entries je
|
|
ORDER BY je.entry_date DESC, je.id DESC LIMIT 100")->fetchAll();
|
|
$data['accounts'] = db()->query("SELECT * FROM acc_accounts ORDER BY code ASC")->fetchAll();
|
|
|
|
if (isset($_GET['action']) && $_GET['action'] === 'get_entry_details') {
|
|
header('Content-Type: application/json');
|
|
$id = (int)$_GET['id'];
|
|
$stmt = db()->prepare("SELECT l.*, a.name_en, a.code FROM acc_ledger l JOIN acc_accounts a ON l.account_id = a.id WHERE l.journal_entry_id = ?");
|
|
$stmt->execute([$id]);
|
|
echo json_encode($stmt->fetchAll());
|
|
exit;
|
|
}
|
|
|
|
if (isset($_GET['view']) && $_GET['view'] === 'trial_balance') {
|
|
$data['trial_balance'] = db()->query("SELECT a.code, a.name_en, SUM(l.debit) as total_debit, SUM(l.credit) as total_credit
|
|
FROM acc_accounts a
|
|
LEFT JOIN acc_ledger l ON a.id = l.account_id
|
|
GROUP BY a.id
|
|
HAVING total_debit > 0 OR total_credit > 0
|
|
ORDER BY a.code ASC")->fetchAll();
|
|
}
|
|
|
|
if (isset($_GET['view']) && $_GET['view'] === 'profit_loss') {
|
|
$data['revenue_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'revenue' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll();
|
|
$data['expense_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'expense' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll();
|
|
}
|
|
|
|
if (isset($_GET['view']) && $_GET['view'] === 'balance_sheet') {
|
|
$data['asset_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'asset' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll();
|
|
$data['liability_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'liability' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll();
|
|
$data['equity_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'equity' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll();
|
|
}
|
|
|
|
if (isset($_GET['view']) && $_GET['view'] === 'vat_report') {
|
|
$start = $_GET['start_date'] ?? date('Y-m-01');
|
|
$end = $_GET['end_date'] ?? date('Y-m-d');
|
|
$data['vat_report'] = getVatReport($start, $end);
|
|
$data['start_date'] = $start;
|
|
$data['end_date'] = $end;
|
|
}
|
|
|
|
if (isset($_GET['view']) && $_GET['view'] === 'coa') {
|
|
$data['coa'] = db()->query("SELECT a.*, p.name_en as parent_name
|
|
FROM acc_accounts a
|
|
LEFT JOIN acc_accounts p ON a.parent_id = p.id
|
|
ORDER BY a.code 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;
|
|
case 'expiry_report':
|
|
$where = ["expiry_date IS NOT NULL"];
|
|
$params = [];
|
|
$filter = $_GET['filter'] ?? 'all';
|
|
if ($filter === 'expired') {
|
|
$where[] = "expiry_date <= CURDATE()";
|
|
} elseif ($filter === 'near_expiry') {
|
|
$where[] = "expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)";
|
|
}
|
|
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar
|
|
FROM stock_items i
|
|
LEFT JOIN stock_categories c ON i.category_id = c.id
|
|
WHERE $whereSql
|
|
ORDER BY i.expiry_date ASC");
|
|
$stmt->execute($params);
|
|
$data['expiry_items'] = $stmt->fetchAll();
|
|
break;
|
|
case 'low_stock_report':
|
|
$stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar, s.name as supplier_name
|
|
FROM stock_items i
|
|
LEFT JOIN stock_categories c ON i.category_id = c.id
|
|
LEFT JOIN customers s ON i.supplier_id = s.id AND s.type = 'supplier'
|
|
WHERE i.stock_quantity <= i.min_stock_level
|
|
ORDER BY (i.min_stock_level - i.stock_quantity) DESC");
|
|
$stmt->execute();
|
|
$data['low_stock_items'] = $stmt->fetchAll();
|
|
break;
|
|
case 'cashflow_report':
|
|
$start_date = $_GET['start_date'] ?? date('Y-m-01');
|
|
$end_date = $_GET['end_date'] ?? date('Y-m-d');
|
|
|
|
// Fetch Cash & Bank Account IDs
|
|
$cash_accounts = db()->query("SELECT id FROM acc_accounts WHERE code IN (1100, 1200)")->fetchAll(PDO::FETCH_COLUMN);
|
|
$cash_ids_str = implode(',', $cash_accounts);
|
|
|
|
if (!empty($cash_ids_str)) {
|
|
// Opening Balance
|
|
$stmt = db()->prepare("SELECT SUM(debit - credit) FROM acc_ledger l JOIN acc_journal_entries je ON l.journal_entry_id = je.id WHERE l.account_id IN ($cash_ids_str) AND je.entry_date < ?");
|
|
$stmt->execute([$start_date]);
|
|
$data['opening_balance'] = $stmt->fetchColumn() ?: 0;
|
|
|
|
// Transactions in range
|
|
$stmt = db()->prepare("SELECT
|
|
je.entry_date,
|
|
je.description,
|
|
l.debit as inflow,
|
|
l.credit as outflow,
|
|
a.name_en as other_account,
|
|
a.type as other_type
|
|
FROM acc_ledger l
|
|
JOIN acc_journal_entries je ON l.journal_entry_id = je.id
|
|
LEFT JOIN acc_ledger l2 ON l2.journal_entry_id = je.id AND l2.id != l.id
|
|
LEFT JOIN acc_accounts a ON l2.account_id = a.id
|
|
WHERE l.account_id IN ($cash_ids_str)
|
|
AND je.entry_date BETWEEN ? AND ?
|
|
ORDER BY je.entry_date ASC, je.id ASC");
|
|
$stmt->execute([$start_date, $end_date]);
|
|
$data['cash_transactions'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} else {
|
|
$data['opening_balance'] = 0;
|
|
$data['cash_transactions'] = [];
|
|
}
|
|
break;
|
|
case 'hr_departments':
|
|
$data['departments'] = db()->query("SELECT * FROM hr_departments ORDER BY id DESC")->fetchAll();
|
|
break;
|
|
case 'hr_employees':
|
|
$data['employees'] = db()->query("SELECT e.*, d.name as dept_name FROM hr_employees e LEFT JOIN hr_departments d ON e.department_id = d.id ORDER BY e.id DESC")->fetchAll();
|
|
$data['departments'] = db()->query("SELECT * FROM hr_departments ORDER BY name ASC")->fetchAll();
|
|
break;
|
|
case 'hr_attendance':
|
|
$date = $_GET['date'] ?? date('Y-m-d');
|
|
$data['attendance_date'] = $date;
|
|
$data['employees'] = db()->query("SELECT e.id, e.name, d.name as dept_name, a.status, a.clock_in, a.clock_out
|
|
FROM hr_employees e
|
|
LEFT JOIN hr_departments d ON e.department_id = d.id
|
|
LEFT JOIN hr_attendance a ON e.id = a.employee_id AND a.attendance_date = '$date'
|
|
WHERE e.status = 'active' ORDER BY e.name ASC")->fetchAll();
|
|
break;
|
|
case 'hr_payroll':
|
|
$month = (int)($_GET['month'] ?? date('m'));
|
|
$year = (int)($_GET['year'] ?? date('Y'));
|
|
$data['month'] = $month;
|
|
$data['year'] = $year;
|
|
$data['payroll'] = db()->query("SELECT p.*, e.name as emp_name FROM hr_payroll p JOIN hr_employees e ON p.employee_id = e.id WHERE p.payroll_month = $month AND p.payroll_year = $year ORDER BY p.id DESC")->fetchAll();
|
|
$data['employees'] = db()->query("SELECT id, name, salary FROM hr_employees WHERE status = 'active' ORDER BY name ASC")->fetchAll();
|
|
break;
|
|
case 'loyalty_history':
|
|
$where = ["1=1"];
|
|
$params = [];
|
|
if (!empty($_GET['customer_id'])) {
|
|
$where[] = "lt.customer_id = ?";
|
|
$params[] = (int)$_GET['customer_id'];
|
|
}
|
|
if (!empty($_GET['type'])) {
|
|
$where[] = "lt.transaction_type = ?";
|
|
$params[] = $_GET['type'];
|
|
}
|
|
$whereSql = implode(" AND ", $where);
|
|
$stmt = db()->prepare("SELECT lt.*, c.name as customer_name, c.loyalty_tier, c.loyalty_points
|
|
FROM loyalty_transactions lt
|
|
JOIN customers c ON lt.customer_id = c.id
|
|
WHERE $whereSql
|
|
ORDER BY lt.created_at DESC");
|
|
$stmt->execute($params);
|
|
$data['loyalty_transactions'] = $stmt->fetchAll();
|
|
break;
|
|
case 'devices':
|
|
$data['devices'] = db()->query("SELECT * FROM hr_biometric_devices ORDER BY id DESC")->fetchAll();
|
|
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,
|
|
'expired_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE()")->fetchColumn(),
|
|
'near_expiry_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)")->fetchColumn(),
|
|
'low_stock_items_count' => db()->query("SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level")->fetchColumn(),
|
|
];
|
|
$data['stats']['total_receivable'] = $data['stats']['total_sales'] - $data['stats']['total_received'];
|
|
$data['stats']['total_payable'] = $data['stats']['total_purchases'] - $data['stats']['total_paid'];
|
|
|
|
// Sales Chart Data
|
|
$data['monthly_sales'] = db()->query("SELECT DATE_FORMAT(invoice_date, '%M %Y') as label, SUM(total_with_vat) as total FROM invoices WHERE type = 'sale' GROUP BY DATE_FORMAT(invoice_date, '%Y-%m') ORDER BY invoice_date ASC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC);
|
|
$data['yearly_sales'] = db()->query("SELECT YEAR(invoice_date) as label, SUM(total_with_vat) as total FROM invoices WHERE type = 'sale' GROUP BY label ORDER BY label ASC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
|
|
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/bootstrap@5.3.0/dist/js/bootstrap.bundle.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>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></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: 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=sales_returns" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'sales_returns' ? 'active' : '' ?>">
|
|
<i class="bi bi-arrow-return-left"></i> <span data-en="Sales Returns" data-ar="مرتجع المبيعات">Sales Returns</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=purchase_returns" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'purchase_returns' ? 'active' : '' ?>">
|
|
<i class="bi bi-arrow-return-right"></i> <span data-en="Purchase Returns" data-ar="مرتجع المشتريات">Purchase Returns</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>
|
|
|
|
<!-- Accounting Section -->
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['accounting', 'expense_report']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#accounting-collapse">
|
|
<span data-en="Accounting" data-ar="المحاسبة">Accounting</span>
|
|
<i class="bi bi-chevron-down"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['accounting', 'expense_report']) ? 'show' : '' ?>" id="accounting-collapse">
|
|
<a href="index.php?page=accounting" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'accounting' ? 'active' : '' ?>">
|
|
<i class="bi bi-calculator"></i> <span data-en="Journal & Ledger" data-ar="اليومية والأستاذ">Journal & Ledger</span>
|
|
</a>
|
|
<a href="index.php?page=accounting&view=trial_balance" class="nav-link <?= isset($_GET['view']) && $_GET['view'] === 'trial_balance' ? 'active' : '' ?>">
|
|
<i class="bi bi-list-columns-reverse"></i> <span data-en="Trial Balance" data-ar="ميزان المراجعة">Trial Balance</span>
|
|
</a>
|
|
<a href="index.php?page=accounting&view=profit_loss" class="nav-link <?= isset($_GET['view']) && $_GET['view'] === 'profit_loss' ? 'active' : '' ?>">
|
|
<i class="bi bi-graph-up-arrow"></i> <span data-en="Profit & Loss" data-ar="الأرباح والخسائر">Profit & Loss</span>
|
|
</a>
|
|
<a href="index.php?page=accounting&view=balance_sheet" class="nav-link <?= isset($_GET['view']) && $_GET['view'] === 'balance_sheet' ? 'active' : '' ?>">
|
|
<i class="bi bi-journal-text"></i> <span data-en="Balance Sheet" data-ar="الميزانية العمومية">Balance Sheet</span>
|
|
</a>
|
|
<a href="index.php?page=accounting&view=vat_report" class="nav-link <?= isset($_GET['view']) && $_GET['view'] === 'vat_report' ? 'active' : '' ?>">
|
|
<i class="bi bi-percent"></i> <span data-en="VAT Report" data-ar="تقرير الضريبة">VAT Report</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']) ? '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']) ? '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-cash-stack"></i> <span data-en="Expenses" data-ar="المصروفات">Expenses</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', 'cashflow_report']) ? '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', 'cashflow_report']) ? '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>
|
|
<a href="index.php?page=cashflow_report" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'cashflow_report' ? 'active' : '' ?>">
|
|
<i class="bi bi-cash-stack"></i> <span data-en="Cashflow Statement" data-ar="قائمة التدفقات النقدية">Cashflow Statement</span>
|
|
</a>
|
|
<a href="index.php?page=expiry_report" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'expiry_report' ? 'active' : '' ?>">
|
|
<i class="bi bi-calendar-x"></i> <span data-en="Expiry Report" data-ar="تقرير الانتهاء">Expiry Report</span>
|
|
</a>
|
|
<a href="index.php?page=low_stock_report" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'low_stock_report' ? 'active' : '' ?>">
|
|
<i class="bi bi-graph-down-arrow"></i> <span data-en="Low Stock Report" data-ar="تقرير نواقص المخزون">Low Stock Report</span>
|
|
</a>
|
|
<a href="index.php?page=loyalty_history" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'loyalty_history' ? 'active' : '' ?>">
|
|
<i class="bi bi-star"></i> <span data-en="Loyalty History" data-ar="سجل الولاء">Loyalty History</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', 'devices']) ? '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=devices" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'devices' ? 'active' : '' ?>">
|
|
<i class="bi bi-fingerprint"></i> <span data-en="Biometric Devices" data-ar="أجهزة البصمة">Biometric Devices</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>
|
|
</div>
|
|
|
|
<!-- HR Section -->
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['hr_employees', 'hr_departments', 'hr_attendance', 'hr_payroll']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#hr-collapse">
|
|
<span data-en="HR" data-ar="الموارد البشرية">HR</span>
|
|
<i class="bi bi-chevron-down"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['hr_employees', 'hr_departments', 'hr_attendance', 'hr_payroll']) ? 'show' : '' ?>" id="hr-collapse">
|
|
<a href="index.php?page=hr_departments" class="nav-link <?= $page === 'hr_departments' ? 'active' : '' ?>">
|
|
<i class="bi bi-building"></i> <span data-en="Departments" data-ar="الأقسام">Departments</span>
|
|
</a>
|
|
<a href="index.php?page=hr_employees" class="nav-link <?= $page === 'hr_employees' ? 'active' : '' ?>">
|
|
<i class="bi bi-person-badge"></i> <span data-en="Employees" data-ar="الموظفون">Employees</span>
|
|
</a>
|
|
<a href="index.php?page=hr_attendance" class="nav-link <?= $page === 'hr_attendance' ? 'active' : '' ?>">
|
|
<i class="bi bi-calendar-check"></i> <span data-en="Attendance" data-ar="الحضور">Attendance</span>
|
|
</a>
|
|
<a href="index.php?page=hr_payroll" class="nav-link <?= $page === 'hr_payroll' ? 'active' : '' ?>">
|
|
<i class="bi bi-cash-coin"></i> <span data-en="Payroll" data-ar="الرواتب">Payroll</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' => 'فواتير المشتريات الضريبية'],
|
|
'sales_returns' => ['en' => 'Sales Returns', 'ar' => 'مرتجع المبيعات'],
|
|
'customer_statement' => ['en' => 'Customer Statement', 'ar' => 'كشف حساب عميل'],
|
|
'supplier_statement' => ['en' => 'Supplier Statement', 'ar' => 'كشف حساب مورد'],
|
|
'expiry_report' => ['en' => 'Expiry Report', 'ar' => 'تقرير انتهاء الصلاحية'],
|
|
'low_stock_report' => ['en' => 'Low Stock Report', 'ar' => 'تقرير نواقص المخزون'],
|
|
'settings' => ['en' => 'Company Profile', 'ar' => 'ملف الشركة'],
|
|
'devices' => ['en' => 'Biometric Devices', 'ar' => 'أجهزة البصمة'],
|
|
'hr_departments' => ['en' => 'HR Departments', 'ar' => 'أقسام الموارد البشرية'],
|
|
'hr_employees' => ['en' => 'HR Employees', 'ar' => 'موظفي الموارد البشرية'],
|
|
'hr_attendance' => ['en' => 'HR Attendance', 'ar' => 'حضور الموارد البشرية'],
|
|
'hr_payroll' => ['en' => 'HR Payroll', 'ar' => 'رواتب الموارد البشرية'],
|
|
'cashflow_report' => ['en' => 'Cashflow Statement', 'ar' => 'قائمة التدفقات النقدية'],
|
|
'loyalty_history' => ['en' => 'Loyalty History', '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'): ?>
|
|
|
|
<?php if ($data['stats']['expired_items'] > 0 || $data['stats']['near_expiry_items'] > 0 || $data['stats']['low_stock_items_count'] > 0): ?>
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="alert alert-warning border-0 shadow-sm d-flex align-items-center mb-0">
|
|
<i class="bi bi-exclamation-triangle-fill h4 mb-0 me-3 text-warning"></i>
|
|
<div class="flex-grow-1">
|
|
<span data-en="Inventory Alert:" data-ar="تنبيه المخزون:"><strong>Inventory Alert:</strong></span>
|
|
<?php if ($data['stats']['expired_items'] > 0): ?>
|
|
<span data-en="<?= $data['stats']['expired_items'] ?> items have expired." data-ar="هنالك <?= $data['stats']['expired_items'] ?> صنف منتهي الصلاحية."><?= $data['stats']['expired_items'] ?> items have expired.</span>
|
|
<?php endif; ?>
|
|
<?php if ($data['stats']['near_expiry_items'] > 0): ?>
|
|
<span data-en="<?= $data['stats']['near_expiry_items'] ?> items are expiring soon (within 30 days)." data-ar="هنالك <?= $data['stats']['near_expiry_items'] ?> صنف ستنتهي صلاحيتها قريباً (خلال 30 يوم)."><?= $data['stats']['near_expiry_items'] ?> items are expiring soon (within 30 days).</span>
|
|
<?php endif; ?>
|
|
<?php if ($data['stats']['low_stock_items_count'] > 0): ?>
|
|
<span data-en="<?= $data['stats']['low_stock_items_count'] ?> items are below minimum level." data-ar="هنالك <?= $data['stats']['low_stock_items_count'] ?> صنف تحت الحد الأدنى للمخزون."><?= $data['stats']['low_stock_items_count'] ?> items are below minimum level.</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="index.php?page=expiry_report" class="btn btn-warning btn-sm" data-en="Expiry Report" data-ar="تقرير الانتهاء">Expiry Report</a>
|
|
<a href="index.php?page=low_stock_report" class="btn btn-danger btn-sm" data-en="Low Stock Report" data-ar="تقرير النواقص">Low Stock Report</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-primary border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<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 class="ms-3">
|
|
<i class="bi bi-cart h2 text-primary op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-warning border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<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 class="ms-3">
|
|
<i class="bi bi-cash-stack h2 text-warning op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-danger border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small" data-en="Customer Due" data-ar="مستحقات العملاء">Customer Due</div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_receivable'] ?? 0), 3) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-person-exclamation h2 text-danger op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-dark border-4 h-100 bg-light">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<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 class="ms-3">
|
|
<i class="bi bi-bag h2 text-dark op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-info border-4 h-100 bg-light">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small" data-en="Total Paid" data-ar="إجمالي المدفوع">Total Paid</div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_paid'] ?? 0), 3) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-wallet2 h2 text-info op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-secondary border-4 h-100 bg-light">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small" data-en="Supplier Due" data-ar="مستحقات الموردين">Supplier Due</div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_payable'] ?? 0), 3) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-truck h2 text-secondary op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-success border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<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 class="ms-3">
|
|
<i class="bi bi-people h2 text-success op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-info border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<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 class="ms-3">
|
|
<i class="bi bi-box-seam h2 text-info op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-12">
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Sales Performance" data-ar="أداء المبيعات">Sales Performance</h5>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-primary active" id="btnMonthly" data-en="Monthly" data-ar="شهري">Monthly</button>
|
|
<button type="button" class="btn btn-outline-primary" id="btnYearly" data-en="Yearly" data-ar="سنوي">Yearly</button>
|
|
</div>
|
|
</div>
|
|
<div style="height: 300px;">
|
|
<canvas id="salesChart"></canvas>
|
|
</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 table-bordered 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>
|
|
<a href="promo-catalog.php" class="btn btn-outline-danger me-2">
|
|
<i class="bi bi-file-pdf"></i> <span data-en="Promo Catalog" data-ar="كتالوج العروض">Promo Catalog</span>
|
|
</a>
|
|
<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']) ?>
|
|
<?php if (isset($item['is_promotion']) && $item['is_promotion']): ?>
|
|
<span class="badge bg-success ms-1" style="font-size: 0.65rem;" data-en="Promo" data-ar="عرض">Promo</span>
|
|
<?php endif; ?>
|
|
</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 class="col-12">
|
|
<hr>
|
|
<h6 data-en="Promotion Details" data-ar="تفاصيل العرض">Promotion Details</h6>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="form-check form-switch mt-4">
|
|
<input class="form-check-input isPromotionToggleEdit" type="checkbox" name="is_promotion" value="1" <?= $item['is_promotion'] ? 'checked' : '' ?> id="isPromotionToggleEdit<?= $item['id'] ?>" data-id="<?= $item['id'] ?>">
|
|
<label class="form-check-label" for="isPromotionToggleEdit<?= $item['id'] ?>" data-en="On Promotion?" data-ar="في العرض؟">On Promotion?</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="row g-3 promotionFieldsContainerEdit" id="promotionFieldsContainerEdit<?= $item['id'] ?>" style="display: <?= $item['is_promotion'] ? 'flex' : 'none' ?>;">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Start Date</label>
|
|
<input type="date" name="promotion_start" class="form-control" value="<?= $item['promotion_start'] ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">End Date</label>
|
|
<input type="date" name="promotion_end" class="form-control" value="<?= $item['promotion_end'] ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Percent (%)</label>
|
|
<input type="number" step="0.01" name="promotion_percent" class="form-control" value="<?= (float)$item['promotion_percent'] ?>">
|
|
</div>
|
|
</div>
|
|
</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 === 'expiry_report'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Expiry Report" data-ar="تقرير انتهاء الصلاحية">Expiry Report</h5>
|
|
<div class="d-flex gap-2">
|
|
<a href="index.php?page=expiry_report&filter=all" class="btn btn-sm <?= !isset($_GET['filter']) || $_GET['filter'] === 'all' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="All" data-ar="الكل">All</a>
|
|
<a href="index.php?page=expiry_report&filter=expired" class="btn btn-sm <?= isset($_GET['filter']) && $_GET['filter'] === 'expired' ? 'btn-danger' : 'btn-outline-danger' ?>" data-en="Expired" data-ar="منتهي">Expired</a>
|
|
<a href="index.php?page=expiry_report&filter=near_expiry" class="btn btn-sm <?= isset($_GET['filter']) && $_GET['filter'] === 'near_expiry' ? 'btn-warning' : 'btn-outline-warning' ?>" data-en="Near Expiry" data-ar="قريب الانتهاء">Near Expiry</a>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="SKU" data-ar="الباركود">SKU</th>
|
|
<th data-en="Item Name" data-ar="اسم الصنف">Item Name</th>
|
|
<th data-en="Category" data-ar="الفئة">Category</th>
|
|
<th data-en="Stock Level" data-ar="المخزون">Stock Level</th>
|
|
<th data-en="Expiry Date" data-ar="تاريخ الانتهاء">Expiry Date</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['expiry_items'])): ?>
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted p-4" data-en="No items found." data-ar="لا توجد أصناف.">No items found.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<?php foreach ($data['expiry_items'] as $item): ?>
|
|
<?php
|
|
$is_expired = strtotime($item['expiry_date']) <= strtotime(date('Y-m-d'));
|
|
$is_near = !$is_expired && strtotime($item['expiry_date']) <= strtotime(date('Y-m-d', strtotime('+30 days')));
|
|
?>
|
|
<tr class="<?= $is_expired ? 'table-danger' : ($is_near ? 'table-warning' : '') ?>">
|
|
<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><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td>
|
|
<td><?= number_format((float)$item['stock_quantity'], 3) ?></td>
|
|
<td><?= htmlspecialchars($item['expiry_date']) ?></td>
|
|
<td>
|
|
<?php if ($is_expired): ?>
|
|
<span class="badge bg-danger" data-en="Expired" data-ar="منتهي">Expired</span>
|
|
<?php elseif ($is_near): ?>
|
|
<span class="badge bg-warning text-dark" data-en="Near Expiry" data-ar="قريب الانتهاء">Near Expiry</span>
|
|
<?php else: ?>
|
|
<span class="badge bg-success" data-en="Good" data-ar="صالح">Good</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'low_stock_report'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Low Stock Report" data-ar="تقرير نواقص المخزون">Low Stock Report</h5>
|
|
<button class="btn btn-outline-primary btn-sm d-print-none" onclick="window.print()">
|
|
<i class="bi bi-printer"></i> <span data-en="Print" data-ar="طباعة">Print</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="SKU" data-ar="الباركود">SKU</th>
|
|
<th data-en="Item Name" data-ar="اسم الصنف">Item Name</th>
|
|
<th data-en="Category" data-ar="الفئة">Category</th>
|
|
<th data-en="Supplier" data-ar="المورد">Supplier</th>
|
|
<th data-en="Min Level" data-ar="أدنى مستوى">Min Level</th>
|
|
<th data-en="Current Stock" data-ar="المخزون الحالي">Current Stock</th>
|
|
<th data-en="Shortage" data-ar="النقص">Shortage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['low_stock_items'])): ?>
|
|
<tr>
|
|
<td colspan="7" class="text-center text-muted p-4" data-en="All items are above minimum levels." data-ar="جميع الأصناف فوق الحد الأدنى.">All items are above minimum levels.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<?php foreach ($data['low_stock_items'] as $item): ?>
|
|
<?php $shortage = (float)$item['min_stock_level'] - (float)$item['stock_quantity']; ?>
|
|
<tr class="<?= (float)$item['stock_quantity'] <= 0 ? 'table-danger' : 'table-warning' ?>">
|
|
<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><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td>
|
|
<td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td>
|
|
<td><?= number_format((float)$item['min_stock_level'], 2) ?></td>
|
|
<td>
|
|
<span class="badge <?= (float)$item['stock_quantity'] <= 0 ? 'bg-danger' : 'bg-warning text-dark' ?>">
|
|
<?= number_format((float)$item['stock_quantity'], 3) ?>
|
|
</span>
|
|
</td>
|
|
<td class="fw-bold text-danger"><?= number_format($shortage, 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'loyalty_history'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Loyalty Transaction History" data-ar="سجل عمليات الولاء">Loyalty Transaction History</h5>
|
|
<button class="btn btn-outline-primary btn-sm 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="loyalty_history">
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-bold" data-en="Customer" data-ar="العميل">Customer</label>
|
|
<select name="customer_id" class="form-select select2">
|
|
<option value="">All Customers</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-3">
|
|
<label class="form-label small fw-bold" data-en="Type" data-ar="النوع">Type</label>
|
|
<select name="type" class="form-select">
|
|
<option value="">All Types</option>
|
|
<option value="earned" <?= (($_GET['type'] ?? '') == 'earned') ? 'selected' : '' ?>>Earned</option>
|
|
<option value="redeemed" <?= (($_GET['type'] ?? '') == 'redeemed') ? 'selected' : '' ?>>Redeemed</option>
|
|
<option value="adjustment" <?= (($_GET['type'] ?? '') == 'adjustment') ? 'selected' : '' ?>>Adjustment</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-filter"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Customer" data-ar="العميل">Customer</th>
|
|
<th data-en="Tier" data-ar="الفئة">Tier</th>
|
|
<th data-en="Type" data-ar="النوع">Type</th>
|
|
<th data-en="Points" data-ar="النقاط">Points</th>
|
|
<th data-en="Description" data-ar="الوصف">Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['loyalty_transactions'])): ?>
|
|
<tr><td colspan="6" class="text-center py-4 text-muted">No transactions found.</td></tr>
|
|
<?php endif; ?>
|
|
<?php foreach ($data['loyalty_transactions'] as $lt): ?>
|
|
<tr>
|
|
<td><?= date('Y-m-d H:i', strtotime($lt['created_at'])) ?></td>
|
|
<td>
|
|
<div class="fw-bold"><?= htmlspecialchars($lt['customer_name']) ?></div>
|
|
<div class="smaller text-muted">Current Balance: <?= number_format($lt['loyalty_points'], 0) ?> pts</div>
|
|
</td>
|
|
<td>
|
|
<?php
|
|
$tier = $lt['loyalty_tier'];
|
|
$badge = ($tier === 'gold') ? 'bg-warning text-dark' : (($tier === 'silver') ? 'bg-info text-dark' : 'bg-secondary');
|
|
?>
|
|
<span class="badge text-uppercase <?= $badge ?>"><?= $tier ?></span>
|
|
</td>
|
|
<td>
|
|
<?php
|
|
$type = $lt['transaction_type'];
|
|
$typeBadge = ($type === 'earned') ? 'bg-success' : (($type === 'redeemed') ? 'bg-danger' : 'bg-info');
|
|
?>
|
|
<span class="badge <?= $typeBadge ?>"><?= ucfirst($type) ?></span>
|
|
</td>
|
|
<td class="fw-bold <?= (float)$lt['points_change'] > 0 ? 'text-success' : 'text-danger' ?>">
|
|
<?= (float)$lt['points_change'] > 0 ? '+' : '' ?><?= number_format($lt['points_change'], 0) ?>
|
|
</td>
|
|
<td class="small"><?= htmlspecialchars($lt['description']) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'pos'): ?>
|
|
<?php
|
|
$products_raw = db()->query("SELECT * FROM stock_items WHERE stock_quantity > 0 ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
|
|
$products = [];
|
|
foreach ($products_raw as $p) {
|
|
$p['original_price'] = (float)$p['sale_price'];
|
|
$p['sale_price'] = getPromotionalPrice($p);
|
|
$products[] = $p;
|
|
}
|
|
$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">
|
|
<div class="d-flex flex-column">
|
|
<?php if ($p['sale_price'] < $p['original_price']): ?>
|
|
<span class="text-muted smaller text-decoration-line-through">OMR <?= number_format($p['original_price'], 3) ?></span>
|
|
<?php endif; ?>
|
|
<span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span>
|
|
</div>
|
|
<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'] ?>"
|
|
data-points="<?= $c['loyalty_points'] ?>"
|
|
data-tier="<?= $c['loyalty_tier'] ?>"
|
|
data-multiplier="<?= getLoyaltyMultiplier($c['loyalty_tier'] ?? 'bronze') ?>"
|
|
data-spent="<?= $c['total_spent'] ?>">
|
|
<?= 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="mt-2 p-2 rounded bg-light border border-primary-subtle" style="display:none">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<div>
|
|
<span id="tierBadge" class="badge text-uppercase">Bronze</span>
|
|
<span class="fw-bold ms-1 text-primary"><span id="customerPoints">0</span> pts</span>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="redeemLoyalty" onchange="cart.render()">
|
|
<label class="form-check-label small fw-bold" for="redeemLoyalty">Redeem</label>
|
|
</div>
|
|
</div>
|
|
<div class="progress" style="height: 4px;">
|
|
<div id="tierProgress" class="progress-bar bg-primary" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
<div id="nextTierInfo" class="smaller text-muted mt-1">Spend more to unlock Silver</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: [],
|
|
loyaltySettings: {
|
|
enabled: <?= json_encode($data['settings']['loyalty_enabled'] ?? '0') ?>,
|
|
pointsPerUnit: parseFloat(<?= json_encode($data['settings']['loyalty_points_per_unit'] ?? '1') ?>),
|
|
redeemPointsPerUnit: parseFloat(<?= json_encode($data['settings']['loyalty_redeem_points_per_unit'] ?? '100') ?>)
|
|
},
|
|
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();
|
|
},
|
|
onCustomerChange() {
|
|
const select = document.getElementById('posCustomer');
|
|
const option = select.options[select.selectedIndex];
|
|
const display = document.getElementById('loyaltyDisplay');
|
|
|
|
if (!select.value || this.loyaltySettings.enabled !== '1') {
|
|
if (display) display.style.display = 'none';
|
|
this.customerPoints = 0;
|
|
this.customerTier = 'bronze';
|
|
this.customerMultiplier = 1.0;
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
if (redeemSwitch) redeemSwitch.checked = false;
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
this.customerPoints = parseFloat(option.dataset.points) || 0;
|
|
this.customerTier = option.dataset.tier || 'bronze';
|
|
this.customerMultiplier = parseFloat(option.dataset.multiplier) || 1.0;
|
|
const spent = parseFloat(option.dataset.spent) || 0;
|
|
|
|
document.getElementById('customerPoints').innerText = Math.floor(this.customerPoints);
|
|
const badge = document.getElementById('tierBadge');
|
|
badge.innerText = this.customerTier;
|
|
badge.className = 'badge text-uppercase ' + (this.customerTier === 'gold' ? 'bg-warning text-dark' : (this.customerTier === 'silver' ? 'bg-info text-dark' : 'bg-secondary'));
|
|
|
|
const progressBar = document.getElementById('tierProgress');
|
|
const nextTierInfo = document.getElementById('nextTierInfo');
|
|
let progress = 0;
|
|
if (this.customerTier === 'bronze') {
|
|
progress = (spent / 500) * 100;
|
|
nextTierInfo.innerText = `Spend ${(500 - spent).toFixed(3)} OMR more for Silver (1.2x points)`;
|
|
} else if (this.customerTier === 'silver') {
|
|
progress = ((spent - 500) / 1000) * 100;
|
|
nextTierInfo.innerText = `Spend ${(1500 - spent).toFixed(3)} OMR more for Gold (1.5x points)`;
|
|
} else {
|
|
progress = 100;
|
|
nextTierInfo.innerText = 'You are a Gold member! (1.5x points)';
|
|
}
|
|
progressBar.style.width = Math.min(100, progress) + '%';
|
|
|
|
display.style.display = 'block';
|
|
this.render();
|
|
},
|
|
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 loyaltyRedeemedValue = 0;
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
if (redeemSwitch && redeemSwitch.checked) {
|
|
const maxRedeemValue = subtotal - discountAmount;
|
|
const availableRedeemValue = this.customerPoints / 100;
|
|
loyaltyRedeemedValue = Math.min(maxRedeemValue, availableRedeemValue);
|
|
}
|
|
|
|
const total = subtotal - discountAmount - loyaltyRedeemedValue;
|
|
const pointsToEarn = Math.floor(total * (this.customerMultiplier || 1.0));
|
|
|
|
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 (loyaltyRedeemedValue > 0) totalHtml += `<div class="smaller text-success">- Loyalty: OMR ${loyaltyRedeemedValue.toFixed(3)}</div>`;
|
|
|
|
if (document.getElementById('posCustomer').value) {
|
|
totalHtml += `<div class="smaller text-info">+ Earn: ${pointsToEarn} pts</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 loyaltyRedeemedValue = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints / 100) : 0;
|
|
const total = subtotal - discountAmount - loyaltyRedeemedValue;
|
|
|
|
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 loyaltyRedeemedValue = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints / this.loyaltySettings.redeemPointsPerUnit) : 0;
|
|
return subtotal - discountAmount - loyaltyRedeemedValue;
|
|
},
|
|
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 / this.loyaltySettings.redeemPointsPerUnit) : 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-warning return-invoice-btn" data-id="<?= $inv['id'] ?>" data-bs-toggle="modal" data-bs-target="<?= $page === 'sales' ? '#addSalesReturnModal' : '#addPurchaseReturnModal' ?>" title="Return"><i class="bi bi-arrow-return-left"></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 d-print-none">
|
|
<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 select2" 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 === 'cashflow_report'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
|
<h5 class="m-0" data-en="Cashflow Statement" data-ar="قائمة التدفقات النقدية">Cashflow Statement</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-5">
|
|
<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-5">
|
|
<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="Generate" data-ar="توليد">Generate</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="cashflow-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="Cashflow Statement" data-ar="قائمة التدفقات النقدية">Cashflow Statement</h2>
|
|
<p class="text-muted small">Period: <?= htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?> to <?= htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?></p>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="table table-bordered">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th data-en="Description" data-ar="الوصف">Description</th>
|
|
<th data-en="Amount (OMR)" data-ar="المبلغ (ريال عماني)" class="text-end">Amount (OMR)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr class="fw-bold table-light">
|
|
<td data-en="Opening Cash Balance" data-ar="رصيد النقدية الافتتاحي">Opening Cash Balance</td>
|
|
<td class="text-end"><?= number_format($data['opening_balance'], 3) ?></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td colspan="2" class="fw-bold bg-light" data-en="Operating Activities" data-ar="الأنشطة التشغيلية">Operating Activities</td>
|
|
</tr>
|
|
<?php
|
|
$op_inflow = 0; $op_outflow = 0;
|
|
$inv_inflow = 0; $inv_outflow = 0;
|
|
$fin_inflow = 0; $fin_outflow = 0;
|
|
|
|
foreach ($data['cash_transactions'] as $t) {
|
|
$amt = (float)$t['inflow'] - (float)$t['outflow'];
|
|
// Very simple categorization based on account type
|
|
if ($t['other_type'] === 'revenue' || $t['other_type'] === 'expense' || in_array($t['other_account'], ['Accounts Receivable', 'Accounts Payable', 'VAT Input', 'VAT Payable'])) {
|
|
if ($amt > 0) $op_inflow += $amt; else $op_outflow += abs($amt);
|
|
} elseif ($t['other_type'] === 'asset' && !in_array($t['other_account'], ['Accounts Receivable', 'Inventory'])) {
|
|
// Fixed assets etc
|
|
if ($amt > 0) $inv_inflow += $amt; else $inv_outflow += abs($amt);
|
|
} elseif ($t['other_type'] === 'equity' || $t['other_type'] === 'liability') {
|
|
if ($amt > 0) $fin_inflow += $amt; else $fin_outflow += abs($amt);
|
|
} else {
|
|
// Default to operating if unsure
|
|
if ($amt > 0) $op_inflow += $amt; else $op_outflow += abs($amt);
|
|
}
|
|
}
|
|
?>
|
|
<tr>
|
|
<td class="ps-4" data-en="Cash Received from Customers & Others" data-ar="المقبوضات النقدية من العملاء وغيرهم">Cash Received from Customers & Others</td>
|
|
<td class="text-end text-success"><?= number_format($op_inflow, 3) ?></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-4" data-en="Cash Paid to Suppliers & Expenses" data-ar="المدفوعات النقدية للموردين والمصروفات">Cash Paid to Suppliers & Expenses</td>
|
|
<td class="text-end text-danger">(<?= number_format($op_outflow, 3) ?>)</td>
|
|
</tr>
|
|
<tr class="fw-bold">
|
|
<td data-en="Net Cash from Operating Activities" data-ar="صافي النقد من الأنشطة التشغيلية">Net Cash from Operating Activities</td>
|
|
<td class="text-end border-top"><?= number_format($op_inflow - $op_outflow, 3) ?></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td colspan="2" class="fw-bold bg-light" data-en="Investing Activities" data-ar="الأنشطة الاستثمارية">Investing Activities</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-4" data-en="Net Cash from Investing Activities" data-ar="صافي النقد من الأنشطة الاستثمارية">Net Cash from Investing Activities</td>
|
|
<td class="text-end"><?= number_format($inv_inflow - $inv_outflow, 3) ?></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td colspan="2" class="fw-bold bg-light" data-en="Financing Activities" data-ar="الأنشطة التمويلية">Financing Activities</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-4" data-en="Net Cash from Financing Activities" data-ar="صافي النقد من الأنشطة التمويلية">Net Cash from Financing Activities</td>
|
|
<td class="text-end"><?= number_format($fin_inflow - $fin_outflow, 3) ?></td>
|
|
</tr>
|
|
|
|
<tr class="fw-bold table-primary">
|
|
<?php $net_change = ($op_inflow - $op_outflow) + ($inv_inflow - $inv_outflow) + ($fin_inflow - $fin_outflow); ?>
|
|
<td data-en="Net Change in Cash" data-ar="صافي التغير في النقدية">Net Change in Cash</td>
|
|
<td class="text-end"><?= number_format($net_change, 3) ?></td>
|
|
</tr>
|
|
<tr class="fw-bold table-success">
|
|
<td data-en="Closing Cash Balance" data-ar="رصيد النقدية الختامي">Closing Cash Balance</td>
|
|
<td class="text-end"><?= number_format($data['opening_balance'] + $net_change, 3) ?></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="mt-4 d-none d-print-block">
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<p>___________________<br>Prepared By</p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<p>___________________<br>Approved By</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card p-4 d-print-none">
|
|
<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 === 'accounting'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
|
<h5 class="m-0"><i class="fas fa-calculator me-2"></i> <span data-en="Accounting Module" data-ar="وحدة المحاسبة">Accounting Module</span></h5>
|
|
<div class="d-flex gap-2">
|
|
<form method="POST" onsubmit="return confirm('This will re-calculate all automatic journal entries from scratch. Continue?')">
|
|
<button type="submit" name="sync_accounting" class="btn btn-outline-warning">
|
|
<i class="bi bi-arrow-repeat"></i> <span data-en="Sync All" data-ar="مزامنة الكل">Sync All</span>
|
|
</button>
|
|
</form>
|
|
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addManualJournalModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Manual Entry" data-ar="قيد يدوي">Manual Entry</span>
|
|
</button>
|
|
<div class="btn-group">
|
|
<a href="index.php?page=accounting" class="btn <?= !isset($_GET['view']) ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="Journal" data-ar="اليومية">Journal</a>
|
|
<a href="index.php?page=accounting&view=coa" class="btn <?= isset($_GET['view']) && $_GET['view'] === 'coa' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="Accounts" data-ar="الحسابات">Accounts</a>
|
|
<a href="index.php?page=accounting&view=trial_balance" class="btn <?= isset($_GET['view']) && $_GET['view'] === 'trial_balance' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="Trial Balance" data-ar="ميزان المراجعة">Trial Balance</a>
|
|
<a href="index.php?page=accounting&view=profit_loss" class="btn <?= isset($_GET['view']) && $_GET['view'] === 'profit_loss' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="P&L" data-ar="الأرباح">P&L</a>
|
|
<a href="index.php?page=accounting&view=balance_sheet" class="btn <?= isset($_GET['view']) && $_GET['view'] === 'balance_sheet' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="Balance Sheet" data-ar="الميزانية">Balance Sheet</a>
|
|
<a href="index.php?page=accounting&view=vat_report" class="btn <?= isset($_GET['view']) && $_GET['view'] === 'vat_report' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="VAT Report" data-ar="تقرير الضريبة">VAT Report</a>
|
|
</div>
|
|
<button class="btn btn-outline-secondary" onclick="window.print()">
|
|
<i class="bi bi-printer"></i> <span data-en="Print" data-ar="طباعة">Print</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-none d-print-block mb-4">
|
|
<div class="row">
|
|
<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"><?= isset($_GET['view']) ? ucwords(str_replace('_', ' ', $_GET['view'])) : 'Journal' ?></h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!isset($_GET['view'])): ?>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Description" data-ar="الوصف">Description</th>
|
|
<th data-en="Reference" data-ar="المرجع">Reference</th>
|
|
<th data-en="Amount" data-ar="المبلغ">Amount</th>
|
|
<th data-en="Action" data-ar="الإجراء">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['journal_entries'] as $entry): ?>
|
|
<tr>
|
|
<td><?= $entry['entry_date'] ?></td>
|
|
<td><?= htmlspecialchars($entry['description']) ?></td>
|
|
<td><span class="badge bg-secondary"><?= htmlspecialchars($entry['reference']) ?></span></td>
|
|
<td><?= number_format((float)$entry['total_debit'], 3) ?></td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-info" onclick="viewJournalEntry(<?= $entry['id'] ?>)">
|
|
<i class="bi bi-eye"></i> <span data-en="View" data-ar="عرض">View</span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php elseif ($_GET['view'] === 'coa'): ?>
|
|
<div class="d-flex justify-content-end mb-3">
|
|
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addAccountModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Account" data-ar="إضافة حساب">Add Account</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th data-en="Code" data-ar="الكود">Code</th>
|
|
<th data-en="Name" data-ar="الاسم">Name</th>
|
|
<th data-en="Type" data-ar="النوع">Type</th>
|
|
<th data-en="Parent" data-ar="الحساب الأب">Parent</th>
|
|
<th data-en="Balance" data-ar="الرصيد" class="text-end">Balance</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['coa'] as $acc): ?>
|
|
<tr>
|
|
<td class="fw-bold"><?= $acc['code'] ?></td>
|
|
<td>
|
|
<?= htmlspecialchars($acc['name_en']) ?><br>
|
|
<small class="text-muted"><?= htmlspecialchars($acc['name_ar']) ?></small>
|
|
</td>
|
|
<td><span class="badge bg-light text-dark border text-uppercase"><?= $acc['type'] ?></span></td>
|
|
<td><?= htmlspecialchars($acc['parent_name'] ?? '---') ?></td>
|
|
<td class="text-end fw-bold"><?= number_format(getAccountBalance($acc['code']), 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php elseif ($_GET['view'] === 'vat_report'): ?>
|
|
<div class="row">
|
|
<div class="col-md-6 mx-auto">
|
|
<div class="bg-light p-3 rounded mb-4 d-print-none">
|
|
<form method="GET" class="row g-2">
|
|
<input type="hidden" name="page" value="accounting">
|
|
<input type="hidden" name="view" value="vat_report">
|
|
<div class="col">
|
|
<input type="date" name="start_date" class="form-control" value="<?= $data['start_date'] ?>">
|
|
</div>
|
|
<div class="col">
|
|
<input type="date" name="end_date" class="form-control" value="<?= $data['end_date'] ?>">
|
|
</div>
|
|
<div class="col-auto">
|
|
<button type="submit" class="btn btn-primary">Filter</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-body">
|
|
<h4 class="text-center mb-4 d-print-none">VAT Summary Report</h4>
|
|
<table class="table">
|
|
<tr>
|
|
<th data-en="VAT Input (Purchases)" data-ar="ضريبة المدخلات (المشتريات)">VAT Input (Purchases)</th>
|
|
<td class="text-end text-success fw-bold"><?= number_format($data['vat_report']['input_vat'], 3) ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th data-en="VAT Output (Sales)" data-ar="ضريبة المخرجات (المبيعات)">VAT Output (Sales)</th>
|
|
<td class="text-end text-danger fw-bold"><?= number_format($data['vat_report']['output_vat'], 3) ?></td>
|
|
</tr>
|
|
<tr class="table-dark h5">
|
|
<th data-en="Net VAT Payable / (Refundable)" data-ar="صافي الضريبة المستحقة / (المستردة)">Net VAT Payable / (Refundable)</th>
|
|
<td class="text-end"><?= number_format($data['vat_report']['net_vat'], 3) ?></td>
|
|
</tr>
|
|
</table>
|
|
<div class="alert alert-info small mt-3">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
This report calculates the difference between VAT collected on sales and VAT paid on purchases for the selected period.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($_GET['view'] === 'trial_balance'): ?>
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th data-en="Code" data-ar="الكود">Code</th>
|
|
<th data-en="Account Name" data-ar="اسم الحساب">Account Name</th>
|
|
<th class="text-end" data-en="Debit" data-ar="مدين">Debit</th>
|
|
<th class="text-end" data-en="Credit" data-ar="دائن">Credit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$total_d = 0; $total_c = 0;
|
|
foreach ($data['trial_balance'] as $row):
|
|
$total_d += (float)$row['total_debit'];
|
|
$total_c += (float)$row['total_credit'];
|
|
?>
|
|
<tr>
|
|
<td><?= $row['code'] ?></td>
|
|
<td><?= htmlspecialchars($row['name_en']) ?></td>
|
|
<td class="text-end"><?= number_format((float)$row['total_debit'], 3) ?></td>
|
|
<td class="text-end"><?= number_format((float)$row['total_credit'], 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
<tfoot class="table-light fw-bold">
|
|
<tr>
|
|
<td colspan="2" class="text-end" data-en="Total" data-ar="الإجمالي">Total</td>
|
|
<td class="text-end"><?= number_format($total_d, 3) ?></td>
|
|
<td class="text-end"><?= number_format($total_c, 3) ?></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
<?php elseif ($_GET['view'] === 'profit_loss'): ?>
|
|
<div class="row">
|
|
<div class="col-md-8 mx-auto">
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<h4 class="text-center mb-4 d-print-none" data-en="Profit & Loss Statement" data-ar="قائمة الأرباح والخسائر">Profit & Loss Statement</h4>
|
|
<table class="table">
|
|
<tr class="table-primary"><th colspan="2" data-en="Revenue" data-ar="الإيرادات">Revenue</th></tr>
|
|
<?php
|
|
$total_rev = 0;
|
|
foreach ($data['revenue_accounts'] as $acc):
|
|
$bal = getAccountBalance($acc['code']);
|
|
if ($bal == 0) continue;
|
|
$total_rev += $bal;
|
|
?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($acc['name_en']) ?></td>
|
|
<td class="text-end"><?= number_format($bal, 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<tr class="fw-bold">
|
|
<td data-en="Total Revenue" data-ar="إجمالي الإيرادات">Total Revenue</td>
|
|
<td class="text-end border-top"><?= number_format($total_rev, 3) ?></td>
|
|
</tr>
|
|
|
|
<tr class="table-warning"><th colspan="2" class="pt-4" data-en="Expenses" data-ar="المصروفات">Expenses</th></tr>
|
|
<?php
|
|
$total_exp = 0;
|
|
foreach ($data['expense_accounts'] as $acc):
|
|
$bal = getAccountBalance($acc['code']);
|
|
if ($bal == 0) continue;
|
|
$total_exp += $bal;
|
|
?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($acc['name_en']) ?></td>
|
|
<td class="text-end"><?= number_format($bal, 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<tr class="fw-bold">
|
|
<td data-en="Total Expenses" data-ar="إجمالي المصروفات">Total Expenses</td>
|
|
<td class="text-end border-top"><?= number_format($total_exp, 3) ?></td>
|
|
</tr>
|
|
|
|
<tr class="table-success h4">
|
|
<td data-en="Net Profit / Loss" data-ar="صافي الربح / الخسارة">Net Profit / Loss</td>
|
|
<td class="text-end"><?= number_format($total_rev - $total_exp, 3) ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($_GET['view'] === 'balance_sheet'): ?>
|
|
<div class="row">
|
|
<div class="col-md-10 mx-auto">
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<h4 class="text-center mb-4 d-print-none" data-en="Balance Sheet" data-ar="الميزانية العمومية">Balance Sheet</h4>
|
|
<div class="row">
|
|
<div class="col-md-6 border-end">
|
|
<h5 class="text-primary border-bottom pb-2" data-en="Assets" data-ar="الأصول">Assets</h5>
|
|
<table class="table table-sm">
|
|
<?php
|
|
$total_assets = 0;
|
|
foreach ($data['asset_accounts'] as $acc):
|
|
$bal = getAccountBalance($acc['code']);
|
|
if ($bal == 0) continue;
|
|
$total_assets += $bal;
|
|
?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($acc['name_en']) ?></td>
|
|
<td class="text-end"><?= number_format($bal, 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<tr class="fw-bold h5">
|
|
<td data-en="Total Assets" data-ar="إجمالي الأصول">Total Assets</td>
|
|
<td class="text-end border-top"><?= number_format($total_assets, 3) ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h5 class="text-danger border-bottom pb-2" data-en="Liabilities & Equity" data-ar="الالتزامات وحقوق الملكية">Liabilities & Equity</h5>
|
|
<table class="table table-sm">
|
|
<tr class="bg-light"><td colspan="2" class="small fw-bold text-muted">Liabilities</td></tr>
|
|
<?php
|
|
$total_liab = 0;
|
|
foreach ($data['liability_accounts'] as $acc):
|
|
$bal = getAccountBalance($acc['code']);
|
|
if ($bal == 0) continue;
|
|
$total_liab += $bal;
|
|
?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($acc['name_en']) ?></td>
|
|
<td class="text-end"><?= number_format($bal, 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
|
|
<tr class="bg-light"><td colspan="2" class="small fw-bold text-muted pt-3">Equity</td></tr>
|
|
<?php
|
|
$total_equity = 0;
|
|
foreach ($data['equity_accounts'] as $acc):
|
|
$bal = getAccountBalance($acc['code']);
|
|
if ($bal == 0) continue;
|
|
$total_equity += $bal;
|
|
?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($acc['name_en']) ?></td>
|
|
<td class="text-end"><?= number_format($bal, 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
|
|
<?php
|
|
// Current Year Earnings (Revenue - Expenses)
|
|
$rev = 0; foreach(db()->query("SELECT code FROM acc_accounts WHERE type='revenue' AND parent_id IS NOT NULL")->fetchAll() as $a) $rev += getAccountBalance($a['code']);
|
|
$exp = 0; foreach(db()->query("SELECT code FROM acc_accounts WHERE type='expense' AND parent_id IS NOT NULL")->fetchAll() as $a) $exp += getAccountBalance($a['code']);
|
|
$earnings = $rev - $exp;
|
|
$total_equity += $earnings;
|
|
?>
|
|
<tr>
|
|
<td data-en="Retained Earnings (Current)" data-ar="الأرباح المحتجزة (الحالية)">Retained Earnings (Current)</td>
|
|
<td class="text-end"><?= number_format($earnings, 3) ?></td>
|
|
</tr>
|
|
|
|
<tr class="fw-bold h5 pt-3">
|
|
<td data-en="Total Liab. & Equity" data-ar="إجمالي الالتزامات وحقوق الملكية">Total Liab. & Equity</td>
|
|
<td class="text-end border-top"><?= number_format($total_liab + $total_equity, 3) ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Journal Entry Details Modal -->
|
|
<div class="modal fade" id="journalModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Journal Entry Details" data-ar="تفاصيل قيد اليومية">Journal Entry Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Account" data-ar="الحساب">Account</th>
|
|
<th class="text-end" data-en="Debit" data-ar="مدين">Debit</th>
|
|
<th class="text-end" data-en="Credit" data-ar="دائن">Credit</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="journalDetailsBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function viewJournalEntry(id) {
|
|
fetch('index.php?page=accounting&action=get_entry_details&id=' + id)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
let html = '';
|
|
data.forEach(row => {
|
|
html += `<tr>
|
|
<td>${row.code} - ${row.name_en}</td>
|
|
<td class="text-end">${parseFloat(row.debit).toFixed(3)}</td>
|
|
<td class="text-end">${parseFloat(row.credit).toFixed(3)}</td>
|
|
</tr>`;
|
|
});
|
|
document.getElementById('journalDetailsBody').innerHTML = html;
|
|
new bootstrap.Modal(document.getElementById('journalModal')).show();
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<?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 select2" 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 elseif ($page === 'sales_returns'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Sales Returns" data-ar="مرتجع المبيعات">Sales Returns</h5>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addSalesReturnModal" id="createSalesReturnBtn">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Create New Return" data-ar="إنشاء مرتجع جديد">Create New Return</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="sales_returns">
|
|
<div class="col-md-9">
|
|
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Search by Return ID, Customer or Invoice ID...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<button type="submit" class="btn btn-primary btn-sm w-100">Filter</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Return #" data-ar="رقم المرتجع">Return #</th>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Invoice #" data-ar="رقم الفاتورة">Invoice #</th>
|
|
<th data-en="Customer" data-ar="العميل">Customer</th>
|
|
<th data-en="Total Amount" data-ar="إجمالي المرتجع" class="text-end">Total Amount</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['returns'] as $ret): ?>
|
|
<tr>
|
|
<td>RET-<?= str_pad((string)$ret['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= $ret['return_date'] ?></td>
|
|
<td>INV-<?= str_pad((string)$ret['invoice_id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= htmlspecialchars($ret['customer_name'] ?? 'Walk-in') ?></td>
|
|
<td class="text-end fw-bold text-danger">OMR <?= number_format((float)$ret['total_amount'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info view-return-btn" data-id="<?= $ret['id'] ?>"><i class="bi bi-eye"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($data['returns'])): ?>
|
|
<tr><td colspan="6" class="text-center py-4 text-muted">No returns found</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'purchase_returns'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Purchase Returns" data-ar="مرتجع المشتريات">Purchase Returns</h5>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPurchaseReturnModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Create New Return" data-ar="إنشاء مرتجع جديد">Create New Return</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="purchase_returns">
|
|
<div class="col-md-9">
|
|
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Search by Return ID, Supplier or Invoice ID...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<button type="submit" class="btn btn-primary btn-sm w-100">Filter</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Return #" data-ar="رقم المرتجع">Return #</th>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Invoice #" data-ar="رقم الفاتورة">Invoice #</th>
|
|
<th data-en="Supplier" data-ar="المورد">Supplier</th>
|
|
<th data-en="Total Amount" data-ar="إجمالي المرتجع" class="text-end">Total Amount</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['returns'] as $ret): ?>
|
|
<tr>
|
|
<td>PRET-<?= str_pad((string)$ret['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= $ret['return_date'] ?></td>
|
|
<td>INV-<?= str_pad((string)$ret['invoice_id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= htmlspecialchars($ret['supplier_name'] ?? 'Unknown') ?></td>
|
|
<td class="text-end fw-bold text-danger">OMR <?= number_format((float)$ret['total_amount'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info view-return-btn" data-id="<?= $ret['id'] ?>"><i class="bi bi-eye"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($data['returns'])): ?>
|
|
<tr><td colspan="6" class="text-center py-4 text-muted">No returns found</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'hr_departments'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="HR Departments" data-ar="أقسام الموارد البشرية">HR Departments</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addHrDepartmentModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Department" data-ar="إضافة قسم">Add Department</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="Department Name" data-ar="اسم القسم">Department Name</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['departments'] as $d): ?>
|
|
<tr>
|
|
<td><?= $d['id'] ?></td>
|
|
<td><?= htmlspecialchars($d['name']) ?></td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editHrDepartmentModal<?= $d['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="<?= $d['id'] ?>">
|
|
<button type="submit" name="delete_hr_department" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<!-- Edit Dept Modal -->
|
|
<div class="modal fade" id="editHrDepartmentModal<?= $d['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Edit Department" data-ar="تعديل القسم">Edit Department</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Department Name" data-ar="اسم القسم">Department Name</label>
|
|
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($d['name']) ?>" 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="edit_hr_department" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'hr_employees'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="HR Employees" data-ar="موظفي الموارد البشرية">HR Employees</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addHrEmployeeModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Employee" data-ar="إضافة موظف">Add Employee</span>
|
|
</button>
|
|
</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="Biometric ID" data-ar="معرف البصمة">Biometric ID</th>
|
|
<th data-en="Department" data-ar="القسم">Department</th>
|
|
<th data-en="Position" data-ar="المنصب">Position</th>
|
|
<th data-en="Salary" data-ar="الراتب">Salary</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['employees'] as $e): ?>
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold"><?= htmlspecialchars($e['name']) ?></div>
|
|
<div class="small text-muted"><?= htmlspecialchars($e['email']) ?></div>
|
|
</td>
|
|
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars($e['biometric_id'] ?? '---') ?></span></td>
|
|
<td><?= htmlspecialchars($e['dept_name'] ?? '---') ?></td>
|
|
<td><?= htmlspecialchars($e['position']) ?></td>
|
|
<td>OMR <?= number_format($e['salary'], 3) ?></td>
|
|
<td>
|
|
<span class="badge <?= $e['status'] === 'active' ? 'bg-success' : 'bg-danger' ?> text-uppercase">
|
|
<?= $e['status'] ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editHrEmployeeModal<?= $e['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="<?= $e['id'] ?>">
|
|
<button type="submit" name="delete_hr_employee" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<!-- Edit Employee Modal -->
|
|
<div class="modal fade" id="editHrEmployeeModal<?= $e['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 Employee" data-ar="تعديل الموظف">Edit Employee</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $e['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Full Name" data-ar="الاسم الكامل">Full Name</label>
|
|
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($e['name']) ?>" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Department" data-ar="القسم">Department</label>
|
|
<select name="department_id" class="form-select">
|
|
<option value="">--- Select ---</option>
|
|
<?php foreach ($data['departments'] as $d): ?>
|
|
<option value="<?= $d['id'] ?>" <?= $e['department_id'] == $d['id'] ? 'selected' : '' ?>><?= htmlspecialchars($d['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Email" data-ar="البريد">Email</label>
|
|
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($e['email']) ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars($e['phone']) ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Position" data-ar="المنصب">Position</label>
|
|
<input type="text" name="position" class="form-control" value="<?= htmlspecialchars($e['position']) ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Basic Salary" data-ar="الراتب الأساسي">Basic Salary</label>
|
|
<input type="number" step="0.001" name="salary" class="form-control" value="<?= $e['salary'] ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Biometric ID" data-ar="معرف البصمة">Biometric ID</label>
|
|
<input type="text" name="biometric_id" class="form-control" value="<?= htmlspecialchars($e['biometric_id'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Joining Date" data-ar="تاريخ الانضمام">Joining Date</label>
|
|
<input type="date" name="joining_date" class="form-control" value="<?= $e['joining_date'] ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Status" data-ar="الحالة">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="active" <?= $e['status'] === 'active' ? 'selected' : '' ?>>Active</option>
|
|
<option value="inactive" <?= $e['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
|
|
</select>
|
|
</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="edit_hr_employee" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'hr_attendance'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="HR Attendance" data-ar="حضور الموارد البشرية">HR Attendance</h5>
|
|
<div class="d-flex gap-2">
|
|
<form method="POST" class="d-inline">
|
|
<button type="submit" name="pull_biometric_data" class="btn btn-primary btn-sm">
|
|
<i class="bi bi-cloud-download"></i> <span data-en="Pull Data from Devices" data-ar="سحب البيانات من الأجهزة">Pull Data from Devices</span>
|
|
</button>
|
|
</form>
|
|
<button class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#biometricInfoModal">
|
|
<i class="bi bi-fingerprint"></i> <span data-en="Biometric Sync" data-ar="مزامنة البصمة">Biometric Sync</span>
|
|
</button>
|
|
<form method="GET" class="d-flex gap-2">
|
|
<input type="hidden" name="page" value="hr_attendance">
|
|
<input type="date" name="date" class="form-control form-control-sm" value="<?= $data['attendance_date'] ?>" onchange="this.form.submit()">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Employee" data-ar="الموظف">Employee</th>
|
|
<th data-en="Department" data-ar="القسم">Department</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Clock In" data-ar="وقت الدخول">Clock In</th>
|
|
<th data-en="Clock Out" data-ar="وقت الخروج">Clock Out</th>
|
|
<th data-en="Action" data-ar="إجراء" class="text-end">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['employees'] as $e): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($e['name']) ?></td>
|
|
<td><?= htmlspecialchars($e['dept_name'] ?? '---') ?></td>
|
|
<td>
|
|
<?php if ($e['status']): ?>
|
|
<span class="badge <?= $e['status'] === 'present' ? 'bg-success' : ($e['status'] === 'absent' ? 'bg-danger' : 'bg-warning') ?> text-uppercase">
|
|
<?= $e['status'] ?>
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="badge bg-secondary text-uppercase">Not Marked</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= $e['clock_in'] ?? '---' ?></td>
|
|
<td><?= $e['clock_out'] ?? '---' ?></td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#markAttendanceModal<?= $e['id'] ?>">
|
|
<i class="bi bi-calendar-check"></i> <span data-en="Mark" data-ar="تسجيل">Mark</span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<!-- Attendance Modal -->
|
|
<div class="modal fade" id="markAttendanceModal<?= $e['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Mark Attendance" data-ar="تسجيل الحضور">Mark Attendance - <?= htmlspecialchars($e['name']) ?></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="employee_id" value="<?= $e['id'] ?>">
|
|
<input type="hidden" name="attendance_date" value="<?= $data['attendance_date'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Status" data-ar="الحالة">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="present" <?= $e['status'] === 'present' ? 'selected' : '' ?>>Present</option>
|
|
<option value="absent" <?= $e['status'] === 'absent' ? 'selected' : '' ?>>Absent</option>
|
|
<option value="on_leave" <?= $e['status'] === 'on_leave' ? 'selected' : '' ?>>On Leave</option>
|
|
</select>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Clock In" data-ar="وقت الدخول">Clock In</label>
|
|
<input type="time" name="clock_in" class="form-control" value="<?= $e['clock_in'] ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Clock Out" data-ar="وقت الخروج">Clock Out</label>
|
|
<input type="time" name="clock_out" class="form-control" value="<?= $e['clock_out'] ?>">
|
|
</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="mark_attendance" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Biometric Info Modal -->
|
|
<div class="modal fade" id="biometricInfoModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Biometric Integration Info" data-ar="معلومات تكامل البصمة">Biometric Integration Info</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p data-en="To sync attendance from your biometric device, use the following API endpoint:" data-ar="لمزامنة الحضور من جهاز البصمة الخاص بك، استخدم نقطة نهاية API التالية:">
|
|
To sync attendance from your biometric device, use the following API endpoint:
|
|
</p>
|
|
<div class="bg-light p-3 rounded mb-3 border">
|
|
<code><?= (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" ?>/api/biometric_sync.php</code>
|
|
</div>
|
|
<p data-en="Expected JSON format:" data-ar="تنسيق JSON المتوقع:">Expected JSON format:</p>
|
|
<pre class="bg-dark text-light p-3 rounded">
|
|
[
|
|
{
|
|
"biometric_id": "101",
|
|
"device_id": 1,
|
|
"timestamp": "2026-02-17 08:30:00",
|
|
"type": "in"
|
|
},
|
|
{
|
|
"biometric_id": "101",
|
|
"device_id": 1,
|
|
"timestamp": "2026-02-17 17:30:00",
|
|
"type": "out"
|
|
}
|
|
]
|
|
</pre>
|
|
<p class="small text-muted" data-en="Note: Ensure Employee Biometric IDs match those in the device logs." data-ar="ملاحظة: تأكد من مطابقة معرفات الموظفين الحيوية مع تلك الموجودة في سجلات الجهاز.">
|
|
Note: Ensure Employee Biometric IDs match those in the device logs.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'hr_payroll'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="HR Payroll" data-ar="رواتب الموارد البشرية">HR Payroll</h5>
|
|
<div class="d-flex gap-2">
|
|
<form method="GET" class="d-flex gap-2">
|
|
<input type="hidden" name="page" value="hr_payroll">
|
|
<select name="month" class="form-select form-select-sm" onchange="this.form.submit()">
|
|
<?php for($m=1; $m<=12; $m++): ?>
|
|
<option value="<?= $m ?>" <?= $data['month'] == $m ? 'selected' : '' ?>><?= date('F', mktime(0, 0, 0, $m, 1)) ?></option>
|
|
<?php endfor; ?>
|
|
</select>
|
|
<select name="year" class="form-select form-select-sm" onchange="this.form.submit()">
|
|
<?php for($y=date('Y'); $y>=date('Y')-2; $y--): ?>
|
|
<option value="<?= $y ?>" <?= $data['year'] == $y ? 'selected' : '' ?>><?= $y ?></option>
|
|
<?php endfor; ?>
|
|
</select>
|
|
</form>
|
|
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#generatePayrollModal">
|
|
<i class="bi bi-gear"></i> <span data-en="Generate" data-ar="توليد">Generate</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Employee" data-ar="الموظف">Employee</th>
|
|
<th data-en="Basic" data-ar="الأساسي">Basic</th>
|
|
<th data-en="Bonus" data-ar="مكافأة">Bonus</th>
|
|
<th data-en="Deductions" data-ar="استقطاعات">Deductions</th>
|
|
<th data-en="Net Salary" data-ar="صافي الراتب">Net Salary</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['payroll'] as $p): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($p['emp_name']) ?></td>
|
|
<td>OMR <?= number_format($p['basic_salary'], 3) ?></td>
|
|
<td class="text-success">+ OMR <?= number_format($p['bonus'], 3) ?></td>
|
|
<td class="text-danger">- OMR <?= number_format($p['deductions'], 3) ?></td>
|
|
<td class="fw-bold">OMR <?= number_format($p['net_salary'], 3) ?></td>
|
|
<td>
|
|
<span class="badge <?= $p['status'] === 'paid' ? 'bg-success' : 'bg-warning' ?> text-uppercase">
|
|
<?= $p['status'] ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<?php if ($p['status'] === 'pending'): ?>
|
|
<form method="POST" class="d-inline">
|
|
<input type="hidden" name="id" value="<?= $p['id'] ?>">
|
|
<button type="submit" name="pay_payroll" class="btn btn-sm btn-success" title="Mark Paid"><i class="bi bi-check-circle"></i></button>
|
|
</form>
|
|
<?php endif; ?>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $p['id'] ?>">
|
|
<button type="submit" name="delete_payroll" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Generate Payroll Modal -->
|
|
<div class="modal fade" id="generatePayrollModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Generate Payroll" data-ar="توليد الرواتب">Generate Payroll</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="month" value="<?= $data['month'] ?>">
|
|
<input type="hidden" name="year" value="<?= $data['year'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Employee" data-ar="الموظف">Employee</label>
|
|
<select name="employee_id" class="form-select select2" required>
|
|
<option value="">--- Select ---</option>
|
|
<?php foreach ($data['employees'] as $e): ?>
|
|
<option value="<?= $e['id'] ?>"><?= htmlspecialchars($e['name']) ?> (Basic: <?= number_format($e['salary'], 3) ?>)</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Bonus" data-ar="مكافأة">Bonus</label>
|
|
<input type="number" step="0.001" name="bonus" class="form-control" value="0.000">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Deductions" data-ar="استقطاعات">Deductions</label>
|
|
<input type="number" step="0.001" name="deductions" class="form-control" value="0.000">
|
|
</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="generate_payroll" class="btn btn-primary" data-en="Generate" data-ar="توليد">Generate</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'devices'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Biometric Devices" data-ar="أجهزة البصمة">Biometric Devices</h5>
|
|
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addDeviceModal">
|
|
<i class="bi bi-plus-circle"></i> <span data-en="Add Device" data-ar="إضافة جهاز">Add Device</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Device Name" data-ar="اسم الجهاز">Device Name</th>
|
|
<th data-en="IP / IO Address" data-ar="عنوان IP / IO">IP / IO Address</th>
|
|
<th data-en="Port" data-ar="المنفذ">Port</th>
|
|
<th data-en="Serial" data-ar="الرقم التسلسلي">Serial</th>
|
|
<th data-en="Last Sync" data-ar="آخر مزامنة">Last Sync</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['devices'] as $d): ?>
|
|
<tr>
|
|
<td>
|
|
<strong><?= htmlspecialchars($d['device_name']) ?></strong>
|
|
</td>
|
|
<td>
|
|
<div><small class="text-muted">IP:</small> <?= htmlspecialchars($d['ip_address']) ?></div>
|
|
<?php if ($d['io_address']): ?>
|
|
<div><small class="text-muted">IO:</small> <?= htmlspecialchars($d['io_address']) ?></div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= $d['port'] ?></td>
|
|
<td><?= htmlspecialchars($d['serial_number'] ?? '---') ?></td>
|
|
<td><?= $d['last_sync'] ?? 'Never' ?></td>
|
|
<td>
|
|
<span class="badge <?= $d['status'] === 'active' ? 'bg-success' : 'bg-danger' ?> text-uppercase">
|
|
<?= $d['status'] ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<form method="POST" class="d-inline">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<button type="submit" name="test_device_connection" class="btn btn-sm btn-outline-info" title="Test Connection"><i class="bi bi-broadcast"></i></button>
|
|
</form>
|
|
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editDeviceModal<?= $d['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="<?= $d['id'] ?>">
|
|
<button type="submit" name="delete_biometric_device" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Edit Device Modal -->
|
|
<div class="modal fade" id="editDeviceModal<?= $d['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Edit Device" data-ar="تعديل الجهاز">Edit Device</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Device Name" data-ar="اسم الجهاز">Device Name</label>
|
|
<input type="text" name="device_name" class="form-control" value="<?= htmlspecialchars($d['device_name']) ?>" required>
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-8">
|
|
<label class="form-label" data-en="IP Address" data-ar="عنوان IP">IP Address</label>
|
|
<input type="text" name="ip_address" class="form-control" value="<?= htmlspecialchars($d['ip_address']) ?>" required>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Port" data-ar="المنفذ">Port</label>
|
|
<input type="number" name="port" class="form-control" value="<?= $d['port'] ?>" required>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="IO Address" data-ar="عنوان IO">IO Address</label>
|
|
<input type="text" name="io_address" class="form-control" value="<?= htmlspecialchars($d['io_address']) ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Serial Number" data-ar="الرقم التسلسلي">Serial Number</label>
|
|
<input type="text" name="serial_number" class="form-control" value="<?= htmlspecialchars($d['serial_number']) ?>">
|
|
</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_biometric_device" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Device Modal -->
|
|
<div class="modal fade" id="addDeviceModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add Biometric Device" data-ar="إضافة جهاز بصمة">Add Biometric Device</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Device Name" data-ar="اسم الجهاز">Device Name</label>
|
|
<input type="text" name="device_name" class="form-control" required placeholder="e.g. Main Entrance">
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-8">
|
|
<label class="form-label" data-en="IP Address" data-ar="عنوان IP">IP Address</label>
|
|
<input type="text" name="ip_address" class="form-control" required placeholder="192.168.1.201">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Port" data-ar="المنفذ">Port</label>
|
|
<input type="number" name="port" class="form-control" value="4370" required>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="IO Address" data-ar="عنوان IO">IO Address</label>
|
|
<input type="text" name="io_address" class="form-control" placeholder="Optional IO address">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Serial Number" data-ar="الرقم التسلسلي">Serial Number</label>
|
|
<input type="text" name="serial_number" class="form-control" placeholder="Device Serial Number">
|
|
</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_biometric_device" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'settings'): ?>
|
|
<div class="card p-4">
|
|
<h5 class="mb-4" data-en="Company Profile" data-ar="ملف الشركة">Company Profile</h5>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Company Name" data-ar="اسم الشركة">Company Name</label>
|
|
<input type="text" name="settings[company_name]" class="form-control" value="<?= htmlspecialchars($data['settings']['company_name'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="settings[company_phone]" class="form-control" value="<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Email" data-ar="البريد الإلكتروني">Email</label>
|
|
<input type="email" name="settings[company_email]" class="form-control" value="<?= htmlspecialchars($data['settings']['company_email'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="VAT Number" data-ar="الرقم الضريبي">VAT Number</label>
|
|
<input type="text" name="settings[vat_number]" class="form-control" value="<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label" data-en="Address" data-ar="العنوان">Address</label>
|
|
<textarea name="settings[company_address]" class="form-control" rows="3"><?= htmlspecialchars($data['settings']['company_address'] ?? '') ?></textarea>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Company Logo" data-ar="شعار الشركة">Company Logo</label>
|
|
<input type="file" name="company_logo" class="form-control" accept="image/*">
|
|
<?php if (!empty($data['settings']['company_logo'])): ?>
|
|
<div class="mt-2">
|
|
<img src="<?= htmlspecialchars($data['settings']['company_logo']) ?>?v=<?= time() ?>" alt="Logo" class="img-thumbnail" style="max-height: 80px;">
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Favicon" data-ar="أيقونة الموقع">Favicon</label>
|
|
<input type="file" name="favicon" class="form-control" accept="image/*">
|
|
<?php if (!empty($data['settings']['favicon'])): ?>
|
|
<div class="mt-2">
|
|
<img src="<?= htmlspecialchars($data['settings']['favicon']) ?>?v=<?= time() ?>" alt="Favicon" class="img-thumbnail" style="max-height: 32px;">
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Manager Signature" data-ar="توقيع المدير">Manager Signature</label>
|
|
<input type="file" name="manager_signature" class="form-control" accept="image/*">
|
|
<?php if (!empty($data['settings']['manager_signature'])): ?>
|
|
<div class="mt-2">
|
|
<img src="<?= htmlspecialchars($data['settings']['manager_signature']) ?>?v=<?= time() ?>" alt="Signature" class="img-thumbnail" style="max-height: 80px;">
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="col-md-12 mt-4">
|
|
<h5 class="mb-3" data-en="Loyalty Configuration" data-ar="إعدادات الولاء">Loyalty Configuration</h5>
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Loyalty System" data-ar="نظام الولاء">Loyalty System</label>
|
|
<select name="settings[loyalty_enabled]" class="form-select">
|
|
<option value="0" <?= ($data['settings']['loyalty_enabled'] ?? '0') === '0' ? 'selected' : '' ?> data-en="Disabled" data-ar="معطل">Disabled</option>
|
|
<option value="1" <?= ($data['settings']['loyalty_enabled'] ?? '0') === '1' ? 'selected' : '' ?> data-en="Enabled" data-ar="مفعل">Enabled</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Points per 1 OMR" data-ar="النقاط لكل 1 ريال">Points per 1 OMR</label>
|
|
<input type="number" step="0.01" name="settings[loyalty_points_per_unit]" class="form-control" value="<?= htmlspecialchars($data['settings']['loyalty_points_per_unit'] ?? '1') ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Points for 1 OMR Discount" data-ar="النقاط لخصم 1 ريال">Points for 1 OMR Discount</label>
|
|
<input type="number" step="0.01" name="settings[loyalty_redeem_points_per_unit]" class="form-control" value="<?= htmlspecialchars($data['settings']['loyalty_redeem_points_per_unit'] ?? '100') ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-12 mt-4">
|
|
<button type="submit" name="update_settings" class="btn btn-primary">
|
|
<i class="bi bi-save"></i> <span data-en="Save Changes" data-ar="حفظ التغييرات">Save Changes</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Add HR Department Modal -->
|
|
<div class="modal fade" id="addHrDepartmentModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add HR Department" data-ar="إضافة قسم">Add HR Department</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="Department Name" data-ar="اسم القسم">Department Name</label>
|
|
<input type="text" name="name" 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_hr_department" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add HR Employee Modal -->
|
|
<div class="modal fade" id="addHrEmployeeModal" 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 HR Employee" data-ar="إضافة موظف">Add HR Employee</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="Full Name" data-ar="الاسم الكامل">Full Name</label>
|
|
<input type="text" name="name" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Department" data-ar="القسم">Department</label>
|
|
<select name="department_id" class="form-select">
|
|
<option value="">--- Select ---</option>
|
|
<?php foreach ($data['departments'] ?? [] as $d): ?>
|
|
<option value="<?= $d['id'] ?>"><?= htmlspecialchars($d['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Email" data-ar="البريد">Email</label>
|
|
<input type="email" name="email" class="form-control">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="phone" class="form-control">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Position" data-ar="المنصب">Position</label>
|
|
<input type="text" name="position" class="form-control">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Basic Salary" data-ar="الراتب الأساسي">Basic Salary</label>
|
|
<input type="number" step="0.001" name="salary" class="form-control" value="0.000">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Biometric ID" data-ar="معرف البصمة">Biometric ID</label>
|
|
<input type="text" name="biometric_id" class="form-control">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Joining Date" data-ar="تاريخ الانضمام">Joining Date</label>
|
|
<input type="date" name="joining_date" class="form-control" value="<?= date('Y-m-d') ?>">
|
|
</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_hr_employee" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</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 class="col-12 mt-3">
|
|
<hr>
|
|
<h6 data-en="Promotion Details" data-ar="تفاصيل العرض">Promotion Details</h6>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="form-check form-switch mt-4">
|
|
<input class="form-check-input" type="checkbox" name="is_promotion" id="isPromotionToggle">
|
|
<label class="form-check-label" for="isPromotionToggle" data-en="On Promotion?" data-ar="في العرض؟">On Promotion?</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="row g-3" id="promotionFieldsContainer" style="display: none;">
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Start Date" data-ar="تاريخ البدء">Start Date</label>
|
|
<input type="date" name="promotion_start" class="form-control">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="End Date" data-ar="تاريخ الانتهاء">End Date</label>
|
|
<input type="date" name="promotion_end" class="form-control">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Percent (%)" data-ar="النسبة (%)">Percent (%)</label>
|
|
<input type="number" step="0.01" name="promotion_percent" class="form-control" value="0.00">
|
|
</div>
|
|
</div>
|
|
</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() {
|
|
console.log("DOM Content Loaded - Accounting System");
|
|
try {
|
|
// Initialize Select2 for all searchable dropdowns
|
|
$('.select2').each(function() {
|
|
$(this).select2({
|
|
width: '100%',
|
|
dropdownParent: $(this).closest('.modal').length ? $(this).closest('.modal') : $(document.body)
|
|
});
|
|
});
|
|
|
|
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';
|
|
});
|
|
}
|
|
|
|
const isPromotionToggle = document.getElementById('isPromotionToggle');
|
|
const promotionFieldsContainer = document.getElementById('promotionFieldsContainer');
|
|
if (isPromotionToggle && promotionFieldsContainer) {
|
|
isPromotionToggle.addEventListener('change', function() {
|
|
promotionFieldsContainer.style.display = this.checked ? 'flex' : 'none';
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.isPromotionToggleEdit').forEach(toggle => {
|
|
toggle.addEventListener('change', function() {
|
|
const id = this.getAttribute('data-id');
|
|
const container = document.getElementById('promotionFieldsContainerEdit' + id);
|
|
if (container) {
|
|
container.style.display = this.checked ? 'flex' : 'none';
|
|
}
|
|
});
|
|
});
|
|
|
|
// 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']);
|
|
?>
|
|
if (typeof showReceipt === 'function') {
|
|
showReceipt(<?= $rid ?>);
|
|
} else {
|
|
setTimeout(() => { if (typeof showReceipt === 'function') showReceipt(<?= $rid ?>); }, 500);
|
|
}
|
|
<?php endif; ?>
|
|
|
|
function showReceipt(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);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Return Logic (General for Sales and Purchase)
|
|
const setupReturnLogic = (selectId, containerId, tbodyId, totalDisplayId, submitBtnId) => {
|
|
const select = document.getElementById(selectId);
|
|
if (!select) return;
|
|
|
|
const calculateTotal = function() {
|
|
let total = 0;
|
|
document.querySelectorAll('#' + tbodyId + ' tr').forEach(row => {
|
|
const qtyInput = row.querySelector('.return-qty-input');
|
|
if (!qtyInput) return;
|
|
const qty = parseFloat(qtyInput.value) || 0;
|
|
const price = parseFloat(qtyInput.dataset.price) || 0;
|
|
const lineTotal = qty * price;
|
|
const lineTotalDisplay = row.querySelector('.line-total');
|
|
if (lineTotalDisplay) {
|
|
lineTotalDisplay.innerText = lineTotal.toFixed(3);
|
|
}
|
|
total += lineTotal;
|
|
});
|
|
const totalDisplay = document.getElementById(totalDisplayId);
|
|
if (totalDisplay) {
|
|
totalDisplay.innerText = 'OMR ' + total.toFixed(3);
|
|
}
|
|
const submitBtn = document.getElementById(submitBtnId);
|
|
if (submitBtn) {
|
|
submitBtn.disabled = total <= 0;
|
|
}
|
|
};
|
|
|
|
const handleInvoiceChange = async function() {
|
|
const invoiceId = select.value;
|
|
const container = document.getElementById(containerId);
|
|
const tbody = document.getElementById(tbodyId);
|
|
const submitBtn = document.getElementById(submitBtnId);
|
|
|
|
if (!invoiceId) {
|
|
if (container) container.style.display = 'none';
|
|
if (submitBtn) submitBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
if (tbody) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm text-primary"></div> Loading items...</td></tr>';
|
|
}
|
|
if (container) container.style.display = 'block';
|
|
|
|
try {
|
|
const resp = await fetch(`index.php?action=get_invoice_items&invoice_id=${invoiceId}`);
|
|
const items = await resp.json();
|
|
|
|
if (tbody) {
|
|
tbody.innerHTML = '';
|
|
if (items.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No items found for this invoice.</td></tr>';
|
|
} else {
|
|
let html = '';
|
|
items.forEach(item => {
|
|
html += `
|
|
<tr>
|
|
<td>${item.name_en}<br><small class="text-muted">${item.sku}</small></td>
|
|
<td>${parseFloat(item.quantity).toFixed(2)}</td>
|
|
<td>
|
|
<input type="number" name="quantities[]" class="form-control form-control-sm return-qty-input"
|
|
step="0.01" min="0" max="${item.quantity}" value="0"
|
|
data-price="${item.unit_price}">
|
|
<input type="hidden" name="item_ids[]" value="${item.item_id}">
|
|
<input type="hidden" name="prices[]" value="${item.unit_price}">
|
|
</td>
|
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
|
<td class="text-end line-total">0.000</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
tbody.innerHTML = html;
|
|
}
|
|
}
|
|
|
|
if (submitBtn) submitBtn.disabled = true;
|
|
|
|
const qtyInputs = tbody.querySelectorAll('.return-qty-input');
|
|
qtyInputs.forEach(input => {
|
|
['input', 'change', 'keyup'].forEach(evt => input.addEventListener(evt, calculateTotal));
|
|
});
|
|
|
|
calculateTotal();
|
|
} catch (e) {
|
|
console.error(e);
|
|
if (window.Swal) Swal.fire('Error', 'Failed to fetch invoice items', 'error');
|
|
}
|
|
};
|
|
|
|
select.addEventListener('change', handleInvoiceChange);
|
|
if (window.jQuery && jQuery.fn.select2) {
|
|
$(select).on('select2:select change', handleInvoiceChange);
|
|
}
|
|
};
|
|
|
|
setupReturnLogic('return_invoice_select', 'return_items_container', 'return_items_tbody', 'return_total_display', 'submit_return_btn');
|
|
setupReturnLogic('purchase_return_invoice_select', 'purchase_return_items_container', 'purchase_return_items_tbody', 'purchase_return_total_display', 'purchase_submit_return_btn');
|
|
|
|
// Return Invoice Button from Sales/Purchases list
|
|
document.querySelectorAll('.return-invoice-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const invoiceId = this.dataset.id;
|
|
const targetModal = this.dataset.bsTarget;
|
|
const selectId = targetModal === '#addSalesReturnModal' ? 'return_invoice_select' : 'purchase_return_invoice_select';
|
|
const select = document.getElementById(selectId);
|
|
if (select) {
|
|
$(select).val(invoiceId).trigger('change');
|
|
}
|
|
});
|
|
});
|
|
|
|
// View Return Logic
|
|
document.querySelectorAll('.view-return-btn').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
const returnId = this.dataset.id;
|
|
const type = '<?= $page === "purchase_returns" ? "purchase" : "sale" ?>';
|
|
const modal = new bootstrap.Modal(document.getElementById('viewReturnDetailsModal'));
|
|
|
|
try {
|
|
const resp = await fetch(`index.php?action=get_return_details&return_id=${returnId}&type=${type}`);
|
|
const data = await resp.json();
|
|
|
|
if (data) {
|
|
document.getElementById('view_return_no').innerText = (type === 'purchase' ? 'PRET-' : 'SRET-') + String(data.id).padStart(5, '0');
|
|
document.getElementById('view_return_party').innerText = data.party_name;
|
|
document.getElementById('view_return_date').innerText = data.return_date;
|
|
document.getElementById('view_return_invoice').innerText = 'INV-' + String(data.invoice_id).padStart(5, '0');
|
|
document.getElementById('view_return_total').innerText = 'OMR ' + parseFloat(data.total_amount).toFixed(3);
|
|
document.getElementById('view_return_notes').innerText = data.notes || 'No notes';
|
|
|
|
let itemsHtml = '';
|
|
data.items.forEach(item => {
|
|
itemsHtml += `
|
|
<tr>
|
|
<td>${item.name_en}<br><small class="text-muted">${item.sku}</small></td>
|
|
<td class="text-center">${parseFloat(item.quantity).toFixed(2)}</td>
|
|
<td class="text-end">${parseFloat(item.unit_price).toFixed(3)}</td>
|
|
<td class="text-end">${parseFloat(item.total_price).toFixed(3)}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
document.getElementById('view_return_items_tbody').innerHTML = itemsHtml;
|
|
modal.show();
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
Swal.fire('Error', 'Failed to fetch return details', 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
} catch (e) { console.error("JS Error in DOMContentLoaded:", e); }
|
|
});
|
|
</script>
|
|
<!-- View Return Details Modal -->
|
|
<div class="modal fade" id="viewReturnDetailsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content text-start border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Return Details <span id="view_return_no"></span></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row mb-4">
|
|
<div class="col-6">
|
|
<label class="text-muted small d-block">Party Name</label>
|
|
<div id="view_return_party" class="fw-bold"></div>
|
|
</div>
|
|
<div class="col-3">
|
|
<label class="text-muted small d-block">Return Date</label>
|
|
<div id="view_return_date"></div>
|
|
</div>
|
|
<div class="col-3">
|
|
<label class="text-muted small d-block">Against Invoice</label>
|
|
<div id="view_return_invoice"></div>
|
|
</div>
|
|
</div>
|
|
<h6>Returned Items</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-bordered">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th>Item</th>
|
|
<th class="text-center">Returned Qty</th>
|
|
<th class="text-end">Unit Price</th>
|
|
<th class="text-end">Total Price</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="view_return_items_tbody"></tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<th colspan="3" class="text-end">Total Amount:</th>
|
|
<th class="text-end fw-bold" id="view_return_total"></th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
<div class="mt-3">
|
|
<label class="text-muted small d-block">Notes</label>
|
|
<div id="view_return_notes" class="p-2 bg-light rounded"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($page === 'sales_returns' || $page === 'purchase_returns'): ?>
|
|
<div class="modal fade" id="addPurchaseReturnModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content text-start border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Process Purchase Return</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 mb-4 text-start">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Select Invoice" data-ar="اختر الفاتورة">Select Invoice</label>
|
|
<select name="invoice_id" id="purchase_return_invoice_select" class="form-select select2" required>
|
|
<option value="">Choose Invoice...</option>
|
|
<?php if (!empty($data['purchase_invoices'])): ?>
|
|
<?php foreach ($data['purchase_invoices'] as $inv): ?>
|
|
<option value="<?= $inv['id'] ?>">INV-<?= str_pad((string)$inv['id'], 5, '0', STR_PAD_LEFT) ?> (<?= $inv['invoice_date'] ?>) - OMR <?= number_format((float)$inv['total_with_vat'], 3) ?></option>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Return Date</label>
|
|
<input type="date" name="return_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="purchase_return_items_container" style="display:none;" class="text-start">
|
|
<h6>Items for Return</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-bordered">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th>Item</th>
|
|
<th>Purchased Qty</th>
|
|
<th>Return Qty</th>
|
|
<th>Price</th>
|
|
<th>Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="purchase_return_items_tbody"></tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<th colspan="4" class="text-end">Total Return Amount:</th>
|
|
<th class="text-end" id="purchase_return_total_display">OMR 0.000</th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Notes / Reason for Return</label>
|
|
<textarea name="notes" class="form-control" rows="2"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" name="add_purchase_return" class="btn btn-danger" id="purchase_submit_return_btn" disabled>Process Return</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Add Sales Return Modal -->
|
|
<div class="modal fade" id="addSalesReturnModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content text-start border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Process Sales Return</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 mb-4 text-start">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Select Invoice" data-ar="اختر الفاتورة">Select Invoice</label>
|
|
<select name="invoice_id" id="return_invoice_select" class="form-select select2" required>
|
|
<option value="">Choose Invoice...</option>
|
|
<?php if (!empty($data['sales_invoices'])): ?>
|
|
<?php foreach ($data['sales_invoices'] as $inv): ?>
|
|
<option value="<?= $inv['id'] ?>">INV-<?= str_pad((string)$inv['id'], 5, '0', STR_PAD_LEFT) ?> (<?= $inv['invoice_date'] ?>) - OMR <?= number_format((float)$inv['total_with_vat'], 3) ?></option>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Return Date</label>
|
|
<input type="date" name="return_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="return_items_container" style="display:none;" class="text-start">
|
|
<h6>Items for Return</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-bordered">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th>Item</th>
|
|
<th>Sold Qty</th>
|
|
<th>Return Qty</th>
|
|
<th>Price</th>
|
|
<th>Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="return_items_tbody"></tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<th colspan="4" class="text-end">Total Return Amount:</th>
|
|
<th class="text-end" id="return_total_display">OMR 0.000</th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Notes / Reason for Return</label>
|
|
<textarea name="notes" class="form-control" rows="2"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" name="add_sales_return" class="btn btn-danger" id="submit_return_btn" disabled>Process Return</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Add Account Modal -->
|
|
<div class="modal fade" id="addAccountModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add New Account" data-ar="إضافة حساب جديد">Add New Account</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-4">
|
|
<label class="form-label" data-en="Code" data-ar="الكود">Code</label>
|
|
<input type="text" name="code" class="form-control" required placeholder="e.g. 1101">
|
|
</div>
|
|
<div class="col-md-8">
|
|
<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-12">
|
|
<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="Type" data-ar="النوع">Type</label>
|
|
<select name="type" class="form-select" required>
|
|
<option value="asset">Asset</option>
|
|
<option value="liability">Liability</option>
|
|
<option value="equity">Equity</option>
|
|
<option value="revenue">Revenue</option>
|
|
<option value="expense">Expense</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Parent Account" data-ar="الحساب الأب">Parent Account</label>
|
|
<select name="parent_id" class="form-select">
|
|
<option value="">--- None ---</option>
|
|
<?php foreach ($data['accounts'] as $acc): ?>
|
|
<option value="<?= $acc['id'] ?>"><?= $acc['code'] ?> - <?= htmlspecialchars($acc['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" name="add_account" class="btn btn-primary">Save Account</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Manual Journal Modal -->
|
|
<div class="modal fade" id="addManualJournalModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title" data-en="New Manual Journal Entry" data-ar="قيد يومية يدوي جديد">New Manual Journal Entry</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Entry Date" data-ar="تاريخ القيد">Entry Date</label>
|
|
<input type="date" name="entry_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label class="form-label" data-en="Description" data-ar="الوصف">Description</label>
|
|
<input type="text" name="description" class="form-control" required placeholder="e.g. Opening Balance or Adjustment">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Reference" data-ar="المرجع">Reference</label>
|
|
<input type="text" name="reference" class="form-control" placeholder="e.g. JV-001">
|
|
</div>
|
|
</div>
|
|
|
|
<h6>Journal Details</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th style="width: 50%;">Account</th>
|
|
<th style="width: 20%;">Debit</th>
|
|
<th style="width: 20%;">Credit</th>
|
|
<th style="width: 10%;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="manualJournalBody">
|
|
<tr class="journal-row">
|
|
<td>
|
|
<select name="codes[]" class="form-select select2-journal" required>
|
|
<option value="">--- Select Account ---</option>
|
|
<?php foreach ($data['accounts'] as $acc): ?>
|
|
<option value="<?= $acc['code'] ?>"><?= $acc['code'] ?> - <?= htmlspecialchars($acc['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</td>
|
|
<td><input type="number" step="0.001" name="debits[]" class="form-control journal-debit" value="0.000"></td>
|
|
<td><input type="number" step="0.001" name="credits[]" class="form-control journal-credit" value="0.000"></td>
|
|
<td></td>
|
|
</tr>
|
|
<tr class="journal-row">
|
|
<td>
|
|
<select name="codes[]" class="form-select select2-journal" required>
|
|
<option value="">--- Select Account ---</option>
|
|
<?php foreach ($data['accounts'] as $acc): ?>
|
|
<option value="<?= $acc['code'] ?>"><?= $acc['code'] ?> - <?= htmlspecialchars($acc['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</td>
|
|
<td><input type="number" step="0.001" name="debits[]" class="form-control journal-debit" value="0.000"></td>
|
|
<td><input type="number" step="0.001" name="credits[]" class="form-control journal-credit" value="0.000"></td>
|
|
<td></td>
|
|
</tr>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td colspan="4">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addJournalRow()">
|
|
<i class="bi bi-plus-lg"></i> Add Row
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<tr class="table-light fw-bold">
|
|
<td class="text-end">Total</td>
|
|
<td id="totalDebitDisplay">0.000</td>
|
|
<td id="totalCreditDisplay">0.000</td>
|
|
<td></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
<div id="journalBalanceWarning" class="alert alert-danger py-2 mt-2" style="display:none;">
|
|
<small><i class="bi bi-exclamation-triangle"></i> Journal is not balanced! Difference: <span id="journalDifference">0.000</span></small>
|
|
</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_manual_journal" id="saveJournalBtn" class="btn btn-primary" data-en="Save Entry" data-ar="حفظ القيد" disabled>Save Entry</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function addJournalRow() {
|
|
const tbody = document.getElementById('manualJournalBody');
|
|
const row = document.createElement('tr');
|
|
row.className = 'journal-row';
|
|
row.innerHTML = `
|
|
<td>
|
|
<select name="codes[]" class="form-select select2-journal" required>
|
|
<option value="">--- Select Account ---</option>
|
|
<?php foreach ($data['accounts'] as $acc): ?>
|
|
<option value="<?= $acc['code'] ?>"><?= $acc['code'] ?> - <?= htmlspecialchars($acc['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</td>
|
|
<td><input type="number" step="0.001" name="debits[]" class="form-control journal-debit" value="0.000"></td>
|
|
<td><input type="number" step="0.001" name="credits[]" class="form-control journal-credit" value="0.000"></td>
|
|
<td class="text-center">
|
|
<button type="button" class="btn btn-link text-danger p-0" onclick="this.closest('tr').remove(); calculateJournalBalance();">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
|
|
// Init Select2 for new row
|
|
$(row).find('.select2-journal').select2({
|
|
width: '100%',
|
|
dropdownParent: $('#addManualJournalModal')
|
|
});
|
|
|
|
attachJournalListeners(row);
|
|
}
|
|
|
|
function attachJournalListeners(row) {
|
|
row.querySelectorAll('input').forEach(input => {
|
|
input.addEventListener('input', calculateJournalBalance);
|
|
});
|
|
}
|
|
|
|
function calculateJournalBalance() {
|
|
let totalDebit = 0;
|
|
let totalCredit = 0;
|
|
document.querySelectorAll('.journal-row').forEach(row => {
|
|
totalDebit += parseFloat(row.querySelector('.journal-debit').value) || 0;
|
|
totalCredit += parseFloat(row.querySelector('.journal-credit').value) || 0;
|
|
});
|
|
|
|
document.getElementById('totalDebitDisplay').innerText = totalDebit.toFixed(3);
|
|
document.getElementById('totalCreditDisplay').innerText = totalCredit.toFixed(3);
|
|
|
|
const diff = Math.abs(totalDebit - totalCredit);
|
|
const isBalanced = diff < 0.001 && (totalDebit > 0 || totalCredit > 0);
|
|
|
|
document.getElementById('saveJournalBtn').disabled = !isBalanced;
|
|
|
|
const warning = document.getElementById('journalBalanceWarning');
|
|
if (diff >= 0.001) {
|
|
warning.style.display = 'block';
|
|
document.getElementById('journalDifference').innerText = diff.toFixed(3);
|
|
} else {
|
|
warning.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.querySelectorAll('.journal-row').forEach(attachJournalListeners);
|
|
$('.select2-journal').select2({
|
|
width: '100%',
|
|
dropdownParent: $('#addManualJournalModal')
|
|
});
|
|
});
|
|
</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 select2" 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 select2" 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 select2" 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 select2" 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, .btn, .modal-header, .modal-footer, .d-print-none,
|
|
.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 ONLY when a modal is open */
|
|
body.modal-open > *:not(.modal) { display: none !important; }
|
|
body.modal-open .main-content { 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 select2" 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 select2" 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();
|
|
}
|
|
|
|
<?php if ($page === 'dashboard'): ?>
|
|
const monthlyData = <?= json_encode($data['monthly_sales']) ?>;
|
|
const yearlyData = <?= json_encode($data['yearly_sales']) ?>;
|
|
|
|
const ctx = document.getElementById('salesChart').getContext('2d');
|
|
let salesChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: monthlyData.map(d => d.label),
|
|
datasets: [{
|
|
label: 'Sales (OMR)',
|
|
data: monthlyData.map(d => d.total),
|
|
borderColor: '#0d6efd',
|
|
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false }
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) { return 'OMR ' + value.toFixed(3); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
document.getElementById('btnMonthly').addEventListener('click', function() {
|
|
this.classList.add('active');
|
|
document.getElementById('btnYearly').classList.remove('active');
|
|
salesChart.data.labels = monthlyData.map(d => d.label);
|
|
salesChart.data.datasets[0].data = monthlyData.map(d => d.total);
|
|
salesChart.update();
|
|
});
|
|
|
|
document.getElementById('btnYearly').addEventListener('click', function() {
|
|
this.classList.add('active');
|
|
document.getElementById('btnMonthly').classList.remove('active');
|
|
salesChart.data.labels = yearlyData.map(d => d.label);
|
|
salesChart.data.datasets[0].data = yearlyData.map(d => d.total);
|
|
salesChart.update();
|
|
});
|
|
<?php endif; ?>
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|