diff --git a/app.php b/app.php
new file mode 100644
index 0000000..1c20190
--- /dev/null
+++ b/app.php
@@ -0,0 +1,763 @@
+exec("CREATE TABLE IF NOT EXISTS customers (
+ id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(160) NOT NULL,
+ branch_name VARCHAR(160) NOT NULL,
+ phone VARCHAR(40) DEFAULT NULL,
+ status VARCHAR(20) NOT NULL DEFAULT 'active',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
+
+ $pdo->exec("CREATE TABLE IF NOT EXISTS products (
+ id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ sku VARCHAR(80) NOT NULL UNIQUE,
+ name VARCHAR(160) NOT NULL,
+ category VARCHAR(20) NOT NULL,
+ unit VARCHAR(40) NOT NULL,
+ stock_qty DECIMAL(12,2) NOT NULL DEFAULT 0,
+ cost_price DECIMAL(12,2) NOT NULL DEFAULT 0,
+ sale_price DECIMAL(12,2) NOT NULL DEFAULT 0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
+
+ $pdo->exec("CREATE TABLE IF NOT EXISTS customer_prices (
+ id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ customer_id INT UNSIGNED NOT NULL,
+ product_id INT UNSIGNED NOT NULL,
+ special_price DECIMAL(12,2) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE KEY uniq_customer_product (customer_id, product_id),
+ CONSTRAINT fk_customer_prices_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
+ CONSTRAINT fk_customer_prices_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
+
+ $pdo->exec("CREATE TABLE IF NOT EXISTS sales_orders (
+ id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ order_number VARCHAR(40) NOT NULL UNIQUE,
+ customer_id INT UNSIGNED NOT NULL,
+ status VARCHAR(30) NOT NULL DEFAULT 'confirmed',
+ subtotal DECIMAL(12,2) NOT NULL DEFAULT 0,
+ expected_profit DECIMAL(12,2) NOT NULL DEFAULT 0,
+ notes TEXT DEFAULT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT fk_sales_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
+
+ $pdo->exec("CREATE TABLE IF NOT EXISTS sales_order_items (
+ id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ sales_order_id INT UNSIGNED NOT NULL,
+ product_id INT UNSIGNED NOT NULL,
+ qty DECIMAL(12,2) NOT NULL,
+ unit_price DECIMAL(12,2) NOT NULL,
+ line_total DECIMAL(12,2) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT fk_sales_items_order FOREIGN KEY (sales_order_id) REFERENCES sales_orders(id) ON DELETE CASCADE,
+ CONSTRAINT fk_sales_items_product FOREIGN KEY (product_id) REFERENCES products(id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
+
+ $pdo->exec("CREATE TABLE IF NOT EXISTS inventory_movements (
+ id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
+ product_id INT UNSIGNED NOT NULL,
+ movement_type VARCHAR(30) NOT NULL,
+ qty_change DECIMAL(12,2) NOT NULL,
+ stock_before DECIMAL(12,2) NOT NULL DEFAULT 0,
+ stock_after DECIMAL(12,2) NOT NULL DEFAULT 0,
+ reference_code VARCHAR(80) DEFAULT NULL,
+ notes VARCHAR(255) DEFAULT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_inventory_product_created (product_id, created_at),
+ CONSTRAINT fk_inventory_movements_product FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
+
+ seed_demo_data($pdo);
+ $booted = true;
+}
+
+function seed_demo_data(PDO $pdo): void
+{
+ $customerCount = (int) $pdo->query("SELECT COUNT(*) FROM customers")->fetchColumn();
+ if ($customerCount === 0) {
+ $stmt = $pdo->prepare('INSERT INTO customers (name, branch_name, phone, status) VALUES (?, ?, ?, ?)');
+ $rows = [
+ ['شركة السهول التجارية', 'فرع الرياض', '+966500000101', 'active'],
+ ['مؤسسة الواجهة الذكية', 'فرع جدة', '+966500000202', 'active'],
+ ['شركة المدى للتوزيع', 'فرع الدمام', '+966500000303', 'active'],
+ ];
+ foreach ($rows as $row) {
+ $stmt->execute($row);
+ }
+ }
+
+ $productCount = (int) $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
+ if ($productCount === 0) {
+ $stmt = $pdo->prepare('INSERT INTO products (sku, name, category, unit, stock_qty, cost_price, sale_price) VALUES (?, ?, ?, ?, ?, ?, ?)');
+ $rows = [
+ ['FG-101', 'منظف أرضيات 5 لتر', 'finished', 'كرتون', 84, 18.00, 24.00],
+ ['FG-102', 'معقم يد 500 مل', 'finished', 'كرتون', 52, 12.50, 17.00],
+ ['FG-103', 'صابون سائل 1 لتر', 'finished', 'كرتون', 38, 10.00, 14.50],
+ ['RM-201', 'مادة فعالة A', 'raw', 'برميل', 120, 7.50, 0.00],
+ ['RM-202', 'عبوات فارغة 500 مل', 'raw', 'رتبة', 360, 1.20, 0.00],
+ ];
+ foreach ($rows as $row) {
+ $stmt->execute($row);
+ }
+ }
+
+ $priceCount = (int) $pdo->query("SELECT COUNT(*) FROM customer_prices")->fetchColumn();
+ if ($priceCount === 0) {
+ $customerIds = $pdo->query('SELECT id, name FROM customers ORDER BY id')->fetchAll();
+ $productIds = $pdo->query("SELECT id, sku FROM products WHERE category = 'finished' ORDER BY id")->fetchAll();
+ $customerMap = [];
+ foreach ($customerIds as $row) {
+ $customerMap[$row['name']] = (int) $row['id'];
+ }
+ $productMap = [];
+ foreach ($productIds as $row) {
+ $productMap[$row['sku']] = (int) $row['id'];
+ }
+
+ $stmt = $pdo->prepare('INSERT INTO customer_prices (customer_id, product_id, special_price) VALUES (?, ?, ?)');
+ $rows = [
+ [$customerMap['شركة السهول التجارية'], $productMap['FG-101'], 22.50],
+ [$customerMap['شركة السهول التجارية'], $productMap['FG-102'], 15.90],
+ [$customerMap['مؤسسة الواجهة الذكية'], $productMap['FG-102'], 16.10],
+ [$customerMap['مؤسسة الواجهة الذكية'], $productMap['FG-103'], 13.60],
+ [$customerMap['شركة المدى للتوزيع'], $productMap['FG-101'], 23.20],
+ [$customerMap['شركة المدى للتوزيع'], $productMap['FG-103'], 13.80],
+ ];
+ foreach ($rows as $row) {
+ $stmt->execute($row);
+ }
+ }
+}
+
+function csrf_token(): string
+{
+ if (empty($_SESSION['csrf_token'])) {
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+ }
+ return $_SESSION['csrf_token'];
+}
+
+function verify_csrf(?string $token): bool
+{
+ return is_string($token) && isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
+}
+
+function flash(string $type, string $message): void
+{
+ $_SESSION['flash'] = ['type' => $type, 'message' => $message];
+}
+
+function consume_flash(): ?array
+{
+ if (!isset($_SESSION['flash'])) {
+ return null;
+ }
+ $flash = $_SESSION['flash'];
+ unset($_SESSION['flash']);
+ return $flash;
+}
+
+function project_name(): string
+{
+ return trim((string) ($_SERVER['PROJECT_NAME'] ?? 'NexusERP')) ?: 'NexusERP';
+}
+
+function render_header(string $pageTitle, string $active = 'dashboard'): void
+{
+ $projectDescription = (string) ($_SERVER['PROJECT_DESCRIPTION'] ?? 'ERP dashboard and sales operations workspace');
+ $projectImageUrl = (string) ($_SERVER['PROJECT_IMAGE_URL'] ?? '');
+ $fullTitle = $pageTitle . ' | ' . project_name();
+ $nav = [
+ 'dashboard' => ['href' => 'index.php', 'label' => 'لوحة التحكم'],
+ 'customers' => ['href' => 'customers.php', 'label' => 'العملاء'],
+ 'products' => ['href' => 'products.php', 'label' => 'المخزون'],
+ 'sales' => ['href' => 'sales.php', 'label' => 'أوامر البيع'],
+ ];
+ $cssVersion = file_exists(__DIR__ . '/assets/css/custom.css') ? (string) filemtime(__DIR__ . '/assets/css/custom.css') : (string) time();
+ echo '';
+ echo '';
+ echo '
';
+ echo ' ';
+ echo ' ';
+ echo '' . h($fullTitle) . ' ';
+ echo ' ';
+ echo ' ';
+ echo ' ';
+ echo ' ';
+ echo ' ';
+ if ($projectImageUrl !== '') {
+ echo ' ';
+ echo ' ';
+ }
+ echo ' ';
+ echo ' ';
+ echo '';
+ echo '';
+ echo '';
+ echo '
';
+ echo '';
+ echo '
';
+ echo '
';
+ echo '
NE
';
+ echo '
';
+ echo '
Single-company ERP
';
+ echo '
' . h(project_name()) . '
';
+ echo '
';
+ echo '
';
+ echo '
';
+ echo '
Admin Demo ';
+ echo '
Health ';
+ echo '
';
+ echo '
';
+ echo '
';
+ foreach ($nav as $key => $item) {
+ $class = $key === $active ? 'nav-link active' : 'nav-link';
+ echo '' . h($item['label']) . ' ';
+ }
+ echo 'المشتريات · التصنيع · المحاسبة التالي ';
+ echo ' ';
+ echo '
';
+ echo ' ';
+ echo '
';
+ echo '';
+}
+
+function render_footer(): void
+{
+ $flash = consume_flash();
+ $jsVersion = file_exists(__DIR__ . '/assets/js/main.js') ? (string) filemtime(__DIR__ . '/assets/js/main.js') : (string) time();
+ if ($flash) {
+ echo '
';
+ echo '
';
+ echo '
';
+ echo '
' . h($flash['message']) . '
';
+ echo '
';
+ echo '
';
+ }
+ echo '
';
+ echo ' ';
+ echo '';
+ echo '
';
+ echo '';
+ echo '';
+ echo '';
+}
+
+function fetch_dashboard_metrics(): array
+{
+ $pdo = db();
+ return [
+ 'customers' => (int) $pdo->query('SELECT COUNT(*) FROM customers')->fetchColumn(),
+ 'products' => (int) $pdo->query('SELECT COUNT(*) FROM products')->fetchColumn(),
+ 'finished_stock' => (float) $pdo->query("SELECT COALESCE(SUM(stock_qty),0) FROM products WHERE category = 'finished'")->fetchColumn(),
+ 'raw_stock' => (float) $pdo->query("SELECT COALESCE(SUM(stock_qty),0) FROM products WHERE category = 'raw'")->fetchColumn(),
+ 'low_stock_products' => (int) $pdo->query("SELECT COUNT(*) FROM products WHERE stock_qty <= 40")->fetchColumn(),
+ 'orders_today' => (int) $pdo->query('SELECT COUNT(*) FROM sales_orders WHERE DATE(created_at) = CURDATE()')->fetchColumn(),
+ 'sales_today' => (float) $pdo->query('SELECT COALESCE(SUM(subtotal),0) FROM sales_orders WHERE DATE(created_at) = CURDATE()')->fetchColumn(),
+ 'profit_today' => (float) $pdo->query('SELECT COALESCE(SUM(expected_profit),0) FROM sales_orders WHERE DATE(created_at) = CURDATE()')->fetchColumn(),
+ ];
+}
+
+function fetch_sales_trend(): array
+{
+ $pdo = db();
+ $stmt = $pdo->query("SELECT DATE(created_at) AS order_date, COUNT(*) AS order_count, COALESCE(SUM(subtotal),0) AS revenue
+ FROM sales_orders
+ WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 6 DAY)
+ GROUP BY DATE(created_at)
+ ORDER BY order_date ASC");
+ $rows = $stmt->fetchAll();
+ $indexed = [];
+ foreach ($rows as $row) {
+ $indexed[$row['order_date']] = $row;
+ }
+ $trend = [];
+ for ($i = 6; $i >= 0; $i--) {
+ $date = date('Y-m-d', strtotime('-' . $i . ' days'));
+ $trend[] = [
+ 'date' => $date,
+ 'label' => date('d M', strtotime($date)),
+ 'order_count' => isset($indexed[$date]) ? (int) $indexed[$date]['order_count'] : 0,
+ 'revenue' => isset($indexed[$date]) ? (float) $indexed[$date]['revenue'] : 0.0,
+ ];
+ }
+ return $trend;
+}
+
+function fetch_recent_orders(int $limit = 8): array
+{
+ $pdo = db();
+ $stmt = $pdo->prepare("SELECT so.id, so.order_number, so.status, so.subtotal, so.expected_profit, so.created_at,
+ c.name AS customer_name, c.branch_name
+ FROM sales_orders so
+ INNER JOIN customers c ON c.id = so.customer_id
+ ORDER BY so.created_at DESC, so.id DESC
+ LIMIT :limit");
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll();
+}
+
+function fetch_products(): array
+{
+ $pdo = db();
+ return $pdo->query('SELECT * FROM products ORDER BY category ASC, name ASC')->fetchAll();
+}
+
+function fetch_product_by_id(int $productId): ?array
+{
+ $pdo = db();
+ $stmt = $pdo->prepare('SELECT * FROM products WHERE id = ? LIMIT 1');
+ $stmt->execute([$productId]);
+ $product = $stmt->fetch();
+ return $product ?: null;
+}
+
+function fetch_inventory_movements(int $limit = 12): array
+{
+ $pdo = db();
+ $stmt = $pdo->prepare("SELECT im.*, p.sku, p.name AS product_name, p.unit
+ FROM inventory_movements im
+ INNER JOIN products p ON p.id = im.product_id
+ ORDER BY im.created_at DESC, im.id DESC
+ LIMIT :limit");
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll();
+}
+
+function fetch_product_options(): array
+{
+ $pdo = db();
+ $stmt = $pdo->query('SELECT id, sku, name, category, unit, stock_qty FROM products ORDER BY name ASC');
+ return $stmt->fetchAll();
+}
+
+function fetch_customers_with_catalog(): array
+{
+ $pdo = db();
+ $customers = $pdo->query('SELECT * FROM customers ORDER BY created_at DESC, id DESC')->fetchAll();
+ $stmt = $pdo->query("SELECT cp.customer_id, p.sku, p.name, p.unit, cp.special_price
+ FROM customer_prices cp
+ INNER JOIN products p ON p.id = cp.product_id
+ ORDER BY p.name ASC");
+ $catalogRows = $stmt->fetchAll();
+ $catalog = [];
+ foreach ($catalogRows as $row) {
+ $catalog[(int) $row['customer_id']][] = $row;
+ }
+ foreach ($customers as &$customer) {
+ $customer['catalog'] = $catalog[(int) $customer['id']] ?? [];
+ }
+ unset($customer);
+ return $customers;
+}
+
+
+function fetch_finished_products_for_pricing(): array
+{
+ $pdo = db();
+ $stmt = $pdo->query("SELECT id, sku, name, unit, stock_qty, sale_price
+ FROM products
+ WHERE category = 'finished'
+ ORDER BY name ASC");
+ return $stmt->fetchAll();
+}
+
+function normalize_product_category(string $category): ?string
+{
+ $category = trim($category);
+ return in_array($category, ['finished', 'raw'], true) ? $category : null;
+}
+
+function create_product(array $input): array
+{
+ $sku = strtoupper(trim((string) ($input['sku'] ?? '')));
+ $name = trim((string) ($input['name'] ?? ''));
+ $category = normalize_product_category((string) ($input['category'] ?? ''));
+ $unit = trim((string) ($input['unit'] ?? ''));
+ $stockQty = round((float) ($input['stock_qty'] ?? 0), 2);
+ $costPrice = round((float) ($input['cost_price'] ?? 0), 2);
+ $salePrice = round((float) ($input['sale_price'] ?? 0), 2);
+
+ if ($sku === '' || $name === '' || $unit === '' || $category === null) {
+ return ['success' => false, 'message' => 'SKU واسم الصنف والفئة ووحدة القياس مطلوبة.'];
+ }
+ if ($stockQty < 0 || $costPrice < 0 || $salePrice < 0) {
+ return ['success' => false, 'message' => 'قيم المخزون والأسعار يجب أن تكون صفرًا أو أكبر.'];
+ }
+
+ $pdo = db();
+ try {
+ $pdo->beginTransaction();
+
+ $stmt = $pdo->prepare('INSERT INTO products (sku, name, category, unit, stock_qty, cost_price, sale_price) VALUES (?, ?, ?, ?, ?, ?, ?)');
+ $stmt->execute([$sku, $name, $category, $unit, $stockQty, $costPrice, $salePrice]);
+ $productId = (int) $pdo->lastInsertId();
+
+ if ($stockQty > 0) {
+ record_inventory_movement($pdo, $productId, 'opening', $stockQty, 0.0, $stockQty, $sku, 'رصيد افتتاحي عند إنشاء الصنف');
+ }
+
+ $pdo->commit();
+ return ['success' => true, 'message' => 'تم إنشاء الصنف بنجاح.', 'product_id' => $productId];
+ } catch (PDOException $e) {
+ if ($pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+ if ((string) $e->getCode() === '23000') {
+ return ['success' => false, 'message' => 'SKU مستخدم من قبل. اختر رمزًا مختلفًا.'];
+ }
+ return ['success' => false, 'message' => 'تعذر إنشاء الصنف حالياً.'];
+ } catch (Throwable $e) {
+ if ($pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+ return ['success' => false, 'message' => 'تعذر إنشاء الصنف حالياً.'];
+ }
+}
+
+function update_product(int $productId, array $input): array
+{
+ $product = fetch_product_by_id($productId);
+ if (!$product) {
+ return ['success' => false, 'message' => 'الصنف المطلوب غير موجود.'];
+ }
+
+ $sku = strtoupper(trim((string) ($input['sku'] ?? '')));
+ $name = trim((string) ($input['name'] ?? ''));
+ $category = normalize_product_category((string) ($input['category'] ?? ''));
+ $unit = trim((string) ($input['unit'] ?? ''));
+ $costPrice = round((float) ($input['cost_price'] ?? 0), 2);
+ $salePrice = round((float) ($input['sale_price'] ?? 0), 2);
+
+ if ($sku === '' || $name === '' || $unit === '' || $category === null) {
+ return ['success' => false, 'message' => 'SKU واسم الصنف والفئة ووحدة القياس مطلوبة.'];
+ }
+ if ($costPrice < 0 || $salePrice < 0) {
+ return ['success' => false, 'message' => 'الأسعار يجب أن تكون صفرًا أو أكبر.'];
+ }
+
+ $pdo = db();
+ try {
+ $stmt = $pdo->prepare('UPDATE products SET sku = ?, name = ?, category = ?, unit = ?, cost_price = ?, sale_price = ? WHERE id = ?');
+ $stmt->execute([$sku, $name, $category, $unit, $costPrice, $salePrice, $productId]);
+ return ['success' => true, 'message' => 'تم تحديث بيانات الصنف بنجاح.'];
+ } catch (PDOException $e) {
+ if ((string) $e->getCode() === '23000') {
+ return ['success' => false, 'message' => 'SKU مستخدم من قبل. اختر رمزًا مختلفًا.'];
+ }
+ return ['success' => false, 'message' => 'تعذر تحديث الصنف حالياً.'];
+ }
+}
+
+function record_inventory_movement(PDO $pdo, int $productId, string $movementType, float $qtyChange, float $stockBefore, float $stockAfter, ?string $referenceCode = null, ?string $notes = null): void
+{
+ $stmt = $pdo->prepare('INSERT INTO inventory_movements (product_id, movement_type, qty_change, stock_before, stock_after, reference_code, notes) VALUES (?, ?, ?, ?, ?, ?, ?)');
+ $stmt->execute([$productId, $movementType, round($qtyChange, 2), round($stockBefore, 2), round($stockAfter, 2), $referenceCode, $notes]);
+}
+
+function adjust_product_stock(int $productId, string $direction, float $qty, string $reason, string $notes): array
+{
+ $direction = trim($direction);
+ $reason = trim($reason);
+ $notes = trim($notes);
+ if (!in_array($direction, ['add', 'subtract'], true) || $qty <= 0) {
+ return ['success' => false, 'message' => 'أدخل عملية ضبط صحيحة وكمية أكبر من صفر.'];
+ }
+
+ $product = fetch_product_by_id($productId);
+ if (!$product) {
+ return ['success' => false, 'message' => 'الصنف المطلوب غير موجود.'];
+ }
+
+ $pdo = db();
+ try {
+ $pdo->beginTransaction();
+
+ $lockStmt = $pdo->prepare('SELECT id, sku, stock_qty FROM products WHERE id = ? FOR UPDATE');
+ $lockStmt->execute([$productId]);
+ $locked = $lockStmt->fetch();
+ if (!$locked) {
+ throw new RuntimeException('Product not found during lock.');
+ }
+
+ $stockBefore = (float) $locked['stock_qty'];
+ $delta = $direction === 'add' ? $qty : -$qty;
+ $stockAfter = round($stockBefore + $delta, 2);
+ if ($stockAfter < 0) {
+ if ($pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+ return ['success' => false, 'message' => 'لا يمكن خصم كمية أكبر من المخزون الحالي.'];
+ }
+
+ $updateStmt = $pdo->prepare('UPDATE products SET stock_qty = ? WHERE id = ?');
+ $updateStmt->execute([$stockAfter, $productId]);
+
+ $movementType = $direction === 'add' ? 'adjustment_in' : 'adjustment_out';
+ $movementNotes = $reason !== '' ? $reason : 'ضبط يدوي';
+ if ($notes !== '') {
+ $movementNotes .= ' — ' . $notes;
+ }
+ record_inventory_movement($pdo, $productId, $movementType, $delta, $stockBefore, $stockAfter, $locked['sku'], $movementNotes);
+
+ $pdo->commit();
+ return ['success' => true, 'message' => 'تم تحديث المخزون للصنف بنجاح.'];
+ } catch (Throwable $e) {
+ if ($pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+ return ['success' => false, 'message' => 'تعذر ضبط المخزون حالياً.'];
+ }
+}
+
+function create_customer_with_prices(string $name, string $branchName, string $phone, array $selectedProducts, array $priceInputs): array
+{
+ $name = trim($name);
+ $branchName = trim($branchName);
+ $phone = trim($phone);
+
+ if ($name === '' || $branchName === '') {
+ return ['success' => false, 'message' => 'اسم العميل واسم الفرع مطلوبان.'];
+ }
+
+ $productIds = [];
+ foreach ($selectedProducts as $productId) {
+ $id = (int) $productId;
+ if ($id > 0) {
+ $productIds[$id] = $id;
+ }
+ }
+
+ if (!$productIds) {
+ return ['success' => false, 'message' => 'اختر صنفاً واحداً على الأقل وحدد له سعراً خاصاً.'];
+ }
+
+ $pricingRows = [];
+ foreach ($productIds as $productId) {
+ $rawPrice = isset($priceInputs[$productId]) ? trim((string) $priceInputs[$productId]) : '';
+ if ($rawPrice === '' || !is_numeric($rawPrice)) {
+ return ['success' => false, 'message' => 'أدخل سعراً صحيحاً لكل صنف تم اختياره.'];
+ }
+ $price = round((float) $rawPrice, 2);
+ if ($price <= 0) {
+ return ['success' => false, 'message' => 'السعر الخاص يجب أن يكون أكبر من صفر.'];
+ }
+ $pricingRows[] = ['product_id' => $productId, 'special_price' => $price];
+ }
+
+ $pdo = db();
+ try {
+ $pdo->beginTransaction();
+
+ $customerStmt = $pdo->prepare('INSERT INTO customers (name, branch_name, phone, status) VALUES (?, ?, ?, ?)');
+ $customerStmt->execute([$name, $branchName, $phone !== '' ? $phone : null, 'active']);
+ $customerId = (int) $pdo->lastInsertId();
+
+ $priceStmt = $pdo->prepare('INSERT INTO customer_prices (customer_id, product_id, special_price) VALUES (?, ?, ?)');
+ foreach ($pricingRows as $row) {
+ $priceStmt->execute([$customerId, $row['product_id'], $row['special_price']]);
+ }
+
+ $pdo->commit();
+ return [
+ 'success' => true,
+ 'message' => 'تمت إضافة العميل وربط الأسعار الخاصة به بنجاح.',
+ 'customer_id' => $customerId,
+ ];
+ } catch (Throwable $e) {
+ if ($pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+ return ['success' => false, 'message' => 'تعذر حفظ العميل حالياً.'];
+ }
+}
+
+function fetch_sales_form_catalog(): array
+{
+ $pdo = db();
+ $stmt = $pdo->query("SELECT c.id AS customer_id, c.name AS customer_name, c.branch_name,
+ p.id AS product_id, p.sku, p.name AS product_name, p.unit, p.stock_qty, p.cost_price, p.sale_price,
+ cp.special_price
+ FROM customer_prices cp
+ INNER JOIN customers c ON c.id = cp.customer_id
+ INNER JOIN products p ON p.id = cp.product_id
+ ORDER BY c.name ASC, p.name ASC");
+ $catalog = [];
+ foreach ($stmt->fetchAll() as $row) {
+ $customerId = (int) $row['customer_id'];
+ if (!isset($catalog[$customerId])) {
+ $catalog[$customerId] = [
+ 'customer_id' => $customerId,
+ 'customer_name' => $row['customer_name'],
+ 'branch_name' => $row['branch_name'],
+ 'items' => [],
+ ];
+ }
+ $catalog[$customerId]['items'][] = [
+ 'product_id' => (int) $row['product_id'],
+ 'sku' => $row['sku'],
+ 'product_name' => $row['product_name'],
+ 'unit' => $row['unit'],
+ 'stock_qty' => (float) $row['stock_qty'],
+ 'cost_price' => (float) $row['cost_price'],
+ 'sale_price' => (float) $row['sale_price'],
+ 'special_price' => (float) $row['special_price'],
+ ];
+ }
+ return $catalog;
+}
+
+function format_money(float $value): string
+{
+ return number_format($value, 2) . ' ر.س';
+}
+
+function format_qty(float $value): string
+{
+ return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
+}
+
+function next_order_number(PDO $pdo): string
+{
+ $count = (int) $pdo->query('SELECT COUNT(*) FROM sales_orders WHERE DATE(created_at) = CURDATE()')->fetchColumn();
+ return 'SO-' . date('Ymd') . '-' . str_pad((string) ($count + 1), 3, '0', STR_PAD_LEFT);
+}
+
+function create_sales_order(int $customerId, int $productId, float $qty, string $notes): array
+{
+ $pdo = db();
+ if ($qty <= 0) {
+ return ['success' => false, 'message' => 'الكمية يجب أن تكون أكبر من صفر.'];
+ }
+
+ $stmt = $pdo->prepare("SELECT c.id AS customer_id, c.name AS customer_name, c.branch_name,
+ p.id AS product_id, p.name AS product_name, p.sku, p.unit, p.stock_qty, p.cost_price,
+ cp.special_price
+ FROM customer_prices cp
+ INNER JOIN customers c ON c.id = cp.customer_id
+ INNER JOIN products p ON p.id = cp.product_id
+ WHERE c.id = :customer_id AND p.id = :product_id
+ LIMIT 1");
+ $stmt->execute([
+ ':customer_id' => $customerId,
+ ':product_id' => $productId,
+ ]);
+ $row = $stmt->fetch();
+ if (!$row) {
+ return ['success' => false, 'message' => 'هذا الصنف غير متاح لهذا العميل أو لا يوجد سعر خاص له.'];
+ }
+
+ $unitPrice = (float) $row['special_price'];
+ $lineTotal = round($qty * $unitPrice, 2);
+ $expectedProfit = round($lineTotal - ($qty * (float) $row['cost_price']), 2);
+ $orderNumber = next_order_number($pdo);
+
+ try {
+ $pdo->beginTransaction();
+
+ $stockLock = $pdo->prepare('SELECT stock_qty FROM products WHERE id = ? FOR UPDATE');
+ $stockLock->execute([$productId]);
+ $stockRow = $stockLock->fetch();
+ if (!$stockRow) {
+ throw new RuntimeException('الصنف غير موجود أثناء الحفظ.');
+ }
+
+ $stockBefore = (float) $stockRow['stock_qty'];
+ if ($qty > $stockBefore) {
+ if ($pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+ return ['success' => false, 'message' => 'الكمية المطلوبة أكبر من المخزون المتاح حالياً.'];
+ }
+ $stockAfter = round($stockBefore - $qty, 2);
+
+ $orderStmt = $pdo->prepare('INSERT INTO sales_orders (order_number, customer_id, status, subtotal, expected_profit, notes) VALUES (?, ?, ?, ?, ?, ?)');
+ $orderStmt->execute([$orderNumber, $customerId, 'confirmed', $lineTotal, $expectedProfit, $notes !== '' ? $notes : null]);
+ $orderId = (int) $pdo->lastInsertId();
+
+ $itemStmt = $pdo->prepare('INSERT INTO sales_order_items (sales_order_id, product_id, qty, unit_price, line_total) VALUES (?, ?, ?, ?, ?)');
+ $itemStmt->execute([$orderId, $productId, $qty, $unitPrice, $lineTotal]);
+
+ $stockStmt = $pdo->prepare('UPDATE products SET stock_qty = ? WHERE id = ?');
+ $stockStmt->execute([$stockAfter, $productId]);
+
+ record_inventory_movement(
+ $pdo,
+ $productId,
+ 'sale',
+ -$qty,
+ $stockBefore,
+ $stockAfter,
+ $orderNumber,
+ 'خصم تلقائي من أمر البيع ' . $orderNumber
+ );
+
+ $pdo->commit();
+ return [
+ 'success' => true,
+ 'message' => 'تم إنشاء أمر البيع ' . $orderNumber . ' وتحديث المخزون تلقائياً.',
+ 'order_id' => $orderId,
+ 'order_number' => $orderNumber,
+ ];
+ } catch (Throwable $e) {
+ if ($pdo->inTransaction()) {
+ $pdo->rollBack();
+ }
+ return ['success' => false, 'message' => 'حدث خطأ أثناء حفظ أمر البيع.'];
+ }
+}
+
+function fetch_order_detail(int $orderId): ?array
+{
+ $pdo = db();
+ $stmt = $pdo->prepare("SELECT so.*, c.name AS customer_name, c.branch_name, c.phone
+ FROM sales_orders so
+ INNER JOIN customers c ON c.id = so.customer_id
+ WHERE so.id = :id
+ LIMIT 1");
+ $stmt->execute([':id' => $orderId]);
+ $order = $stmt->fetch();
+ if (!$order) {
+ return null;
+ }
+
+ $itemStmt = $pdo->prepare("SELECT soi.qty, soi.unit_price, soi.line_total,
+ p.name AS product_name, p.sku, p.unit, p.cost_price
+ FROM sales_order_items soi
+ INNER JOIN products p ON p.id = soi.product_id
+ WHERE soi.sales_order_id = :id");
+ $itemStmt->execute([':id' => $orderId]);
+ $items = $itemStmt->fetchAll();
+
+ return ['order' => $order, 'items' => $items];
+}
diff --git a/assets/css/custom.css b/assets/css/custom.css
index 789132e..00176e7 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -1,403 +1,407 @@
+:root {
+ --bg: #f3f4f6;
+ --surface: #ffffff;
+ --surface-muted: #f8fafc;
+ --border: #e5e7eb;
+ --text: #0f172a;
+ --muted: #64748b;
+ --primary: #111827;
+ --accent: #0f766e;
+ --warning: #b45309;
+ --success: #166534;
+ --shadow: 0 12px 32px rgba(15, 23, 42, 0.06);
+ --radius-sm: 10px;
+ --radius-md: 14px;
+ --radius-lg: 18px;
+}
+
+html, body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ min-height: 100%;
+}
+
body {
- background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
- background-size: 400% 400%;
- animation: gradient 15s ease infinite;
- color: #212529;
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- font-size: 14px;
margin: 0;
+}
+
+a {
+ text-decoration: none;
+}
+
+.app-shell {
min-height: 100vh;
}
-.main-wrapper {
- display: flex;
+.topbar {
+ background: rgba(255, 255, 255, 0.96);
+ backdrop-filter: blur(10px);
+ position: sticky;
+ top: 0;
+ z-index: 1040;
+}
+
+.brand-mark {
+ width: 42px;
+ height: 42px;
+ border-radius: 12px;
+ display: inline-flex;
align-items: center;
justify-content: center;
- min-height: 100vh;
- width: 100%;
- padding: 20px;
- box-sizing: border-box;
- position: relative;
- z-index: 1;
-}
-
-@keyframes gradient {
- 0% {
- background-position: 0% 50%;
- }
- 50% {
- background-position: 100% 50%;
- }
- 100% {
- background-position: 0% 50%;
- }
-}
-
-.chat-container {
- width: 100%;
- max-width: 600px;
- background: rgba(255, 255, 255, 0.85);
- border: 1px solid rgba(255, 255, 255, 0.3);
- border-radius: 20px;
- display: flex;
- flex-direction: column;
- height: 85vh;
- box-shadow: 0 20px 40px rgba(0,0,0,0.2);
- backdrop-filter: blur(15px);
- -webkit-backdrop-filter: blur(15px);
- overflow: hidden;
-}
-
-.chat-header {
- padding: 1.5rem;
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
- background: rgba(255, 255, 255, 0.5);
+ background: var(--primary);
+ color: #fff;
font-weight: 700;
- font-size: 1.1rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
+ letter-spacing: -0.02em;
}
-.chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 1.5rem;
- display: flex;
- flex-direction: column;
- gap: 1.25rem;
+.brand-title {
+ font-size: 1.05rem;
+ font-weight: 700;
}
-/* Custom Scrollbar */
-::-webkit-scrollbar {
- width: 6px;
+.eyebrow,
+.section-kicker {
+ font-size: 0.75rem;
+ letter-spacing: 0.08em;
+ color: var(--muted);
+ font-weight: 700;
}
-::-webkit-scrollbar-track {
- background: transparent;
+.app-nav {
+ gap: 0.5rem;
}
-::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.3);
- border-radius: 10px;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.5);
-}
-
-.message {
- max-width: 85%;
- padding: 0.85rem 1.1rem;
- border-radius: 16px;
- line-height: 1.5;
- font-size: 0.95rem;
- box-shadow: 0 4px 15px rgba(0,0,0,0.05);
- animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
-}
-
-@keyframes fadeIn {
- from { opacity: 0; transform: translateY(20px) scale(0.95); }
- to { opacity: 1; transform: translateY(0) scale(1); }
-}
-
-.message.visitor {
- align-self: flex-end;
- background: linear-gradient(135deg, #212529 0%, #343a40 100%);
- color: #fff;
- border-bottom-right-radius: 4px;
-}
-
-.message.bot {
- align-self: flex-start;
- background: #ffffff;
- color: #212529;
- border-bottom-left-radius: 4px;
-}
-
-.chat-input-area {
- padding: 1.25rem;
- background: rgba(255, 255, 255, 0.5);
- border-top: 1px solid rgba(0, 0, 0, 0.05);
-}
-
-.chat-input-area form {
- display: flex;
- gap: 0.75rem;
-}
-
-.chat-input-area input {
- flex: 1;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 12px;
- padding: 0.75rem 1rem;
- outline: none;
- background: rgba(255, 255, 255, 0.9);
- transition: all 0.3s ease;
-}
-
-.chat-input-area input:focus {
- border-color: #23a6d5;
- box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
-}
-
-.chat-input-area button {
- background: #212529;
- color: #fff;
- border: none;
- padding: 0.75rem 1.5rem;
- border-radius: 12px;
- cursor: pointer;
+.app-nav .nav-link {
+ border-radius: 999px;
+ color: var(--muted);
font-weight: 600;
- transition: all 0.3s ease;
+ padding: 0.6rem 0.95rem;
}
-.chat-input-area button:hover {
- background: #000;
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(0,0,0,0.2);
+.app-nav .nav-link.active,
+.app-nav .nav-link:hover {
+ background: var(--primary);
+ color: #fff;
}
-/* Background Animations */
-.bg-animations {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 0;
- overflow: hidden;
+.app-nav .nav-link-muted {
+ color: var(--muted);
pointer-events: none;
}
-.blob {
- position: absolute;
- width: 500px;
- height: 500px;
- background: rgba(255, 255, 255, 0.2);
- border-radius: 50%;
- filter: blur(80px);
- animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
+.page-title {
+ font-size: clamp(1.7rem, 3vw, 2.35rem);
+ line-height: 1.15;
+ font-weight: 700;
+ letter-spacing: -0.03em;
}
-.blob-1 {
- top: -10%;
- left: -10%;
- background: rgba(238, 119, 82, 0.4);
+.page-lead {
+ color: var(--muted);
+ max-width: 68ch;
+ line-height: 1.8;
}
-.blob-2 {
- bottom: -10%;
- right: -10%;
- background: rgba(35, 166, 213, 0.4);
- animation-delay: -7s;
- width: 600px;
- height: 600px;
+.panel-card {
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow);
+ background: var(--surface);
}
-.blob-3 {
- top: 40%;
- left: 30%;
- background: rgba(231, 60, 126, 0.3);
- animation-delay: -14s;
- width: 450px;
- height: 450px;
+.hero-panel {
+ background: linear-gradient(180deg, #ffffff 0%, #fbfbfc 100%);
}
-@keyframes move {
- 0% { transform: translate(0, 0) rotate(0deg) scale(1); }
- 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
- 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
- 100% { transform: translate(0, 0) rotate(360deg) scale(1); }
-}
-
-.header-link {
- font-size: 14px;
- color: #fff;
- text-decoration: none;
- background: rgba(0, 0, 0, 0.2);
- padding: 0.5rem 1rem;
- border-radius: 8px;
- transition: all 0.3s ease;
-}
-
-.header-link:hover {
- background: rgba(0, 0, 0, 0.4);
- text-decoration: none;
-}
-
-/* Admin Styles */
-.admin-container {
- max-width: 900px;
- margin: 3rem auto;
- padding: 2.5rem;
- background: rgba(255, 255, 255, 0.85);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- border-radius: 24px;
- box-shadow: 0 20px 50px rgba(0,0,0,0.15);
- border: 1px solid rgba(255, 255, 255, 0.4);
- position: relative;
- z-index: 1;
-}
-
-.admin-container h1 {
- margin-top: 0;
- color: #212529;
- font-weight: 800;
-}
-
-.table {
- width: 100%;
- border-collapse: separate;
- border-spacing: 0 8px;
- margin-top: 1.5rem;
-}
-
-.table th {
- background: transparent;
- border: none;
+.metric-card {
+ border: 1px solid var(--border);
+ background: var(--surface-muted);
+ border-radius: var(--radius-md);
padding: 1rem;
- color: #6c757d;
- font-weight: 600;
- text-transform: uppercase;
- font-size: 0.75rem;
- letter-spacing: 1px;
+ height: 100%;
}
-.table td {
- background: #fff;
- padding: 1rem;
- border: none;
+.metric-label,
+.mini-stat span,
+.summary-row span {
+ color: var(--muted);
+ font-size: 0.86rem;
}
-.table tr td:first-child { border-radius: 12px 0 0 12px; }
-.table tr td:last-child { border-radius: 0 12px 12px 0; }
-
-.form-group {
- margin-bottom: 1.25rem;
+.metric-value {
+ font-size: 1.55rem;
+ font-weight: 700;
+ margin-top: 0.35rem;
+ margin-bottom: 0.25rem;
+ letter-spacing: -0.02em;
}
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: 600;
- font-size: 0.9rem;
+.metric-note {
+ color: var(--muted);
+ font-size: 0.84rem;
}
-.form-control {
- width: 100%;
- padding: 0.75rem 1rem;
- border: 1px solid rgba(0, 0, 0, 0.1);
- border-radius: 12px;
- background: #fff;
- transition: all 0.3s ease;
- box-sizing: border-box;
+.section-title {
+ font-size: 1.15rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
}
-.form-control:focus {
- outline: none;
- border-color: #23a6d5;
- box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
+.small-title {
+ font-size: 1rem;
}
-.header-container {
+.stack-list,
+.price-list,
+.summary-grid {
+ display: grid;
+ gap: 0.85rem;
+}
+
+.stack-item,
+.price-item,
+.summary-row,
+.mini-stat {
display: flex;
- justify-content: space-between;
align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ padding: 0.8rem 0.9rem;
+ background: var(--surface-muted);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
}
-.header-links {
- display: flex;
+.summary-row.total {
+ background: #eef2f7;
+}
+
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 999px;
+ padding: 0.32rem 0.65rem;
+ font-size: 0.76rem;
+ font-weight: 700;
+ border: 1px solid transparent;
+ white-space: nowrap;
+}
+
+.status-badge.success {
+ background: #ecfdf3;
+ color: var(--success);
+ border-color: #bbf7d0;
+}
+
+.status-badge.pending {
+ background: #fff7ed;
+ color: var(--warning);
+ border-color: #fed7aa;
+}
+
+.status-badge.neutral {
+ background: #eff6ff;
+ color: #1d4ed8;
+ border-color: #bfdbfe;
+}
+
+.status-badge.warning {
+ background: #fff7ed;
+ color: var(--warning);
+ border-color: #fdba74;
+}
+
+.chart-list {
+ display: grid;
gap: 1rem;
}
-.admin-card {
- background: rgba(255, 255, 255, 0.6);
- padding: 2rem;
- border-radius: 20px;
- border: 1px solid rgba(255, 255, 255, 0.5);
- margin-bottom: 2.5rem;
- box-shadow: 0 10px 30px rgba(0,0,0,0.05);
+.chart-row {
+ display: grid;
+ grid-template-columns: 96px minmax(0, 1fr) 96px;
+ gap: 0.85rem;
+ align-items: center;
}
-.admin-card h3 {
- margin-top: 0;
- margin-bottom: 1.5rem;
+.chart-meta,
+.chart-value {
+ font-size: 0.88rem;
+}
+
+.chart-track {
+ height: 12px;
+ background: #e2e8f0;
+ border-radius: 999px;
+ overflow: hidden;
+}
+
+.chart-bar {
+ height: 100%;
+ background: #111827;
+ border-radius: 999px;
+}
+
+.app-table thead th {
+ color: var(--muted);
+ font-size: 0.78rem;
font-weight: 700;
-}
-
-.btn-delete {
- background: #dc3545;
- color: white;
- border: none;
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- cursor: pointer;
-}
-
-.btn-add {
- background: #212529;
- color: white;
- border: none;
- padding: 0.5rem 1rem;
- border-radius: 4px;
- cursor: pointer;
- margin-top: 1rem;
-}
-
-.btn-save {
- background: #0088cc;
- color: white;
- border: none;
- padding: 0.8rem 1.5rem;
- border-radius: 12px;
- cursor: pointer;
- font-weight: 600;
- width: 100%;
- transition: all 0.3s ease;
-}
-
-.webhook-url {
- font-size: 0.85em;
- color: #555;
- margin-top: 0.5rem;
-}
-
-.history-table-container {
- overflow-x: auto;
- background: rgba(255, 255, 255, 0.4);
- padding: 1rem;
- border-radius: 12px;
- border: 1px solid rgba(255, 255, 255, 0.3);
-}
-
-.history-table {
- width: 100%;
-}
-
-.history-table-time {
- width: 15%;
+ border-bottom-color: var(--border);
white-space: nowrap;
- font-size: 0.85em;
- color: #555;
}
-.history-table-user {
- width: 35%;
- background: rgba(255, 255, 255, 0.3);
- border-radius: 8px;
- padding: 8px;
+.app-table tbody td {
+ border-bottom-color: var(--border);
+ padding-top: 1rem;
+ padding-bottom: 1rem;
}
-.history-table-ai {
- width: 50%;
- background: rgba(255, 255, 255, 0.5);
- border-radius: 8px;
- padding: 8px;
-}
-
-.no-messages {
+.empty-state {
+ padding: 2.5rem 1rem;
text-align: center;
- color: #777;
-}
\ No newline at end of file
+}
+
+.empty-state.compact {
+ padding: 1.25rem 0.5rem;
+}
+
+.empty-title {
+ font-weight: 700;
+ margin-bottom: 0.35rem;
+}
+
+.empty-copy {
+ color: var(--muted);
+}
+
+.summary-card .alert {
+ border-radius: var(--radius-sm);
+}
+
+.form-control,
+.form-select {
+ min-height: 46px;
+ border-radius: var(--radius-sm);
+ border-color: #d1d5db;
+ padding-inline: 0.9rem;
+}
+
+.form-control:focus,
+.form-select:focus,
+.btn:focus {
+ box-shadow: 0 0 0 0.2rem rgba(17, 24, 39, 0.12);
+ border-color: #9ca3af;
+}
+
+.btn {
+ border-radius: 10px;
+ padding: 0.68rem 1rem;
+ font-weight: 600;
+}
+
+.btn-dark {
+ background: var(--primary);
+ border-color: var(--primary);
+}
+
+.btn-outline-secondary {
+ border-color: #d1d5db;
+ color: var(--text);
+}
+
+.footer-meta {
+ background: rgba(255, 255, 255, 0.92);
+ color: var(--muted);
+}
+
+.app-toast {
+ border-radius: var(--radius-sm);
+ box-shadow: var(--shadow);
+}
+
+@media print {
+ .topbar,
+ .footer-meta,
+ .btn,
+ .toast-container {
+ display: none !important;
+ }
+
+ body {
+ background: #fff;
+ }
+
+ .container-xxl {
+ max-width: 100% !important;
+ }
+
+ .panel-card {
+ box-shadow: none;
+ border-color: #d1d5db;
+ }
+}
+
+@media (max-width: 991.98px) {
+ .chart-row {
+ grid-template-columns: 1fr;
+ }
+
+ .app-nav .nav-link-muted {
+ width: 100%;
+ padding-inline: 0;
+ }
+}
+
+
+.pricing-picker {
+ display: grid;
+ gap: 0.85rem;
+}
+
+.pricing-option,
+.customer-list-card {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) minmax(140px, 180px);
+ gap: 0.85rem;
+ align-items: start;
+ padding: 0.9rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ background: var(--surface-muted);
+}
+
+.customer-list-card {
+ grid-template-columns: 1fr;
+ background: var(--surface);
+}
+
+.pricing-option .form-check-input {
+ margin-top: 0.35rem;
+}
+
+.pricing-option-price .form-control {
+ min-height: 42px;
+}
+
+.compact-list {
+ gap: 0.6rem;
+}
+
+@media (max-width: 767.98px) {
+ .pricing-option {
+ grid-template-columns: auto 1fr;
+ }
+
+ .pricing-option-price {
+ grid-column: 1 / -1;
+ }
+}
+
+.product-inline-note {
+ padding: 0.7rem 0.85rem;
+ border: 1px dashed var(--border);
+ border-radius: var(--radius-md);
+ background: var(--surface-muted);
+ color: var(--ink);
+}
diff --git a/assets/js/main.js b/assets/js/main.js
index d349598..30cf1d9 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -1,39 +1,116 @@
document.addEventListener('DOMContentLoaded', () => {
- const chatForm = document.getElementById('chat-form');
- const chatInput = document.getElementById('chat-input');
- const chatMessages = document.getElementById('chat-messages');
+ if (window.bootstrap) {
+ document.querySelectorAll('.toast').forEach((element) => {
+ const toast = new bootstrap.Toast(element);
+ toast.show();
+ });
+ }
- const appendMessage = (text, sender) => {
- const msgDiv = document.createElement('div');
- msgDiv.classList.add('message', sender);
- msgDiv.textContent = text;
- chatMessages.appendChild(msgDiv);
- chatMessages.scrollTop = chatMessages.scrollHeight;
+ const catalogNode = document.getElementById('catalog-data');
+ const customerSelect = document.getElementById('customer_id');
+ const productSelect = document.getElementById('product_id');
+ const qtyInput = document.getElementById('qty');
+ const submitButton = document.getElementById('submit-order-btn');
+ const hintBox = document.getElementById('sales-order-hint');
+
+ if (!catalogNode || !customerSelect || !productSelect || !qtyInput) {
+ return;
+ }
+
+ let catalog = {};
+ try {
+ catalog = JSON.parse(catalogNode.textContent || '{}');
+ } catch (error) {
+ console.error('Failed to parse catalog JSON', error);
+ }
+
+ const summaryCustomer = document.getElementById('summary-customer');
+ const summaryBranch = document.getElementById('summary-branch');
+ const summaryPrice = document.getElementById('summary-price');
+ const summaryStock = document.getElementById('summary-stock');
+ const summaryMargin = document.getElementById('summary-margin');
+ const summaryTotal = document.getElementById('summary-total');
+
+ const formatMoney = (value) => `${Number(value).toFixed(2)} ر.س`;
+
+ const getSelectedCustomer = () => catalog[customerSelect.value] || null;
+ const getSelectedProduct = () => {
+ const current = getSelectedCustomer();
+ if (!current) return null;
+ return current.items.find((item) => String(item.product_id) === productSelect.value) || null;
};
- chatForm.addEventListener('submit', async (e) => {
- e.preventDefault();
- const message = chatInput.value.trim();
- if (!message) return;
+ const resetSummary = () => {
+ summaryCustomer.textContent = '—';
+ summaryBranch.textContent = '—';
+ summaryPrice.textContent = '—';
+ summaryStock.textContent = '—';
+ summaryMargin.textContent = '—';
+ summaryTotal.textContent = '—';
+ };
- appendMessage(message, 'visitor');
- chatInput.value = '';
-
- try {
- const response = await fetch('api/chat.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
- const data = await response.json();
-
- // Artificial delay for realism
- setTimeout(() => {
- appendMessage(data.reply, 'bot');
- }, 500);
- } catch (error) {
- console.error('Error:', error);
- appendMessage("Sorry, something went wrong. Please try again.", 'bot');
+ const updateProducts = () => {
+ const current = getSelectedCustomer();
+ productSelect.innerHTML = '';
+ if (!current) {
+ productSelect.disabled = true;
+ productSelect.innerHTML = 'اختر العميل أولاً ';
+ resetSummary();
+ hintBox.textContent = 'اختر عميلاً لعرض الأصناف المسموح بها له. إذا كانت الكمية المطلوبة أكبر من المتاح سيظهر تنبيه ويتم تعطيل الإرسال.';
+ submitButton.disabled = false;
+ return;
}
- });
+
+ summaryCustomer.textContent = current.customer_name;
+ summaryBranch.textContent = current.branch_name;
+ productSelect.disabled = false;
+ productSelect.innerHTML = 'اختر الصنف ';
+ current.items.forEach((item) => {
+ const option = document.createElement('option');
+ option.value = item.product_id;
+ option.textContent = `${item.product_name} — ${item.sku}`;
+ productSelect.appendChild(option);
+ });
+ resetCalculatedFields();
+ };
+
+ const resetCalculatedFields = () => {
+ summaryPrice.textContent = '—';
+ summaryStock.textContent = '—';
+ summaryMargin.textContent = '—';
+ summaryTotal.textContent = '—';
+ submitButton.disabled = false;
+ };
+
+ const updateSummary = () => {
+ const product = getSelectedProduct();
+ const qty = Number(qtyInput.value || 0);
+ if (!product) {
+ resetCalculatedFields();
+ return;
+ }
+
+ const price = Number(product.special_price || 0);
+ const margin = price - Number(product.cost_price || 0);
+ const total = qty > 0 ? qty * price : 0;
+ summaryPrice.textContent = formatMoney(price);
+ summaryStock.textContent = `${Number(product.stock_qty).toFixed(0)} ${product.unit}`;
+ summaryMargin.textContent = formatMoney(margin);
+ summaryTotal.textContent = formatMoney(total);
+
+ if (qty > Number(product.stock_qty)) {
+ hintBox.textContent = 'الكمية المطلوبة تتجاوز المخزون الحالي. خفّض الكمية أو حدّث المخزون أولاً.';
+ hintBox.className = 'alert alert-warning border mt-4 mb-0 small';
+ submitButton.disabled = true;
+ } else {
+ hintBox.textContent = 'السعر الخاص تم تطبيقه تلقائياً بناءً على العميل والصنف المحدد.';
+ hintBox.className = 'alert alert-light border mt-4 mb-0 small';
+ submitButton.disabled = false;
+ }
+ };
+
+ customerSelect.addEventListener('change', updateProducts);
+ productSelect.addEventListener('change', updateSummary);
+ qtyInput.addEventListener('input', updateSummary);
+ updateProducts();
});
diff --git a/customers.php b/customers.php
new file mode 100644
index 0000000..7328094
--- /dev/null
+++ b/customers.php
@@ -0,0 +1,136 @@
+
+
+
+
+
Customers
+
العملاء والفروع والأسعار الخاصة
+
أضفت هنا أول خطوة CRUD حقيقية: يمكنك الآن إنشاء عميل جديد، تحديد فرعه، وإسناد الأصناف المسموح بها مع السعر الخاص مباشرة، ليظهر فوراً داخل شاشة أوامر البيع.
+
+
إنشاء أمر بيع
+
+
+
+
+
+
+
+
+
+ Create customer
+
إضافة عميل جديد
+
+
يرتبط بالمبيعات تلقائياً
+
+
+
+
+
+
+
+
+ Customer matrix
+
مصفوفة العملاء الحالية
+
+
الإجمالي: = h((string) count($customers)) ?>
+
+
+
+
+
+
+
+
= h($customer['name']) ?>
+
= h($customer['branch_name']) ?> · = h($customer['phone'] ?: '—') ?>
+
+
نشط
+
+
+ الأصناف المسموح بها
+ = h((string) count($customer['catalog'])) ?>
+
+
+
+
+
+
= h($item['name']) ?>
+
= h($item['sku']) ?> · = h($item['unit']) ?>
+
+
= h(format_money((float) $item['special_price'])) ?>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/healthz.php b/healthz.php
new file mode 100644
index 0000000..aa0d655
--- /dev/null
+++ b/healthz.php
@@ -0,0 +1,21 @@
+query('SELECT 1');
+ echo json_encode([
+ 'status' => 'ok',
+ 'php' => PHP_VERSION,
+ 'time' => gmdate('c'),
+ 'db' => 'connected',
+ ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+} catch (Throwable $e) {
+ http_response_code(500);
+ echo json_encode([
+ 'status' => 'error',
+ 'php' => PHP_VERSION,
+ 'time' => gmdate('c'),
+ 'db' => 'failed',
+ ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+}
diff --git a/index.php b/index.php
index 7205f3d..63b1886 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,257 @@
-
-
-
-
-
- New Style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Analyzing your requirements and generating your website…
-
-
Loading…
+
+
+
+
+
+
+
لوحة تشغيل يومية
+
ERP أولي يربط المبيعات بالمخزون فوراً
+
هذه النسخة الأولى تمنحك شاشة تنفيذ حقيقية: العملاء، الأصناف، أسعار خاصة لكل عميل، إنشاء أمر بيع، وتحديث المخزون والربحية مباشرة.
+
+
+
+
+
+
+
إجمالي العملاء
+
= h((string) $metrics['customers']) ?>
+
عملاء نشطون مع فروع وتسعير خاص
+
+
+
+
+
إجمالي الأصناف
+
= h((string) $metrics['products']) ?>
+
مواد خام + منتجات نهائية
+
+
+
+
+
أصناف منخفضة
+
= h((string) $metrics['low_stock_products']) ?>
+
تحتاج ضبطًا أو توريدًا قريبًا
+
+
+
+
+
أوامر بيع اليوم
+
= h((string) $metrics['orders_today']) ?>
+
مؤكدة ومخصومة من المخزون
+
+
+
+
+
مخزون منتجات نهائية
+
= h(format_qty((float) $metrics['finished_stock'])) ?>
+
جاهز للتسليم
+
+
+
+
+
مخزون خامات
+
= h(format_qty((float) $metrics['raw_stock'])) ?>
+
تغذية مستقبلية للتصنيع
+
+
+
+
+
مبيعات اليوم
+
= h(format_money((float) $metrics['sales_today'])) ?>
+
ربح متوقع: = h(format_money((float) $metrics['profit_today'])) ?>
+
+
+
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
-
-
- Page updated: = htmlspecialchars($now) ?> (UTC)
-
-
-
+
+
+
+
+ حالة التفعيل
+
الوحدات المتاحة الآن
+
+
+
+
+
+
Sales
+
تسعير خاص + خصم مخزون
+
+
جاهز
+
+
+
+
Inventory
+
تحديث مباشر بعد البيع
+
+
جاهز
+
+
+
+
Customers
+
فروع + منتجات مسموحة
+
+
جاهز
+
+
+
+
Purchasing
+
أوامر شراء واستلام
+
+
التالي
+
+
+
+
Manufacturing
+
تحويل خامات لمنتجات
+
+
التالي
+
+
+
+
Accounting
+
كشف حساب وربح وخسارة
+
+
التالي
+
+
+
+
+
+
+
+
+
+
+
+
+
+ مبيعات آخر 7 أيام
+
اتجاه التنفيذ
+
+
رسم مبسط للإيراد اليومي
+
+
+
+
+
+
+ = h($point['label']) ?>
+ = h((string) $point['order_count']) ?> طلب
+
+
+
= h(format_money((float) $point['revenue'])) ?>
+
+
+
+
+
+
+
+
+
+ لقطة مخزون
+
أصناف تحتاج متابعة
+
+
كل المخزون
+
+
+
+
+
+ الصنف
+ الفئة
+ المتاح
+
+
+
+
+
+
+ = h($product['name']) ?>
+ = h($product['sku']) ?>
+
+ = $product['category'] === 'finished' ? 'منتج نهائي' : 'خامة' ?>
+ = h(format_qty((float) $product['stock_qty'])) ?> = h($product['unit']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ آخر أوامر البيع
+
تنفيذ اليوم
+
+
إدارة أوامر البيع
+
+
+
+
+
+ رقم الطلب
+ العميل
+ الفرع
+ القيمة
+ الربح المتوقع
+ الحالة
+
+
+
+
+
+
+
+
+
لا توجد أوامر بعد
+
ابدأ بأول أمر بيع لتفعيل الحركة على الداشبورد والمخزون.
+
+
+
+
+
+
+ = h($order['order_number']) ?>
+ = h($order['customer_name']) ?>
+ = h($order['branch_name']) ?>
+ = h(format_money((float) $order['subtotal'])) ?>
+ = h(format_money((float) $order['expected_profit'])) ?>
+ مؤكد
+ تفاصيل
+
+
+
+
+
+
+
+
+
diff --git a/order.php b/order.php
new file mode 100644
index 0000000..d5e95a9
--- /dev/null
+++ b/order.php
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
Order detail
+
= h($order['order_number']) ?>
+
تم إنشاء الطلب لهذا العميل مع خصم المخزون فوراً. يمكنك استخدام هذه الصفحة كنقطة انطلاق للطباعة وPDF في الخطوة التالية.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ الصنف
+ SKU
+ الكمية
+ سعر الوحدة
+ الإجمالي
+
+
+
+
+
+ = h($item['product_name']) ?>
+ = h($item['sku']) ?>
+ = h(format_qty((float) $item['qty'])) ?> = h($item['unit']) ?>
+ = h(format_money((float) $item['unit_price'])) ?>
+ = h(format_money((float) $item['line_total'])) ?>
+
+
+
+
+
+
+
+
+
+
+
العميل = h($order['customer_name']) ?>
+
الفرع = h($order['branch_name']) ?>
+
الهاتف = h($order['phone'] ?: '—') ?>
+
تاريخ الإنشاء = h(date('Y-m-d H:i', strtotime($order['created_at']))) ?>
+
الحالة مؤكد
+
إجمالي البيع = h(format_money((float) $order['subtotal'])) ?>
+
الربح المتوقع = h(format_money((float) $order['expected_profit'])) ?>
+
+
+
+ ملاحظات
+ = nl2br(h($order['notes'])) ?>
+
+
+
+
+
+
+
diff --git a/products.php b/products.php
new file mode 100644
index 0000000..1e141d3
--- /dev/null
+++ b/products.php
@@ -0,0 +1,338 @@
+ 0) {
+ $redirect = 'products.php?edit=' . $productId;
+ }
+ } elseif ($action === 'adjust_stock') {
+ $productId = (int) ($_POST['product_id'] ?? 0);
+ $result = adjust_product_stock(
+ $productId,
+ (string) ($_POST['direction'] ?? ''),
+ (float) ($_POST['qty'] ?? 0),
+ (string) ($_POST['reason'] ?? ''),
+ (string) ($_POST['notes'] ?? '')
+ );
+ if (!$result['success'] && $productId > 0) {
+ $redirect = 'products.php?adjust=' . $productId;
+ }
+ } else {
+ $result = ['success' => false, 'message' => 'الإجراء المطلوب غير معروف.'];
+ }
+
+ flash($result['success'] ? 'success' : 'danger', $result['message']);
+ header('Location: ' . $redirect);
+ exit;
+}
+
+$editId = (int) ($_GET['edit'] ?? 0);
+$adjustId = (int) ($_GET['adjust'] ?? 0);
+$editingProduct = $editId > 0 ? fetch_product_by_id($editId) : null;
+$products = fetch_products();
+$productOptions = fetch_product_options();
+$movements = fetch_inventory_movements(12);
+
+$finished = 0;
+$raw = 0;
+$low = 0;
+$totalStock = 0.0;
+foreach ($products as $product) {
+ $qty = (float) $product['stock_qty'];
+ $totalStock += $qty;
+ if ($product['category'] === 'finished') {
+ $finished++;
+ } else {
+ $raw++;
+ }
+ if ($qty <= 40) {
+ $low++;
+ }
+}
+
+$productForm = [
+ 'sku' => $editingProduct['sku'] ?? '',
+ 'name' => $editingProduct['name'] ?? '',
+ 'category' => $editingProduct['category'] ?? 'finished',
+ 'unit' => $editingProduct['unit'] ?? '',
+ 'stock_qty' => '',
+ 'cost_price' => isset($editingProduct['cost_price']) ? format_qty((float) $editingProduct['cost_price']) : '',
+ 'sale_price' => isset($editingProduct['sale_price']) ? format_qty((float) $editingProduct['sale_price']) : '',
+];
+
+function movement_type_label(string $type): array
+{
+ return match ($type) {
+ 'opening' => ['label' => 'رصيد افتتاحي', 'class' => 'neutral'],
+ 'sale' => ['label' => 'بيع', 'class' => 'warning'],
+ 'adjustment_in' => ['label' => 'إضافة يدوية', 'class' => 'success'],
+ 'adjustment_out' => ['label' => 'خصم يدوي', 'class' => 'pending'],
+ default => ['label' => $type, 'class' => 'neutral'],
+ };
+}
+
+render_header('الأصناف والمخزون', 'products');
+?>
+
+
+
+
Products & Inventory
+
إدارة الأصناف وضبط المخزون
+
أصبحت الشاشة الآن تشغيلية: إضافة صنف جديد، تعديل بياناته، وضبط المخزون يدويًا مع سجل حركات واضح. كما أن أوامر البيع تسجل الآن حركة خصم تلقائية داخل نفس السجل.
+
+
+ إجمالي المخزون: = h(format_qty($totalStock)) ?>
+ منتجات نهائية: = h((string) $finished) ?>
+ خامات: = h((string) $raw) ?>
+ منخفضة: = h((string) $low) ?>
+
+
+
+
+
+
+
+
+
+
+ = $editingProduct ? 'Edit product' : 'Create product' ?>
+
= $editingProduct ? 'تعديل بيانات الصنف' : 'إضافة صنف جديد' ?>
+
+
+
إلغاء التعديل
+
+
يمكنك إنشاء خامة أو منتج نهائي مع رصيد افتتاحي
+
+
+
+
+
+
+
+
+
+ Stock adjustment
+
ضبط المخزون يدويًا
+
+
Audit trail
+
+
+
+
+
+
+
+
+
+
+
+ Product master
+
كل الأصناف
+
+
تعديل البيانات الأساسية من هنا، وضبط الكميات من البطاقة الجانبية
+
+
+
+
+
+ SKU
+ الصنف
+ الفئة
+ المتاح
+ تكلفة
+ بيع افتراضي
+ الحالة
+
+
+
+
+
+
+
+ = h($product['sku']) ?>
+
+ = h($product['name']) ?>
+ = h($product['unit']) ?>
+
+ = $product['category'] === 'finished' ? 'منتج نهائي' : 'خامة' ?>
+ = h(format_qty($qty)) ?> = h($product['unit']) ?>
+ = h(format_money((float) $product['cost_price'])) ?>
+ = h((float) $product['sale_price'] > 0 ? format_money((float) $product['sale_price']) : '—') ?>
+
+
+ منخفض
+
+ مستقر
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Inventory journal
+
آخر حركات المخزون
+
+
يشمل الرصيد الافتتاحي، التعديلات اليدوية، وخصومات البيع الجديدة
+
+
+
+
+
+ الوقت
+ الصنف
+ النوع
+ التغيير
+ قبل ← بعد
+ مرجع / ملاحظات
+
+
+
+
+
+
+
+
لا توجد حركات حتى الآن
+
أضف صنفًا برصيد افتتاحي أو نفّذ ضبط مخزون أو أمر بيع لتظهر هنا أول حركة.
+
+
+
+
+
+
+
+
+ = h(date('Y-m-d H:i', strtotime($movement['created_at']))) ?>
+
+ = h($movement['product_name']) ?>
+ = h($movement['sku']) ?>
+
+ = h($meta['label']) ?>
+ = h(($delta >= 0 ? '+' : '') . format_qty($delta)) ?> = h($movement['unit']) ?>
+ = h(format_qty((float) $movement['stock_before'])) ?> → = h(format_qty((float) $movement['stock_after'])) ?>
+
+ = h($movement['reference_code'] ?: '—') ?>
+ = h($movement['notes'] ?: 'بدون ملاحظات') ?>
+
+
+
+
+
+
+
+
+
+
diff --git a/sales.php b/sales.php
new file mode 100644
index 0000000..bfe22f4
--- /dev/null
+++ b/sales.php
@@ -0,0 +1,169 @@
+
+
+
+
+
Sales workflow
+
إنشاء أمر بيع مع تسعير خاص وتحديث مخزون
+
اختر العميل أولاً، وستظهر فقط الأصناف المسموح بها له مع السعر الخاص المخزن في النظام. عند التأكيد يتم حفظ الطلب وخصم المخزون مباشرة.
+
+
+ عملاء مفعّلون: = h((string) $customerCount) ?>
+ خيارات بيع: = h((string) $productOptions) ?>
+
+
+
+
+
+
+
+
+
+
+ Create
+
أمر بيع جديد
+
+
الحماية مفعلة: CSRF + prepared statements + output escaping
+
+
+
+
+ العميل
+
+ اختر العميل
+ $entry): ?>
+ = h($entry['customer_name']) ?> — = h($entry['branch_name']) ?>
+
+
+
+
+ الصنف المسموح
+
+ اختر العميل أولاً
+
+
+
+ الكمية
+
+
+
+ ملاحظات
+
+
+
+
سيتم إنشاء أمر بيع مؤكد وحساب الربح المتوقع من تكلفة المنتج الحالية.
+
تأكيد أمر البيع
+
+
+
+
+
+
+
+
+ Live summary
+
ملخص فوري
+
+
Auto pricing
+
+
+
العميل —
+
الفرع —
+
السعر الخاص —
+
المتاح بالمخزون —
+
الربح للوحدة —
+
قيمة السطر —
+
+
+ اختر عميلاً لعرض الأصناف المسموح بها له. إذا كانت الكمية المطلوبة أكبر من المتاح سيظهر تنبيه ويتم تعطيل الإرسال.
+
+
+
+
+
+
+
+
+
+
+ Recent orders
+
قائمة أوامر البيع
+
+
+
+
+
+
+ الرقم
+ العميل
+ الوقت
+ القيمة
+ الربح المتوقع
+ الإجراء
+
+
+
+
+
+
+
+
ابدأ من هنا
+
أول أمر بيع ستنشئه سيظهر هنا مع تأثيره على المخزون ولوحة التحكم.
+
+
+
+
+
+
+ = h($order['order_number']) ?>
+
+ = h($order['customer_name']) ?>
+ = h($order['branch_name']) ?>
+
+ = h(date('Y-m-d H:i', strtotime($order['created_at']))) ?>
+ = h(format_money((float) $order['subtotal'])) ?>
+ = h(format_money((float) $order['expected_profit'])) ?>
+ عرض
+
+
+
+
+
+
+
+
+
+