895 lines
42 KiB
PHP
895 lines
42 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
session_start();
|
||
@date_default_timezone_set('UTC');
|
||
|
||
require_once __DIR__ . '/db/config.php';
|
||
|
||
function e(?string $value): string
|
||
{
|
||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
function redirectTo(string $url): void
|
||
{
|
||
header('Location: ' . $url);
|
||
exit;
|
||
}
|
||
|
||
function setFlash(string $type, string $message): void
|
||
{
|
||
$_SESSION['flash'] = ['type' => $type, 'message' => $message];
|
||
}
|
||
|
||
function pullFlash(): ?array
|
||
{
|
||
if (!isset($_SESSION['flash'])) {
|
||
return null;
|
||
}
|
||
|
||
$flash = $_SESSION['flash'];
|
||
unset($_SESSION['flash']);
|
||
return $flash;
|
||
}
|
||
|
||
function csrfToken(): string
|
||
{
|
||
if (empty($_SESSION['csrf_token'])) {
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
}
|
||
|
||
return (string) $_SESSION['csrf_token'];
|
||
}
|
||
|
||
function verifyCsrf(): void
|
||
{
|
||
$token = (string) ($_POST['csrf_token'] ?? '');
|
||
if ($token === '' || !hash_equals((string) ($_SESSION['csrf_token'] ?? ''), $token)) {
|
||
throw new RuntimeException('انتهت الجلسة. أعد تحميل الصفحة ثم جرّب مرة ثانية.');
|
||
}
|
||
}
|
||
|
||
function fetchAllRows(PDO $pdo, string $sql, array $params = []): array
|
||
{
|
||
$stmt = $pdo->prepare($sql);
|
||
$stmt->execute($params);
|
||
return $stmt->fetchAll();
|
||
}
|
||
|
||
function fetchRow(PDO $pdo, string $sql, array $params = []): ?array
|
||
{
|
||
$stmt = $pdo->prepare($sql);
|
||
$stmt->execute($params);
|
||
$row = $stmt->fetch();
|
||
return $row ?: null;
|
||
}
|
||
|
||
function fetchValue(PDO $pdo, string $sql, array $params = [])
|
||
{
|
||
$stmt = $pdo->prepare($sql);
|
||
$stmt->execute($params);
|
||
return $stmt->fetchColumn();
|
||
}
|
||
|
||
function ensureSchema(PDO $pdo): void
|
||
{
|
||
$pdo->exec(
|
||
"CREATE TABLE IF NOT EXISTS app_users (
|
||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
username VARCHAR(100) NOT NULL UNIQUE,
|
||
full_name VARCHAR(150) NOT NULL,
|
||
password_hash VARCHAR(255) NOT NULL,
|
||
role VARCHAR(50) NOT NULL DEFAULT 'admin',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||
);
|
||
|
||
$pdo->exec(
|
||
"CREATE TABLE IF NOT EXISTS app_settings (
|
||
setting_key VARCHAR(100) PRIMARY KEY,
|
||
setting_value VARCHAR(255) NOT NULL,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||
);
|
||
|
||
$pdo->exec(
|
||
"CREATE TABLE IF NOT EXISTS equipment (
|
||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(180) NOT NULL,
|
||
quantity INT NOT NULL DEFAULT 0,
|
||
price_usd DECIMAL(12,2) NOT NULL DEFAULT 0,
|
||
notes TEXT DEFAULT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||
);
|
||
|
||
$pdo->exec(
|
||
"CREATE TABLE IF NOT EXISTS invoices (
|
||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
invoice_number VARCHAR(50) NOT NULL UNIQUE,
|
||
customer_name VARCHAR(180) DEFAULT NULL,
|
||
exchange_rate DECIMAL(12,2) NOT NULL,
|
||
total_usd DECIMAL(12,2) NOT NULL,
|
||
total_syp BIGINT NOT NULL,
|
||
notes TEXT DEFAULT NULL,
|
||
created_by VARCHAR(120) DEFAULT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||
);
|
||
|
||
$pdo->exec(
|
||
"CREATE TABLE IF NOT EXISTS invoice_items (
|
||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
invoice_id INT UNSIGNED NOT NULL,
|
||
equipment_id INT UNSIGNED NOT NULL,
|
||
item_name VARCHAR(180) NOT NULL,
|
||
quantity INT NOT NULL,
|
||
unit_price_usd DECIMAL(12,2) NOT NULL,
|
||
line_total_usd DECIMAL(12,2) NOT NULL,
|
||
line_total_syp BIGINT NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT fk_invoice_items_invoice FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE,
|
||
CONSTRAINT fk_invoice_items_equipment FOREIGN KEY (equipment_id) REFERENCES equipment(id) ON DELETE RESTRICT
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||
);
|
||
}
|
||
|
||
function seedDefaults(PDO $pdo): void
|
||
{
|
||
$defaultPasswordHash = password_hash('admin123', PASSWORD_DEFAULT);
|
||
$adminUser = fetchRow($pdo, 'SELECT * FROM app_users WHERE username = :username LIMIT 1', [':username' => 'admin']);
|
||
|
||
if (!$adminUser) {
|
||
$stmt = $pdo->prepare('INSERT INTO app_users (username, full_name, password_hash, role) VALUES (:username, :full_name, :password_hash, :role)');
|
||
$stmt->execute([
|
||
':username' => 'admin',
|
||
':full_name' => 'مدير المحل',
|
||
':password_hash' => $defaultPasswordHash,
|
||
':role' => 'admin',
|
||
]);
|
||
} elseif (!password_verify('admin123', (string) $adminUser['password_hash']) || (string) $adminUser['full_name'] !== 'مدير المحل' || (string) $adminUser['role'] !== 'admin') {
|
||
$stmt = $pdo->prepare('UPDATE app_users SET full_name = :full_name, password_hash = :password_hash, role = :role WHERE id = :id');
|
||
$stmt->execute([
|
||
':full_name' => 'مدير المحل',
|
||
':password_hash' => $defaultPasswordHash,
|
||
':role' => 'admin',
|
||
':id' => (int) $adminUser['id'],
|
||
]);
|
||
}
|
||
|
||
$settingCount = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM app_settings WHERE setting_key = :key', [':key' => 'exchange_rate']);
|
||
if ($settingCount === 0) {
|
||
$stmt = $pdo->prepare('INSERT INTO app_settings (setting_key, setting_value) VALUES (:key, :value)');
|
||
$stmt->execute([
|
||
':key' => 'exchange_rate',
|
||
':value' => '15000',
|
||
]);
|
||
}
|
||
|
||
$equipmentCount = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM equipment');
|
||
if ($equipmentCount === 0) {
|
||
$stmt = $pdo->prepare('INSERT INTO equipment (name, quantity, price_usd, notes) VALUES (:name, :quantity, :price_usd, :notes)');
|
||
$items = [
|
||
['طابعة فواتير', 5, 120, 'طابعة حرارية 80 مم'],
|
||
['قارئ باركود', 8, 75, 'مناسب لنقاط البيع'],
|
||
['مولدة صغيرة', 3, 250, 'للأحمال الخفيفة'],
|
||
];
|
||
|
||
foreach ($items as [$name, $quantity, $priceUsd, $notes]) {
|
||
$stmt->execute([
|
||
':name' => $name,
|
||
':quantity' => $quantity,
|
||
':price_usd' => $priceUsd,
|
||
':notes' => $notes,
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function getSetting(PDO $pdo, string $key, string $default = ''): string
|
||
{
|
||
$value = fetchValue($pdo, 'SELECT setting_value FROM app_settings WHERE setting_key = :key LIMIT 1', [':key' => $key]);
|
||
return $value !== false ? (string) $value : $default;
|
||
}
|
||
|
||
function setSetting(PDO $pdo, string $key, string $value): void
|
||
{
|
||
$stmt = $pdo->prepare(
|
||
'INSERT INTO app_settings (setting_key, setting_value) VALUES (:key, :value)
|
||
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'
|
||
);
|
||
$stmt->execute([
|
||
':key' => $key,
|
||
':value' => $value,
|
||
]);
|
||
}
|
||
|
||
function currentUser(): ?array
|
||
{
|
||
return isset($_SESSION['user']) && is_array($_SESSION['user']) ? $_SESSION['user'] : null;
|
||
}
|
||
|
||
function isLoggedIn(): bool
|
||
{
|
||
return currentUser() !== null;
|
||
}
|
||
|
||
function loginUser(array $user): void
|
||
{
|
||
session_regenerate_id(true);
|
||
$_SESSION['user'] = [
|
||
'id' => (int) $user['id'],
|
||
'username' => (string) $user['username'],
|
||
'full_name' => (string) $user['full_name'],
|
||
'role' => (string) $user['role'],
|
||
];
|
||
}
|
||
|
||
function logoutUser(): 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 currentTab(): string
|
||
{
|
||
$allowed = ['equipment', 'invoices', 'reports'];
|
||
$tab = (string) ($_GET['tab'] ?? 'equipment');
|
||
return in_array($tab, $allowed, true) ? $tab : 'equipment';
|
||
}
|
||
|
||
function formatUsd(float $value): string
|
||
{
|
||
return '$' . number_format($value, 2);
|
||
}
|
||
|
||
function formatSyp(float $value): string
|
||
{
|
||
return number_format($value, 0) . ' ل.س';
|
||
}
|
||
|
||
function getEquipmentRows(PDO $pdo): array
|
||
{
|
||
return fetchAllRows($pdo, 'SELECT id, name, quantity, price_usd, notes, created_at, updated_at FROM equipment ORDER BY id DESC');
|
||
}
|
||
|
||
function getInvoices(PDO $pdo): array
|
||
{
|
||
return fetchAllRows(
|
||
$pdo,
|
||
'SELECT id, invoice_number, customer_name, exchange_rate, total_usd, total_syp, created_by, created_at
|
||
FROM invoices ORDER BY id DESC LIMIT 100'
|
||
);
|
||
}
|
||
|
||
function getInvoiceDetails(PDO $pdo, int $invoiceId): ?array
|
||
{
|
||
$invoice = fetchRow($pdo, 'SELECT * FROM invoices WHERE id = :id LIMIT 1', [':id' => $invoiceId]);
|
||
if (!$invoice) {
|
||
return null;
|
||
}
|
||
|
||
$items = fetchAllRows(
|
||
$pdo,
|
||
'SELECT item_name, quantity, unit_price_usd, line_total_usd, line_total_syp
|
||
FROM invoice_items WHERE invoice_id = :invoice_id ORDER BY id ASC',
|
||
[':invoice_id' => $invoiceId]
|
||
);
|
||
|
||
$invoice['items'] = $items;
|
||
return $invoice;
|
||
}
|
||
|
||
$pdo = db();
|
||
ensureSchema($pdo);
|
||
seedDefaults($pdo);
|
||
|
||
$projectName = $_SERVER['PROJECT_NAME'] ?? 'إدارة محل بسيطة';
|
||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'تطبيق مبسط لإدارة المعدات والفواتير والتقارير مع تسعير بالدولار وتحويل فوري إلى الليرة السورية.';
|
||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||
$assetCssVersion = (string) (@filemtime(__DIR__ . '/assets/css/custom.css') ?: time());
|
||
$assetJsVersion = (string) (@filemtime(__DIR__ . '/assets/js/main.js') ?: time());
|
||
|
||
$runtimeFlash = null;
|
||
$tab = currentTab();
|
||
$quickLogin = (string) ($_GET['quick_login'] ?? '');
|
||
if ($quickLogin === 'demo') {
|
||
$demoUser = fetchRow($pdo, 'SELECT * FROM app_users WHERE username = :username LIMIT 1', [':username' => 'admin']);
|
||
if ($demoUser) {
|
||
loginUser($demoUser);
|
||
$runtimeFlash = ['type' => 'success', 'message' => 'تم الدخول التجريبي مباشرة.'];
|
||
$tab = 'equipment';
|
||
} else {
|
||
$runtimeFlash = ['type' => 'error', 'message' => 'الحساب التجريبي غير موجود.'];
|
||
}
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$action = '';
|
||
try {
|
||
verifyCsrf();
|
||
$action = (string) ($_POST['action'] ?? '');
|
||
|
||
if ($action === 'logout') {
|
||
logoutUser();
|
||
redirectTo('index.php');
|
||
}
|
||
|
||
if ($action === 'login') {
|
||
$username = mb_strtolower(trim((string) ($_POST['username'] ?? '')));
|
||
$password = trim((string) ($_POST['password'] ?? ''));
|
||
if ($username === '' || $password === '') {
|
||
throw new RuntimeException('أدخل اسم المستخدم وكلمة المرور.');
|
||
}
|
||
|
||
$user = fetchRow($pdo, 'SELECT * FROM app_users WHERE username = :username LIMIT 1', [':username' => $username]);
|
||
if (!$user || !password_verify($password, (string) $user['password_hash'])) {
|
||
throw new RuntimeException('بيانات الدخول غير صحيحة. استخدم بالضبط: admin / admin123');
|
||
}
|
||
|
||
loginUser($user);
|
||
$runtimeFlash = ['type' => 'success', 'message' => 'تم تسجيل الدخول بنجاح.'];
|
||
$tab = 'equipment';
|
||
}
|
||
|
||
if (!isLoggedIn()) {
|
||
throw new RuntimeException('يرجى تسجيل الدخول أولاً.');
|
||
}
|
||
|
||
if ($action === 'save_rate') {
|
||
$rate = (float) ($_POST['exchange_rate'] ?? 0);
|
||
if ($rate <= 0) {
|
||
throw new RuntimeException('أدخل قيمة صحيحة لسعر الدولار اليومي.');
|
||
}
|
||
|
||
setSetting($pdo, 'exchange_rate', (string) $rate);
|
||
setFlash('success', 'تم تحديث سعر الدولار اليومي.');
|
||
redirectTo('index.php?tab=equipment');
|
||
}
|
||
|
||
if ($action === 'add_equipment') {
|
||
$name = trim((string) ($_POST['name'] ?? ''));
|
||
$quantity = (int) ($_POST['quantity'] ?? 0);
|
||
$priceUsd = (float) ($_POST['price_usd'] ?? 0);
|
||
$notes = trim((string) ($_POST['notes'] ?? ''));
|
||
|
||
if ($name === '') {
|
||
throw new RuntimeException('أدخل اسم المعدة.');
|
||
}
|
||
if ($quantity < 0) {
|
||
throw new RuntimeException('الكمية يجب أن تكون صفر أو أكثر.');
|
||
}
|
||
if ($priceUsd <= 0) {
|
||
throw new RuntimeException('سعر الدولار يجب أن يكون أكبر من الصفر.');
|
||
}
|
||
|
||
$stmt = $pdo->prepare('INSERT INTO equipment (name, quantity, price_usd, notes) VALUES (:name, :quantity, :price_usd, :notes)');
|
||
$stmt->execute([
|
||
':name' => $name,
|
||
':quantity' => $quantity,
|
||
':price_usd' => $priceUsd,
|
||
':notes' => $notes !== '' ? $notes : null,
|
||
]);
|
||
|
||
setFlash('success', 'تمت إضافة المعدة بنجاح.');
|
||
redirectTo('index.php?tab=equipment');
|
||
}
|
||
|
||
if ($action === 'create_invoice') {
|
||
$equipmentId = (int) ($_POST['equipment_id'] ?? 0);
|
||
$quantity = (int) ($_POST['invoice_quantity'] ?? 0);
|
||
$customerName = trim((string) ($_POST['customer_name'] ?? ''));
|
||
$notes = trim((string) ($_POST['invoice_notes'] ?? ''));
|
||
$exchangeRate = (float) getSetting($pdo, 'exchange_rate', '0');
|
||
|
||
if ($equipmentId <= 0) {
|
||
throw new RuntimeException('اختر المعدة أولاً.');
|
||
}
|
||
if ($quantity <= 0) {
|
||
throw new RuntimeException('أدخل كمية صحيحة للفاتورة.');
|
||
}
|
||
if ($exchangeRate <= 0) {
|
||
throw new RuntimeException('أدخل سعر الدولار اليومي قبل إنشاء أي فاتورة.');
|
||
}
|
||
|
||
$pdo->beginTransaction();
|
||
try {
|
||
$equipment = fetchRow($pdo, 'SELECT id, name, quantity, price_usd FROM equipment WHERE id = :id FOR UPDATE', [':id' => $equipmentId]);
|
||
if (!$equipment) {
|
||
throw new RuntimeException('المعدة غير موجودة.');
|
||
}
|
||
if ((int) $equipment['quantity'] < $quantity) {
|
||
throw new RuntimeException('الكمية المطلوبة أكبر من المخزون المتاح.');
|
||
}
|
||
|
||
$lineTotalUsd = round((float) $equipment['price_usd'] * $quantity, 2);
|
||
$lineTotalSyp = (int) round($lineTotalUsd * $exchangeRate);
|
||
|
||
$invoiceStmt = $pdo->prepare(
|
||
'INSERT INTO invoices (invoice_number, customer_name, exchange_rate, total_usd, total_syp, notes, created_by)
|
||
VALUES (:invoice_number, :customer_name, :exchange_rate, :total_usd, :total_syp, :notes, :created_by)'
|
||
);
|
||
|
||
$tempNumber = 'TMP-' . bin2hex(random_bytes(4));
|
||
$invoiceStmt->execute([
|
||
':invoice_number' => $tempNumber,
|
||
':customer_name' => $customerName !== '' ? $customerName : null,
|
||
':exchange_rate' => $exchangeRate,
|
||
':total_usd' => $lineTotalUsd,
|
||
':total_syp' => $lineTotalSyp,
|
||
':notes' => $notes !== '' ? $notes : null,
|
||
':created_by' => currentUser()['full_name'] ?? 'admin',
|
||
]);
|
||
|
||
$invoiceId = (int) $pdo->lastInsertId();
|
||
$invoiceNumber = 'INV-' . gmdate('Ymd') . '-' . str_pad((string) $invoiceId, 4, '0', STR_PAD_LEFT);
|
||
|
||
$updateNumberStmt = $pdo->prepare('UPDATE invoices SET invoice_number = :invoice_number WHERE id = :id');
|
||
$updateNumberStmt->execute([
|
||
':invoice_number' => $invoiceNumber,
|
||
':id' => $invoiceId,
|
||
]);
|
||
|
||
$itemStmt = $pdo->prepare(
|
||
'INSERT INTO invoice_items (invoice_id, equipment_id, item_name, quantity, unit_price_usd, line_total_usd, line_total_syp)
|
||
VALUES (:invoice_id, :equipment_id, :item_name, :quantity, :unit_price_usd, :line_total_usd, :line_total_syp)'
|
||
);
|
||
$itemStmt->execute([
|
||
':invoice_id' => $invoiceId,
|
||
':equipment_id' => $equipmentId,
|
||
':item_name' => $equipment['name'],
|
||
':quantity' => $quantity,
|
||
':unit_price_usd' => $equipment['price_usd'],
|
||
':line_total_usd' => $lineTotalUsd,
|
||
':line_total_syp' => $lineTotalSyp,
|
||
]);
|
||
|
||
$stockStmt = $pdo->prepare('UPDATE equipment SET quantity = quantity - :qty WHERE id = :id');
|
||
$stockStmt->execute([
|
||
':qty' => $quantity,
|
||
':id' => $equipmentId,
|
||
]);
|
||
|
||
$pdo->commit();
|
||
setFlash('success', 'تم إنشاء الفاتورة وتحديث المخزون تلقائياً.');
|
||
redirectTo('index.php?tab=invoices&invoice_id=' . $invoiceId);
|
||
} catch (Throwable $exception) {
|
||
$pdo->rollBack();
|
||
throw $exception;
|
||
}
|
||
}
|
||
} catch (Throwable $exception) {
|
||
if ($action === 'login') {
|
||
$runtimeFlash = ['type' => 'error', 'message' => $exception->getMessage()];
|
||
$tab = 'equipment';
|
||
} else {
|
||
setFlash('error', $exception->getMessage());
|
||
$tab = currentTab();
|
||
redirectTo('index.php?tab=' . $tab);
|
||
}
|
||
}
|
||
}
|
||
|
||
$flash = $runtimeFlash ?? pullFlash();
|
||
$csrfToken = csrfToken();
|
||
$exchangeRate = (float) getSetting($pdo, 'exchange_rate', '15000');
|
||
$equipmentRows = getEquipmentRows($pdo);
|
||
$invoiceRows = getInvoices($pdo);
|
||
$selectedInvoiceId = (int) ($_GET['invoice_id'] ?? ($invoiceRows[0]['id'] ?? 0));
|
||
$selectedInvoice = $selectedInvoiceId > 0 ? getInvoiceDetails($pdo, $selectedInvoiceId) : null;
|
||
$tab = currentTab();
|
||
$user = currentUser();
|
||
|
||
$totalEquipmentTypes = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM equipment');
|
||
$totalStockUnits = (int) fetchValue($pdo, 'SELECT COALESCE(SUM(quantity), 0) FROM equipment');
|
||
$totalInvoices = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM invoices');
|
||
$totalSalesSyp = (float) fetchValue($pdo, 'SELECT COALESCE(SUM(total_syp), 0) FROM invoices');
|
||
$todaySalesSyp = (float) fetchValue($pdo, 'SELECT COALESCE(SUM(total_syp), 0) FROM invoices WHERE DATE(created_at) = UTC_DATE()');
|
||
$todayInvoices = (int) fetchValue($pdo, 'SELECT COUNT(*) FROM invoices WHERE DATE(created_at) = UTC_DATE()');
|
||
?>
|
||
<!doctype html>
|
||
<html lang="ar" dir="rtl">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title><?= e($projectName) ?> | إدارة محل بسيطة</title>
|
||
<meta name="description" content="<?= e($projectDescription) ?>">
|
||
<meta name="keywords" content="إدارة محل, معدات, فواتير, تقارير, دولار, ليرة سورية">
|
||
<meta name="author" content="Flatlogic AI">
|
||
<meta property="og:title" content="<?= e($projectName) ?>">
|
||
<meta property="og:description" content="<?= e($projectDescription) ?>">
|
||
<?php if ($projectImageUrl !== ''): ?>
|
||
<meta property="og:image" content="<?= e($projectImageUrl) ?>">
|
||
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>">
|
||
<?php endif; ?>
|
||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= e($assetCssVersion) ?>">
|
||
</head>
|
||
<body data-theme="light" data-current-rate="<?= e((string) $exchangeRate) ?>">
|
||
<div class="page-shell">
|
||
<header class="topbar">
|
||
<div>
|
||
<p class="eyebrow">Store Admin</p>
|
||
<h1><?= isLoggedIn() ? 'إدارة المحل' : 'تسجيل الدخول' ?></h1>
|
||
<p class="subtitle">
|
||
<?= isLoggedIn() ? 'واجهة بسيطة: معدات + فواتير + تقارير، مع تسعير ثابت بالدولار وتحويل يومي إلى الليرة السورية.' : 'ادخل بحساب واحد بسيط ثم افتح 3 أقسام فقط داخل التطبيق.' ?>
|
||
</p>
|
||
</div>
|
||
<div class="topbar-actions">
|
||
<button type="button" class="ghost-btn" data-theme-toggle>نهاري / ليلي</button>
|
||
<button type="button" class="ghost-btn" data-help-toggle>مساعدة</button>
|
||
</div>
|
||
</header>
|
||
|
||
<aside class="help-drawer" data-help-drawer aria-hidden="true">
|
||
<div class="help-card">
|
||
<div class="help-header">
|
||
<h2>مساعدة سريعة</h2>
|
||
<button type="button" class="icon-btn" data-help-close aria-label="إغلاق">×</button>
|
||
</div>
|
||
<ol>
|
||
<li>سجّل الدخول بالحساب التجريبي أو بحسابك.</li>
|
||
<li>حدّث سعر الدولار اليومي مرة واحدة.</li>
|
||
<li>أضف المعدات بالدولار والكمية الحالية.</li>
|
||
<li>أنشئ فاتورة، وسيتم حساب الليرة السورية وتخفيض المخزون تلقائياً.</li>
|
||
<li>من قسم التقارير راقب المبيعات اليومية والإجمالية.</li>
|
||
</ol>
|
||
<p class="help-note">بيانات التجربة الحالية: <strong>admin</strong> / <strong>admin123</strong></p>
|
||
</div>
|
||
</aside>
|
||
|
||
<?php if (!isLoggedIn()): ?>
|
||
<main class="login-layout">
|
||
<section class="login-card">
|
||
<div class="card-head">
|
||
<h2>دخول المدير</h2>
|
||
<p>أبسط نسخة ممكنة: شاشة دخول واحدة ثم 3 أقسام فقط داخل التطبيق.</p>
|
||
</div>
|
||
|
||
<?php if ($flash): ?>
|
||
<div class="alert alert-<?= e($flash['type']) ?>" data-auto-hide><?= e($flash['message']) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<div class="demo-box">
|
||
<p><strong>بيانات الدخول الأكيدة:</strong> <span dir="ltr">admin / admin123</span></p>
|
||
<a class="primary-link" href="index.php?quick_login=demo">دخول مباشر للتطبيق</a>
|
||
</div>
|
||
|
||
<form method="post" class="stack-form">
|
||
<input type="hidden" name="csrf_token" value="<?= e($csrfToken) ?>">
|
||
<input type="hidden" name="action" value="login">
|
||
|
||
<label>
|
||
<span>اسم المستخدم</span>
|
||
<input type="text" name="username" placeholder="admin" autocomplete="username" required>
|
||
</label>
|
||
<label>
|
||
<span>كلمة المرور</span>
|
||
<input type="password" name="password" placeholder="admin123" autocomplete="current-password" required>
|
||
</label>
|
||
<button type="submit" class="primary-btn">دخول</button>
|
||
</form>
|
||
</section>
|
||
|
||
<section class="login-preview">
|
||
<div class="preview-card gradient-card">
|
||
<h2>شو داخل التطبيق؟</h2>
|
||
<ul>
|
||
<li>واجهة <strong>المعدات</strong> لإدخال أسعار الدولار والكمية.</li>
|
||
<li>واجهة <strong>الفواتير</strong> لحساب السعر بالليرة السورية مباشرة.</li>
|
||
<li>واجهة <strong>التقارير</strong> لمبيعات اليوم والإجمالي.</li>
|
||
<li>زر <strong>PDF</strong> وزر <strong>طباعة</strong> للفواتير.</li>
|
||
<li>زر <strong>نهاري / ليلي</strong> + زر <strong>مساعدة</strong>.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
<?php else: ?>
|
||
<main class="app-layout">
|
||
<?php if ($flash): ?>
|
||
<div class="alert alert-<?= e($flash['type']) ?> app-alert" data-auto-hide><?= e($flash['message']) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<section class="hero-card">
|
||
<div>
|
||
<p class="eyebrow">مرحبا، <?= e($user['full_name'] ?? 'مدير المحل') ?></p>
|
||
<h2>تطبيق مبسط فعلاً</h2>
|
||
<p>كل الأسعار الأساسية بالدولار، والتحويل إلى الليرة السورية يتم حسب سعر اليوم فقط.</p>
|
||
</div>
|
||
<div class="hero-actions">
|
||
<form method="post">
|
||
<input type="hidden" name="csrf_token" value="<?= e($csrfToken) ?>">
|
||
<input type="hidden" name="action" value="logout">
|
||
<button type="submit" class="ghost-btn">تسجيل الخروج</button>
|
||
</form>
|
||
</div>
|
||
</section>
|
||
|
||
<nav class="tab-nav" aria-label="أقسام التطبيق">
|
||
<a class="tab-link <?= $tab === 'equipment' ? 'is-active' : '' ?>" href="index.php?tab=equipment">المعدات</a>
|
||
<a class="tab-link <?= $tab === 'invoices' ? 'is-active' : '' ?>" href="index.php?tab=invoices">الفواتير</a>
|
||
<a class="tab-link <?= $tab === 'reports' ? 'is-active' : '' ?>" href="index.php?tab=reports">التقارير</a>
|
||
</nav>
|
||
|
||
<?php if ($tab === 'equipment'): ?>
|
||
<section class="grid two-col">
|
||
<article class="panel-card">
|
||
<div class="card-head">
|
||
<h2>سعر الدولار اليومي</h2>
|
||
<p>هذا هو الرقم الوحيد الذي يتغير، أما أسعار المعدات فتظل ثابتة بالدولار.</p>
|
||
</div>
|
||
<form method="post" class="stack-form compact-form">
|
||
<input type="hidden" name="csrf_token" value="<?= e($csrfToken) ?>">
|
||
<input type="hidden" name="action" value="save_rate">
|
||
<label>
|
||
<span>سعر الدولار اليوم</span>
|
||
<input type="number" step="0.01" min="0" name="exchange_rate" value="<?= e((string) $exchangeRate) ?>" data-rate-input required>
|
||
</label>
|
||
<div class="stat-inline">
|
||
<span>المعاينة الحالية بالليرة:</span>
|
||
<strong data-rate-preview><?= e(formatSyp($exchangeRate)) ?></strong>
|
||
</div>
|
||
<button type="submit" class="primary-btn">حفظ السعر</button>
|
||
</form>
|
||
</article>
|
||
|
||
<article class="panel-card">
|
||
<div class="card-head">
|
||
<h2>إضافة معدة</h2>
|
||
<p>أدخل السعر بالدولار، والتطبيق سيحسب الليرة السورية عند البيع فقط.</p>
|
||
</div>
|
||
<form method="post" class="stack-form compact-form">
|
||
<input type="hidden" name="csrf_token" value="<?= e($csrfToken) ?>">
|
||
<input type="hidden" name="action" value="add_equipment">
|
||
<label>
|
||
<span>اسم المعدة</span>
|
||
<input type="text" name="name" placeholder="مثال: شاشة 24 إنش" required>
|
||
</label>
|
||
<div class="split-fields">
|
||
<label>
|
||
<span>الكمية</span>
|
||
<input type="number" min="0" name="quantity" placeholder="10" required>
|
||
</label>
|
||
<label>
|
||
<span>السعر بالدولار</span>
|
||
<input type="number" step="0.01" min="0" name="price_usd" placeholder="150" data-equipment-usd required>
|
||
</label>
|
||
</div>
|
||
<label>
|
||
<span>ملاحظات</span>
|
||
<textarea name="notes" rows="3" placeholder="اختياري"></textarea>
|
||
</label>
|
||
<div class="stat-inline">
|
||
<span>معاينة السعر بالليرة حسب اليوم:</span>
|
||
<strong data-equipment-preview>—</strong>
|
||
</div>
|
||
<button type="submit" class="primary-btn">إضافة المعدة</button>
|
||
</form>
|
||
</article>
|
||
</section>
|
||
|
||
<section class="panel-card">
|
||
<div class="card-head inline-head">
|
||
<div>
|
||
<h2>قائمة المعدات</h2>
|
||
<p><?= e((string) $totalEquipmentTypes) ?> نوع / <?= e((string) $totalStockUnits) ?> قطعة في المخزون</p>
|
||
</div>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>المعدة</th>
|
||
<th>الكمية</th>
|
||
<th>السعر بالدولار</th>
|
||
<th>السعر بالليرة</th>
|
||
<th>ملاحظات</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (!$equipmentRows): ?>
|
||
<tr><td colspan="5" class="empty-cell">لا توجد معدات حتى الآن.</td></tr>
|
||
<?php else: ?>
|
||
<?php foreach ($equipmentRows as $item): ?>
|
||
<tr>
|
||
<td><?= e($item['name']) ?></td>
|
||
<td><?= e((string) $item['quantity']) ?></td>
|
||
<td><?= e(formatUsd((float) $item['price_usd'])) ?></td>
|
||
<td><?= e(formatSyp((float) $item['price_usd'] * $exchangeRate)) ?></td>
|
||
<td><?= e($item['notes'] ?? '—') ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
<?php elseif ($tab === 'invoices'): ?>
|
||
<section class="grid two-col invoice-grid">
|
||
<article class="panel-card">
|
||
<div class="card-head">
|
||
<h2>إنشاء فاتورة</h2>
|
||
<p>اختر معدة واحدة، أدخل الكمية، وسيتم خصمها من المخزون مباشرة.</p>
|
||
</div>
|
||
<form method="post" class="stack-form compact-form">
|
||
<input type="hidden" name="csrf_token" value="<?= e($csrfToken) ?>">
|
||
<input type="hidden" name="action" value="create_invoice">
|
||
<label>
|
||
<span>المعدة</span>
|
||
<select name="equipment_id" data-invoice-equipment required>
|
||
<option value="">اختر من القائمة</option>
|
||
<?php foreach ($equipmentRows as $item): ?>
|
||
<option value="<?= e((string) $item['id']) ?>" data-price="<?= e((string) $item['price_usd']) ?>" data-stock="<?= e((string) $item['quantity']) ?>">
|
||
<?= e($item['name']) ?> — <?= e(formatUsd((float) $item['price_usd'])) ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</label>
|
||
<div class="split-fields">
|
||
<label>
|
||
<span>الكمية</span>
|
||
<input type="number" min="1" name="invoice_quantity" value="1" data-invoice-qty required>
|
||
</label>
|
||
<label>
|
||
<span>العميل</span>
|
||
<input type="text" name="customer_name" placeholder="اختياري">
|
||
</label>
|
||
</div>
|
||
<label>
|
||
<span>ملاحظات</span>
|
||
<textarea name="invoice_notes" rows="3" placeholder="اختياري"></textarea>
|
||
</label>
|
||
<div class="invoice-preview-box">
|
||
<div><span>المخزون الحالي:</span> <strong data-invoice-stock>—</strong></div>
|
||
<div><span>إجمالي الفاتورة بالليرة:</span> <strong data-invoice-total>—</strong></div>
|
||
</div>
|
||
<button type="submit" class="primary-btn">حفظ الفاتورة</button>
|
||
</form>
|
||
</article>
|
||
|
||
<article class="panel-card printable-panel" data-printable-invoice>
|
||
<div class="card-head inline-head">
|
||
<div>
|
||
<h2>الفاتورة المحددة</h2>
|
||
<p>اختر فاتورة من الجدول أو أنشئ واحدة جديدة لتظهر هنا.</p>
|
||
</div>
|
||
<div class="inline-actions">
|
||
<button type="button" class="ghost-btn" data-print-invoice <?= $selectedInvoice ? '' : 'disabled' ?>>طباعة</button>
|
||
<button type="button" class="ghost-btn" data-export-selected-pdf <?= $selectedInvoice ? '' : 'disabled' ?>>PDF للفاتورة</button>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if (!$selectedInvoice): ?>
|
||
<div class="empty-state">لا توجد فاتورة محددة بعد.</div>
|
||
<?php else: ?>
|
||
<div class="invoice-sheet">
|
||
<div class="invoice-row"><span>رقم الفاتورة</span><strong><?= e($selectedInvoice['invoice_number']) ?></strong></div>
|
||
<div class="invoice-row"><span>العميل</span><strong><?= e($selectedInvoice['customer_name'] ?: 'عميل مباشر') ?></strong></div>
|
||
<div class="invoice-row"><span>سعر الدولار</span><strong><?= e(formatSyp((float) $selectedInvoice['exchange_rate'])) ?></strong></div>
|
||
<div class="invoice-row"><span>الإجمالي بالدولار</span><strong><?= e(formatUsd((float) $selectedInvoice['total_usd'])) ?></strong></div>
|
||
<div class="invoice-row"><span>الإجمالي بالليرة</span><strong><?= e(formatSyp((float) $selectedInvoice['total_syp'])) ?></strong></div>
|
||
<div class="invoice-row"><span>التاريخ</span><strong><?= e((string) $selectedInvoice['created_at']) ?></strong></div>
|
||
<div class="invoice-items">
|
||
<h3>العناصر</h3>
|
||
<?php foreach ($selectedInvoice['items'] as $line): ?>
|
||
<div class="invoice-item">
|
||
<span><?= e($line['item_name']) ?> × <?= e((string) $line['quantity']) ?></span>
|
||
<strong><?= e(formatSyp((float) $line['line_total_syp'])) ?></strong>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<p class="invoice-note"><?= e($selectedInvoice['notes'] ?: 'بدون ملاحظات') ?></p>
|
||
</div>
|
||
<?php endif; ?>
|
||
</article>
|
||
</section>
|
||
|
||
<section class="panel-card">
|
||
<div class="card-head inline-head">
|
||
<div>
|
||
<h2>سجل الفواتير</h2>
|
||
<p>آخر 100 فاتورة مع إمكانية تصدير القائمة كاملة كملف PDF.</p>
|
||
</div>
|
||
<button type="button" class="primary-btn secondary-tone" data-export-invoices <?= $invoiceRows ? '' : 'disabled' ?>>تصدير سجل الفواتير PDF</button>
|
||
</div>
|
||
<div class="table-wrap">
|
||
<table data-invoices-table>
|
||
<thead>
|
||
<tr>
|
||
<th>رقم الفاتورة</th>
|
||
<th>العميل</th>
|
||
<th>الإجمالي بالدولار</th>
|
||
<th>الإجمالي بالليرة</th>
|
||
<th>التاريخ</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (!$invoiceRows): ?>
|
||
<tr><td colspan="5" class="empty-cell">لا توجد فواتير حتى الآن.</td></tr>
|
||
<?php else: ?>
|
||
<?php foreach ($invoiceRows as $invoice): ?>
|
||
<tr class="clickable-row" onclick="window.location='index.php?tab=invoices&invoice_id=<?= e((string) $invoice['id']) ?>'">
|
||
<td><?= e($invoice['invoice_number']) ?></td>
|
||
<td><?= e($invoice['customer_name'] ?: 'عميل مباشر') ?></td>
|
||
<td><?= e(formatUsd((float) $invoice['total_usd'])) ?></td>
|
||
<td><?= e(formatSyp((float) $invoice['total_syp'])) ?></td>
|
||
<td><?= e((string) $invoice['created_at']) ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
<?php else: ?>
|
||
<section class="stats-grid">
|
||
<article class="stat-card">
|
||
<span>أنواع المعدات</span>
|
||
<strong><?= e((string) $totalEquipmentTypes) ?></strong>
|
||
</article>
|
||
<article class="stat-card">
|
||
<span>إجمالي المخزون</span>
|
||
<strong><?= e((string) $totalStockUnits) ?> قطعة</strong>
|
||
</article>
|
||
<article class="stat-card">
|
||
<span>فواتير اليوم</span>
|
||
<strong><?= e((string) $todayInvoices) ?></strong>
|
||
</article>
|
||
<article class="stat-card">
|
||
<span>مبيعات اليوم</span>
|
||
<strong><?= e(formatSyp($todaySalesSyp)) ?></strong>
|
||
</article>
|
||
</section>
|
||
|
||
<section class="grid two-col reports-grid">
|
||
<article class="panel-card">
|
||
<div class="card-head">
|
||
<h2>ملخص سريع</h2>
|
||
<p>قراءة مباشرة لوضع المحل اليوم.</p>
|
||
</div>
|
||
<ul class="summary-list">
|
||
<li><span>عدد الفواتير الكلي</span><strong><?= e((string) $totalInvoices) ?></strong></li>
|
||
<li><span>إجمالي المبيعات</span><strong><?= e(formatSyp($totalSalesSyp)) ?></strong></li>
|
||
<li><span>سعر الدولار الحالي</span><strong><?= e(formatSyp($exchangeRate)) ?></strong></li>
|
||
<li><span>آخر تحديث للأسعار</span><strong><?= e(gmdate('Y-m-d H:i')) ?></strong></li>
|
||
</ul>
|
||
</article>
|
||
|
||
<article class="panel-card">
|
||
<div class="card-head">
|
||
<h2>آخر الفواتير</h2>
|
||
<p>أحدث الحركات المالية داخل التطبيق.</p>
|
||
</div>
|
||
<div class="mini-list">
|
||
<?php if (!$invoiceRows): ?>
|
||
<div class="empty-state">لا توجد بيانات بعد.</div>
|
||
<?php else: ?>
|
||
<?php foreach (array_slice($invoiceRows, 0, 5) as $invoice): ?>
|
||
<div class="mini-item">
|
||
<div>
|
||
<strong><?= e($invoice['invoice_number']) ?></strong>
|
||
<p><?= e($invoice['customer_name'] ?: 'عميل مباشر') ?></p>
|
||
</div>
|
||
<span><?= e(formatSyp((float) $invoice['total_syp'])) ?></span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
<?php endif; ?>
|
||
</main>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/jspdf-autotable@3.8.2/dist/jspdf.plugin.autotable.min.js"></script>
|
||
<script src="assets/js/main.js?v=<?= e($assetJsVersion) ?>"></script>
|
||
</body>
|
||
</html>
|