$type, 'message' => $message]; } function get_flash(): ?array { if (empty($_SESSION['flash'])) { return null; } $flash = $_SESSION['flash']; unset($_SESSION['flash']); return is_array($flash) ? $flash : null; } function json_payload(?string $payload): array { if (!$payload) { return []; } $decoded = json_decode($payload, true); return is_array($decoded) ? $decoded : []; } function parse_csv_list(string $raw): array { return array_values(array_filter(array_map('trim', explode(',', $raw)), static fn ($v) => $v !== '')); } function role_options(): array { return [ 'admin' => 'Admin', 'sales' => 'Sales', 'purchasing' => 'Purchasing', 'production' => 'Production', 'accounting' => 'Accounting', ]; } function role_label(string $role): string { $labels = [ 'admin' => 'مدير النظام', 'sales' => 'المبيعات', 'purchasing' => 'المشتريات', 'production' => 'الإنتاج', 'accounting' => 'المحاسبة', ]; return $labels[$role] ?? $role; } function ensure_schema(): void { db()->exec( "CREATE TABLE IF NOT EXISTS erp_records ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, record_type VARCHAR(40) NOT NULL, title VARCHAR(190) NOT NULL, code VARCHAR(80) NOT NULL, status VARCHAR(40) NOT NULL DEFAULT 'active', payload LONGTEXT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_record_type (record_type), KEY idx_record_type_status (record_type, status), KEY idx_code (code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); db()->exec( "CREATE TABLE IF NOT EXISTS app_users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(80) NOT NULL UNIQUE, full_name VARCHAR(190) NOT NULL, role VARCHAR(40) NOT NULL, password_hash VARCHAR(255) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_role (role), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); seed_demo_data(); } function next_code(string $prefix, string $type): string { $stmt = db()->prepare("SELECT COUNT(*) FROM erp_records WHERE record_type = :record_type"); $stmt->execute(['record_type' => $type]); $count = (int)$stmt->fetchColumn() + 1; return sprintf('%s-%04d', $prefix, $count); } function create_record(string $type, string $title, string $code, array $payload = [], string $status = 'active'): int { $stmt = db()->prepare( "INSERT INTO erp_records (record_type, title, code, status, payload) VALUES (:record_type, :title, :code, :status, :payload)" ); $stmt->execute([ 'record_type' => $type, 'title' => $title, 'code' => $code, 'status' => $status, 'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); return (int)db()->lastInsertId(); } function update_record_payload(int $id, array $payload, ?string $status = null): void { if ($status !== null) { $stmt = db()->prepare("UPDATE erp_records SET payload = :payload, status = :status WHERE id = :id"); $stmt->execute([ 'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'status' => $status, 'id' => $id, ]); return; } $stmt = db()->prepare("UPDATE erp_records SET payload = :payload WHERE id = :id"); $stmt->execute([ 'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'id' => $id, ]); } function fetch_records(string $type): array { $stmt = db()->prepare("SELECT * FROM erp_records WHERE record_type = :record_type ORDER BY id DESC"); $stmt->execute(['record_type' => $type]); $rows = $stmt->fetchAll(); foreach ($rows as &$row) { $row['payload_data'] = json_payload($row['payload'] ?? null); } return $rows; } function recent_records(string $type, int $limit = 5): array { $stmt = db()->prepare("SELECT * FROM erp_records WHERE record_type = :record_type ORDER BY id DESC LIMIT :limit"); $stmt->bindValue(':record_type', $type, PDO::PARAM_STR); $stmt->bindValue(':limit', max(1, $limit), PDO::PARAM_INT); $stmt->execute(); $rows = $stmt->fetchAll(); foreach ($rows as &$row) { $row['payload_data'] = json_payload($row['payload'] ?? null); } return $rows; } function fetch_record(string $type, int $id): ?array { $stmt = db()->prepare("SELECT * FROM erp_records WHERE record_type = :record_type AND id = :id LIMIT 1"); $stmt->execute(['record_type' => $type, 'id' => $id]); $row = $stmt->fetch(); if (!$row) { return null; } $row['payload_data'] = json_payload($row['payload'] ?? null); return $row; } function record_count(string $type): int { $stmt = db()->prepare("SELECT COUNT(*) FROM erp_records WHERE record_type = :record_type"); $stmt->execute(['record_type' => $type]); return (int)$stmt->fetchColumn(); } function today_record_count(string $type): int { $stmt = db()->prepare("SELECT COUNT(*) FROM erp_records WHERE record_type = :record_type AND DATE(created_at) = CURDATE()"); $stmt->execute(['record_type' => $type]); return (int)$stmt->fetchColumn(); } function record_created_date(array $record): string { return substr((string)($record['created_at'] ?? ''), 0, 10); } function record_matches_date_range(array $record, ?string $startDate = null, ?string $endDate = null): bool { $createdDate = record_created_date($record); if ($createdDate === '') { return false; } if ($startDate && $createdDate < $startDate) { return false; } if ($endDate && $createdDate > $endDate) { return false; } return true; } function filter_records_by_date(array $records, ?string $startDate = null, ?string $endDate = null): array { return array_values(array_filter($records, static fn (array $record): bool => record_matches_date_range($record, $startDate, $endDate))); } function fetch_counts(): array { $types = ['customer', 'supplier', 'product', 'sales_quote', 'sales_order', 'delivery_note', 'sales_invoice', 'purchase_order', 'manufacturing_order', 'stock_movement', 'customer_payment', 'supplier_payment', 'expense_entry']; $counts = []; foreach ($types as $type) { $counts[$type] = record_count($type); } return $counts; } function low_stock_products(int $limit = 6): array { $rows = []; foreach (fetch_records('product') as $product) { $payload = $product['payload_data']; $stock = (float)($payload['stock_qty'] ?? 0); $threshold = (float)($payload['reorder_level'] ?? 0); if ($stock <= $threshold) { $rows[] = $product; } } usort($rows, static function (array $a, array $b): int { return (float)($a['payload_data']['stock_qty'] ?? 0) <=> (float)($b['payload_data']['stock_qty'] ?? 0); }); return array_slice($rows, 0, $limit); } function low_stock_count(): int { return count(low_stock_products(999)); } function total_sales_value(): float { $total = 0.0; foreach (fetch_records('sales_order') as $order) { $total += (float)($order['payload_data']['grand_total'] ?? 0); } return $total; } function total_purchase_value(): float { $total = 0.0; foreach (fetch_records('purchase_order') as $order) { $total += (float)($order['payload_data']['grand_total'] ?? 0); } return $total; } function today_sales_value(): float { $total = 0.0; foreach (recent_records('sales_order', 200) as $order) { if (substr((string)($order['created_at'] ?? ''), 0, 10) === date('Y-m-d')) { $total += (float)($order['payload_data']['grand_total'] ?? 0); } } return $total; } function today_purchase_value(): float { $total = 0.0; foreach (recent_records('purchase_order', 200) as $order) { if (substr((string)($order['created_at'] ?? ''), 0, 10) === date('Y-m-d')) { $total += (float)($order['payload_data']['grand_total'] ?? 0); } } return $total; } function total_sales_invoice_value(): float { $total = 0.0; foreach (fetch_records('sales_invoice') as $invoice) { $total += (float)($invoice['payload_data']['grand_total'] ?? 0); } return $total; } function received_purchase_total(): float { $total = 0.0; foreach (fetch_records('purchase_order') as $order) { if ((string)($order['status'] ?? '') === 'received') { $total += (float)($order['payload_data']['grand_total'] ?? 0); } } return $total; } function total_customer_receipts(): float { $total = 0.0; foreach (fetch_records('customer_payment') as $payment) { $total += (float)($payment['payload_data']['amount'] ?? 0); } return $total; } function total_supplier_payments(): float { $total = 0.0; foreach (fetch_records('supplier_payment') as $payment) { $total += (float)($payment['payload_data']['amount'] ?? 0); } return $total; } function total_expense_value(): float { $total = 0.0; foreach (fetch_records('expense_entry') as $expense) { $total += (float)($expense['payload_data']['amount'] ?? 0); } return $total; } function today_amount_by_type(string $type, string $field = 'amount'): float { $total = 0.0; foreach (recent_records($type, 200) as $row) { if (substr((string)($row['created_at'] ?? ''), 0, 10) === date('Y-m-d')) { $total += (float)($row['payload_data'][$field] ?? 0); } } return $total; } function receivable_total(): float { return total_sales_invoice_value() - total_customer_receipts(); } function payable_total(): float { return received_purchase_total() - total_supplier_payments(); } function expected_profit_value(): float { return total_sales_invoice_value() - received_purchase_total() - total_expense_value(); } function net_cashflow_value(): float { return total_customer_receipts() - total_supplier_payments() - total_expense_value(); } function payment_method_options(): array { return [ 'bank_transfer' => 'تحويل بنكي', 'cash' => 'نقدي', 'card' => 'بطاقة', 'cheque' => 'شيك', ]; } function payment_method_label(string $method): string { $labels = payment_method_options(); return $labels[$method] ?? $method; } function expense_category_options(): array { return [ 'operations' => 'تشغيل', 'logistics' => 'لوجستيات', 'salary' => 'رواتب', 'marketing' => 'تسويق', 'utilities' => 'مرافق', 'other' => 'أخرى', ]; } function expense_category_label(string $category): string { $labels = expense_category_options(); return $labels[$category] ?? $category; } function accounting_summary(): array { $salesInvoices = total_sales_invoice_value(); $purchaseCommitments = received_purchase_total(); $customerReceipts = total_customer_receipts(); $supplierPayments = total_supplier_payments(); $expenses = total_expense_value(); return [ 'sales_invoices' => $salesInvoices, 'purchase_commitments' => $purchaseCommitments, 'customer_receipts' => $customerReceipts, 'supplier_payments' => $supplierPayments, 'expenses' => $expenses, 'receivables' => $salesInvoices - $customerReceipts, 'payables' => $purchaseCommitments - $supplierPayments, 'expected_profit' => $salesInvoices - $purchaseCommitments - $expenses, 'net_cashflow' => $customerReceipts - $supplierPayments - $expenses, 'today_receipts' => today_amount_by_type('customer_payment'), 'today_supplier_payments' => today_amount_by_type('supplier_payment'), 'today_expenses' => today_amount_by_type('expense_entry'), ]; } function customer_statement_rows(): array { $rows = []; foreach (customer_dataset() as $customer) { $rows[$customer['id']] = [ 'id' => $customer['id'], 'name' => $customer['name'], 'code' => $customer['code'], 'invoices' => 0.0, 'payments' => 0.0, 'balance' => 0.0, 'last_activity' => null, ]; } foreach (fetch_records('sales_invoice') as $invoice) { $payload = $invoice['payload_data']; $customerId = (int)($payload['customer_id'] ?? 0); if (!isset($rows[$customerId])) { continue; } $rows[$customerId]['invoices'] += (float)($payload['grand_total'] ?? 0); $rows[$customerId]['last_activity'] = max((string)($rows[$customerId]['last_activity'] ?? ''), (string)($invoice['created_at'] ?? '')); } foreach (fetch_records('customer_payment') as $payment) { $payload = $payment['payload_data']; $customerId = (int)($payload['customer_id'] ?? 0); if (!isset($rows[$customerId])) { continue; } $rows[$customerId]['payments'] += (float)($payload['amount'] ?? 0); $rows[$customerId]['last_activity'] = max((string)($rows[$customerId]['last_activity'] ?? ''), (string)($payment['created_at'] ?? '')); } foreach ($rows as &$row) { $row['balance'] = $row['invoices'] - $row['payments']; } unset($row); usort($rows, static function (array $a, array $b): int { return abs($b['balance']) <=> abs($a['balance']); }); return array_values($rows); } function supplier_statement_rows(): array { $rows = []; foreach (supplier_dataset() as $supplier) { $rows[$supplier['id']] = [ 'id' => $supplier['id'], 'name' => $supplier['name'], 'code' => $supplier['code'], 'purchases' => 0.0, 'payments' => 0.0, 'balance' => 0.0, 'last_activity' => null, ]; } foreach (fetch_records('purchase_order') as $order) { if ((string)($order['status'] ?? '') !== 'received') { continue; } $payload = $order['payload_data']; $supplierId = (int)($payload['supplier_id'] ?? 0); if (!isset($rows[$supplierId])) { continue; } $rows[$supplierId]['purchases'] += (float)($payload['grand_total'] ?? 0); $rows[$supplierId]['last_activity'] = max((string)($rows[$supplierId]['last_activity'] ?? ''), (string)($order['created_at'] ?? '')); } foreach (fetch_records('supplier_payment') as $payment) { $payload = $payment['payload_data']; $supplierId = (int)($payload['supplier_id'] ?? 0); if (!isset($rows[$supplierId])) { continue; } $rows[$supplierId]['payments'] += (float)($payload['amount'] ?? 0); $rows[$supplierId]['last_activity'] = max((string)($rows[$supplierId]['last_activity'] ?? ''), (string)($payment['created_at'] ?? '')); } foreach ($rows as &$row) { $row['balance'] = $row['purchases'] - $row['payments']; } unset($row); usort($rows, static function (array $a, array $b): int { return abs($b['balance']) <=> abs($a['balance']); }); return array_values($rows); } function recent_accounting_activity(int $limit = 8): array { $rows = []; foreach (['customer_payment', 'supplier_payment', 'expense_entry'] as $type) { foreach (fetch_records($type) as $row) { $row['payload_data'] = $row['payload_data'] ?? json_payload($row['payload'] ?? null); $rows[] = $row; } } usort($rows, static function (array $a, array $b): int { return strcmp((string)($b['created_at'] ?? ''), (string)($a['created_at'] ?? '')); }); return array_slice($rows, 0, $limit); } function inventory_totals(): array { $rawQty = 0.0; $finishedQty = 0.0; $all = fetch_records('product'); foreach ($all as $product) { $payload = $product['payload_data']; $qty = (float)($payload['stock_qty'] ?? 0); $category = (string)($payload['category'] ?? ''); if ($category === 'مواد خام') { $rawQty += $qty; } else { $finishedQty += $qty; } } return [ 'raw_qty' => $rawQty, 'finished_qty' => $finishedQty, 'all_qty' => $rawQty + $finishedQty, 'sku_count' => count($all), ]; } function customer_dataset(): array { $rows = []; foreach (fetch_records('customer') as $customer) { $payload = $customer['payload_data']; $rows[] = [ 'id' => (int)$customer['id'], 'name' => $customer['title'], 'code' => $customer['code'], 'branches' => array_values($payload['branches'] ?? []), 'allowed_skus' => array_values($payload['allowed_skus'] ?? []), 'price_overrides' => $payload['price_overrides'] ?? [], ]; } return $rows; } function product_dataset(): array { $rows = []; foreach (fetch_records('product') as $product) { $payload = $product['payload_data']; $rows[] = [ 'id' => (int)$product['id'], 'name' => $product['title'], 'code' => $product['code'], 'sku' => $payload['sku'] ?? $product['code'], 'unit' => $payload['unit'] ?? 'وحدة', 'stock_qty' => (float)($payload['stock_qty'] ?? 0), 'sale_price' => (float)($payload['sale_price'] ?? 0), 'category' => $payload['category'] ?? '', ]; } return $rows; } function supplier_dataset(): array { $rows = []; foreach (fetch_records('supplier') as $supplier) { $payload = $supplier['payload_data']; $rows[] = [ 'id' => (int)$supplier['id'], 'name' => $supplier['title'], 'code' => $supplier['code'], 'supplied_skus' => array_values($payload['supplied_skus'] ?? []), 'phone' => $payload['phone'] ?? '', 'email' => $payload['email'] ?? '', ]; } return $rows; } function raw_material_dataset(): array { return array_values(array_filter(product_dataset(), static fn (array $product): bool => ($product['category'] ?? '') === 'مواد خام')); } function finished_product_dataset(): array { return array_values(array_filter(product_dataset(), static fn (array $product): bool => ($product['category'] ?? '') !== 'مواد خام')); } function manufacturing_quality_options(): array { return [ 'accepted' => 'مطابق', 'rework' => 'بحاجة إعادة معالجة', 'rejected' => 'مرفوض', ]; } function manufacturing_quality_label(string $quality): string { $labels = manufacturing_quality_options(); return $labels[$quality] ?? $quality; } function manufacturing_output_qty(array $payload): float { return (string)($payload['quality_status'] ?? '') === 'rejected' ? 0.0 : (float)($payload['finished_qty'] ?? 0); } function sales_document_types(): array { return [ 'sales_quote' => ['label' => 'عرض سعر', 'prefix' => 'QT'], 'sales_order' => ['label' => 'أمر بيع', 'prefix' => 'SO'], 'delivery_note' => ['label' => 'أمر تسليم', 'prefix' => 'DN'], 'sales_invoice' => ['label' => 'فاتورة', 'prefix' => 'INV'], ]; } function sales_document_label(string $type): string { $types = sales_document_types(); return $types[$type]['label'] ?? $type; } function sales_document_prefix(string $type): string { $types = sales_document_types(); return $types[$type]['prefix'] ?? strtoupper(substr($type, 0, 3)); } function sales_document_record_types(): array { return array_keys(sales_document_types()); } function fetch_sales_document_by_id(int $id): ?array { $stmt = db()->prepare("SELECT * FROM erp_records WHERE id = :id LIMIT 1"); $stmt->execute(['id' => $id]); $row = $stmt->fetch(); if (!$row || !in_array((string)$row['record_type'], sales_document_record_types(), true)) { return null; } $row['payload_data'] = json_payload($row['payload'] ?? null); return $row; } function fetch_sales_documents(): array { $types = sales_document_record_types(); $placeholders = implode(',', array_fill(0, count($types), '?')); $stmt = db()->prepare("SELECT * FROM erp_records WHERE record_type IN ($placeholders) ORDER BY id DESC"); $stmt->execute($types); $rows = $stmt->fetchAll(); foreach ($rows as &$row) { $row['payload_data'] = json_payload($row['payload'] ?? null); } return $rows; } function sales_document_conversion_targets(string $type): array { return match ($type) { 'sales_quote' => ['sales_order'], 'sales_order' => ['delivery_note', 'sales_invoice'], 'delivery_note' => ['sales_invoice'], default => [], }; } function sales_document_child_exists(int $sourceId, string $targetType): bool { foreach (fetch_records($targetType) as $record) { if ((int)($record['payload_data']['source_document_id'] ?? 0) === $sourceId) { return true; } } return false; } function sales_document_children(int $sourceId): array { $children = []; foreach (fetch_sales_documents() as $record) { if ((int)($record['payload_data']['source_document_id'] ?? 0) === $sourceId) { $children[] = $record; } } return $children; } function movement_direction_label(float $qtyChange): string { return $qtyChange >= 0 ? 'إضافة' : 'خصم'; } function movement_badge_class(float $qtyChange): string { return $qtyChange >= 0 ? 'text-bg-success' : 'text-bg-danger'; } function order_status_label(string $status): string { return match ($status) { 'confirmed' => 'مؤكد', 'draft' => 'مسودة', 'received' => 'مستلم', 'completed' => 'مكتمل', 'in_progress' => 'قيد التنفيذ', 'active' => 'نشط', 'posted' => 'مرحل', default => $status, }; } function status_badge_class(string $status): string { return match ($status) { 'confirmed', 'received', 'completed', 'posted' => 'text-bg-dark', 'in_progress' => 'text-bg-warning', 'draft' => 'text-bg-secondary', default => 'text-bg-light', }; } function format_money(float $amount): string { return number_format($amount, 2) . ' ر.س'; } function insert_user(string $username, string $fullName, string $role, string $password): void { $stmt = db()->prepare( "INSERT INTO app_users (username, full_name, role, password_hash, status) VALUES (:username, :full_name, :role, :password_hash, 'active')" ); $stmt->execute([ 'username' => $username, 'full_name' => $fullName, 'role' => $role, 'password_hash' => password_hash($password, PASSWORD_BCRYPT), ]); } function default_account_credentials(): array { return [ ['username' => 'admin', 'password' => 'ChangeMe@123', 'role' => 'admin'], ['username' => 'sales', 'password' => 'ChangeMe@123', 'role' => 'sales'], ['username' => 'purchasing', 'password' => 'ChangeMe@123', 'role' => 'purchasing'], ['username' => 'production', 'password' => 'ChangeMe@123', 'role' => 'production'], ['username' => 'accounting', 'password' => 'ChangeMe@123', 'role' => 'accounting'], ]; } function seed_demo_data(): void { $userCount = (int)db()->query("SELECT COUNT(*) FROM app_users")->fetchColumn(); if ($userCount === 0) { insert_user('admin', 'مدير النظام', 'admin', 'ChangeMe@123'); insert_user('sales', 'موظف المبيعات', 'sales', 'ChangeMe@123'); insert_user('purchasing', 'موظف المشتريات', 'purchasing', 'ChangeMe@123'); insert_user('production', 'مشرف الإنتاج', 'production', 'ChangeMe@123'); insert_user('accounting', 'المحاسب', 'accounting', 'ChangeMe@123'); } $stmt = db()->prepare("SELECT COUNT(*) FROM erp_records WHERE record_type = :record_type"); $stmt->execute(['record_type' => 'customer']); if ((int)$stmt->fetchColumn() === 0) { create_record('customer', 'شركة النخبة للتوزيع', 'CUS-0001', [ 'phone' => '0500000001', 'email' => 'sales@nakhba.local', 'branches' => ['الرياض', 'جدة'], 'allowed_skus' => ['P-100', 'P-200'], 'price_overrides' => ['P-100' => 95, 'P-200' => 145], 'notes' => 'عميل جملة مع أسعار خاصة وخيارات أصناف محددة.', ]); create_record('customer', 'مؤسسة المدار التجارية', 'CUS-0002', [ 'phone' => '0500000002', 'email' => 'ops@almadar.local', 'branches' => ['الدمام'], 'allowed_skus' => ['P-100', 'P-300'], 'price_overrides' => ['P-300' => 42], 'notes' => 'عميل تجزئة بفرع واحد.', ]); } $stmt->execute(['record_type' => 'supplier']); if ((int)$stmt->fetchColumn() === 0) { create_record('supplier', 'مصنع التوريد الحديث', 'SUP-0001', [ 'phone' => '0500000300', 'email' => 'supply@modern.local', 'supplied_skus' => ['RAW-001', 'P-100'], 'notes' => 'مورد أساسي للخامات وبعض مواد التعبئة.', ]); create_record('supplier', 'الشركة العربية للمواد', 'SUP-0002', [ 'phone' => '0500000301', 'email' => 'proc@arabic.local', 'supplied_skus' => ['RAW-001', 'P-200', 'P-300'], 'notes' => 'مورد بديل للحالات العاجلة.', ]); } $stmt->execute(['record_type' => 'product']); if ((int)$stmt->fetchColumn() === 0) { create_record('product', 'مادة خام – بودرة أساسية', 'RAW-001', [ 'sku' => 'RAW-001', 'unit' => 'كجم', 'category' => 'مواد خام', 'stock_qty' => 180, 'reorder_level' => 40, 'sale_price' => 0, 'notes' => 'مخزون خام للتصنيع.', ]); create_record('product', 'منتج نهائي – عبوة 1 لتر', 'P-100', [ 'sku' => 'P-100', 'unit' => 'عبوة', 'category' => 'منتج نهائي', 'stock_qty' => 64, 'reorder_level' => 12, 'sale_price' => 110, 'notes' => 'يباع للعملاء مع تسعير خاص لبعض الحسابات.', ]); create_record('product', 'منتج نهائي – عبوة 5 لتر', 'P-200', [ 'sku' => 'P-200', 'unit' => 'عبوة', 'category' => 'منتج نهائي', 'stock_qty' => 28, 'reorder_level' => 10, 'sale_price' => 160, 'notes' => 'من أكثر الأصناف استخدامًا في أوامر البيع.', ]); create_record('product', 'منتج نهائي – عبوة تجريبية', 'P-300', [ 'sku' => 'P-300', 'unit' => 'عبوة', 'category' => 'منتج نهائي', 'stock_qty' => 9, 'reorder_level' => 8, 'sale_price' => 48, 'notes' => 'صنف منخفض المخزون يظهر في التنبيهات.', ]); } } function current_user(): ?array { static $cached = null; static $loaded = false; if ($loaded) { return $cached; } $loaded = true; $userId = (int)($_SESSION['user_id'] ?? 0); if ($userId <= 0) { return $cached = null; } $stmt = db()->prepare("SELECT id, username, full_name, role, status FROM app_users WHERE id = :id LIMIT 1"); $stmt->execute(['id' => $userId]); $user = $stmt->fetch(); if (!$user || ($user['status'] ?? '') !== 'active') { unset($_SESSION['user_id']); return $cached = null; } return $cached = $user; } function login_attempt(string $username, string $password): ?string { $stmt = db()->prepare("SELECT * FROM app_users WHERE username = :username LIMIT 1"); $stmt->execute(['username' => $username]); $user = $stmt->fetch(); if (!$user || ($user['status'] ?? '') !== 'active' || !password_verify($password, (string)$user['password_hash'])) { return 'بيانات الدخول غير صحيحة.'; } session_regenerate_id(true); $_SESSION['user_id'] = (int)$user['id']; return null; } function logout_user(): void { $_SESSION = []; if (ini_get('session.use_cookies')) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], (bool)$params['secure'], (bool)$params['httponly']); } session_destroy(); } function is_logged_in(): bool { return current_user() !== null; } function permission_map(): array { return [ 'dashboard' => ['admin', 'sales', 'purchasing', 'production', 'accounting'], 'customers' => ['admin', 'sales'], 'products' => ['admin', 'sales', 'purchasing', 'production', 'accounting'], 'orders' => ['admin', 'sales', 'accounting'], 'manufacturing' => ['admin', 'production'], 'suppliers' => ['admin', 'purchasing'], 'purchases' => ['admin', 'purchasing', 'accounting'], 'stock' => ['admin', 'sales', 'purchasing', 'production', 'accounting'], 'documents' => ['admin', 'sales', 'purchasing', 'accounting'], 'accounting' => ['admin', 'accounting'], 'reports' => ['admin', 'sales', 'purchasing', 'production', 'accounting'], ]; } function can_access(string $area): bool { $user = current_user(); if (!$user) { return false; } if (($user['role'] ?? '') === 'admin') { return true; } $map = permission_map(); return in_array((string)$user['role'], $map[$area] ?? [], true); } function require_login(): void { if (!is_logged_in()) { set_flash('warning', 'سجّل الدخول أولًا للوصول إلى النظام.'); redirect('login.php'); } } function require_permission(string $area): void { require_login(); if (!can_access($area)) { http_response_code(403); render_header('غير مصرح', 'ليس لديك صلاحية للوصول إلى هذه الصفحة.', 'dashboard'); echo '

غير مصرح بالوصول

الدور الحالي لا يملك صلاحية لهذه الصفحة. عد إلى لوحة التحكم أو سجّل الدخول بحساب مناسب.

'; render_footer(); exit; } } function active_nav(string $key, string $active): string { return $key === $active ? 'active' : ''; } function nav_items(): array { return [ ['key' => 'dashboard', 'label' => 'لوحة التحكم', 'href' => 'index.php', 'area' => 'dashboard'], ['key' => 'customers', 'label' => 'العملاء', 'href' => 'customers.php', 'area' => 'customers'], ['key' => 'suppliers', 'label' => 'الموردون', 'href' => 'suppliers.php', 'area' => 'suppliers'], ['key' => 'products', 'label' => 'الأصناف', 'href' => 'products.php', 'area' => 'products'], ['key' => 'orders', 'label' => 'المبيعات', 'href' => 'sales_orders.php', 'area' => 'orders'], ['key' => 'manufacturing', 'label' => 'التصنيع', 'href' => 'manufacturing.php', 'area' => 'manufacturing'], ['key' => 'purchases', 'label' => 'المشتريات', 'href' => 'purchases.php', 'area' => 'purchases'], ['key' => 'accounting', 'label' => 'المحاسبة', 'href' => 'accounting.php', 'area' => 'accounting'], ['key' => 'reports', 'label' => 'التقارير', 'href' => 'reports.php', 'area' => 'reports'], ['key' => 'stock', 'label' => 'حركات المخزون', 'href' => 'stock_movements.php', 'area' => 'stock'], ]; } function adjust_product_stock(array $product, float $qtyChange, string $movementType, string $referenceCode, string $referenceType, int $referenceId = 0, string $notes = ''): array { $payload = $product['payload_data']; $before = (float)($payload['stock_qty'] ?? 0); $after = $before + $qtyChange; if ($after < 0) { throw new RuntimeException('Insufficient stock.'); } $payload['stock_qty'] = $after; update_record_payload((int)$product['id'], $payload); $movementPayload = [ 'product_id' => (int)$product['id'], 'product_name' => $product['title'], 'sku' => $payload['sku'] ?? $product['code'], 'unit' => $payload['unit'] ?? 'وحدة', 'qty_change' => $qtyChange, 'qty_before' => $before, 'qty_after' => $after, 'movement_type' => $movementType, 'reference_code' => $referenceCode, 'reference_type' => $referenceType, 'reference_id' => $referenceId, 'notes' => $notes, 'created_by' => current_user()['username'] ?? 'system', ]; create_record( 'stock_movement', (($qtyChange >= 0 ? 'إضافة ' : 'خصم ') . $product['title']), next_code('MOV', 'stock_movement'), $movementPayload, 'confirmed' ); return ['before' => $before, 'after' => $after]; } function render_header(string $pageTitle, string $pageDescription, string $active = 'dashboard'): void { $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? $pageDescription; $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; $projectName = app_name(); $user = current_user(); ?> <?= e($pageTitle) ?> | <?= e($projectName) ?>