Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
8b3e4e72af Autosave: 20260409-094640 2026-04-09 09:46:40 +00:00
17 changed files with 3941 additions and 512 deletions

340
accounting.php Normal file
View File

@ -0,0 +1,340 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('accounting');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = (string)($_POST['form_action'] ?? '');
if ($action === 'customer_payment') {
$customerId = (int)($_POST['customer_id'] ?? 0);
$amount = (float)($_POST['amount'] ?? 0);
$method = (string)($_POST['payment_method'] ?? 'bank_transfer');
$notes = trim((string)($_POST['notes'] ?? ''));
$customer = fetch_record('customer', $customerId);
if (!$customer) {
$errors[] = 'اختر عميلًا صحيحًا لتسجيل المقبوضات.';
}
if ($amount <= 0) {
$errors[] = 'أدخل مبلغ مقبوضات أكبر من صفر.';
}
if (!array_key_exists($method, payment_method_options())) {
$errors[] = 'طريقة السداد غير صحيحة.';
}
if (!$errors && $customer) {
create_record(
'customer_payment',
'مقبوضات من ' . $customer['title'],
next_code('RCPT', 'customer_payment'),
[
'customer_id' => (int)$customer['id'],
'customer_name' => $customer['title'],
'amount' => $amount,
'payment_method' => $method,
'payment_method_label' => payment_method_label($method),
'notes' => $notes,
'created_date' => date('Y-m-d H:i'),
'created_by' => current_user()['username'] ?? 'system',
],
'posted'
);
set_flash('success', 'تم ترحيل مقبوضات العميل بنجاح.');
redirect('accounting.php');
}
}
if ($action === 'supplier_payment') {
$supplierId = (int)($_POST['supplier_id'] ?? 0);
$amount = (float)($_POST['amount'] ?? 0);
$method = (string)($_POST['payment_method'] ?? 'bank_transfer');
$notes = trim((string)($_POST['notes'] ?? ''));
$supplier = fetch_record('supplier', $supplierId);
if (!$supplier) {
$errors[] = 'اختر موردًا صحيحًا لتسجيل الدفعة.';
}
if ($amount <= 0) {
$errors[] = 'أدخل مبلغ دفعة أكبر من صفر.';
}
if (!array_key_exists($method, payment_method_options())) {
$errors[] = 'طريقة السداد غير صحيحة.';
}
if (!$errors && $supplier) {
create_record(
'supplier_payment',
'دفعة إلى ' . $supplier['title'],
next_code('SPAY', 'supplier_payment'),
[
'supplier_id' => (int)$supplier['id'],
'supplier_name' => $supplier['title'],
'amount' => $amount,
'payment_method' => $method,
'payment_method_label' => payment_method_label($method),
'notes' => $notes,
'created_date' => date('Y-m-d H:i'),
'created_by' => current_user()['username'] ?? 'system',
],
'posted'
);
set_flash('success', 'تم ترحيل دفعة المورد بنجاح.');
redirect('accounting.php');
}
}
if ($action === 'expense_entry') {
$title = trim((string)($_POST['title'] ?? ''));
$category = (string)($_POST['category'] ?? 'operations');
$amount = (float)($_POST['amount'] ?? 0);
$notes = trim((string)($_POST['notes'] ?? ''));
if ($title === '') {
$errors[] = 'أدخل اسم المصروف.';
}
if ($amount <= 0) {
$errors[] = 'أدخل مبلغ مصروف أكبر من صفر.';
}
if (!array_key_exists($category, expense_category_options())) {
$errors[] = 'تصنيف المصروف غير صحيح.';
}
if (!$errors) {
create_record(
'expense_entry',
$title,
next_code('EXP', 'expense_entry'),
[
'category' => $category,
'category_label' => expense_category_label($category),
'amount' => $amount,
'notes' => $notes,
'created_date' => date('Y-m-d H:i'),
'created_by' => current_user()['username'] ?? 'system',
],
'posted'
);
set_flash('success', 'تم ترحيل المصروف بنجاح.');
redirect('accounting.php');
}
}
}
$counts = fetch_counts();
$summary = accounting_summary();
$customerStatements = customer_statement_rows();
$supplierStatements = supplier_statement_rows();
$customerPayments = fetch_records('customer_payment');
$supplierPayments = fetch_records('supplier_payment');
$expenses = fetch_records('expense_entry');
$activity = recent_accounting_activity(10);
$customers = customer_dataset();
$suppliers = supplier_dataset();
render_header('المحاسبة والتقارير المالية', 'إدارة المقبوضات والمدفوعات والمصروفات وكشوف حساب العملاء والموردين مع مؤشرات ربح وتدفق نقدي.', 'accounting');
?>
<section class="hero-panel mb-4">
<div class="row g-4 align-items-center">
<div class="col-lg-8">
<span class="eyebrow">Accounting & Statements</span>
<h1 class="hero-title">محاسبة تشغيلية مرتبطة بالمبيعات والمشتريات.</h1>
<p class="hero-copy">تسحب هذه الصفحة الذمم المدينة من <strong>فواتير المبيعات</strong>، والذمم الدائنة من <strong>أوامر الشراء المستلمة</strong>، ثم تخصم منها المقبوضات والمدفوعات والمصروفات لتكوين صورة مالية فورية.</p>
</div>
<div class="col-lg-4">
<div class="hero-side-card h-100">
<div class="metric-label">الربح المتوقع</div>
<div class="metric-value small-money"><?= e(format_money((float)$summary['expected_profit'])) ?></div>
<div class="small text-secondary mb-2">صافي التدفق النقدي: <?= e(format_money((float)$summary['net_cashflow'])) ?></div>
<div class="small text-secondary">المقبوضات اليوم: <?= e(format_money((float)$summary['today_receipts'])) ?></div>
</div>
</div>
</div>
</section>
<section class="row g-3 mb-4">
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">فواتير المبيعات</div><div class="stat-value"><?= e(format_money((float)$summary['sales_invoices'])) ?></div><div class="stat-note"><?= e((string)($counts['sales_invoice'] ?? 0)) ?> فاتورة</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">مشتريات مستلمة</div><div class="stat-value"><?= e(format_money((float)$summary['purchase_commitments'])) ?></div><div class="stat-note">مرتبطة بالموردين</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">مقبوضات العملاء</div><div class="stat-value"><?= e(format_money((float)$summary['customer_receipts'])) ?></div><div class="stat-note"><?= e((string)($counts['customer_payment'] ?? 0)) ?> حركة</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">مدفوعات الموردين</div><div class="stat-value"><?= e(format_money((float)$summary['supplier_payments'])) ?></div><div class="stat-note"><?= e((string)($counts['supplier_payment'] ?? 0)) ?> حركة</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">المصروفات</div><div class="stat-value"><?= e(format_money((float)$summary['expenses'])) ?></div><div class="stat-note"><?= e((string)($counts['expense_entry'] ?? 0)) ?> قيد</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">ذمم العملاء</div><div class="stat-value"><?= e(format_money((float)$summary['receivables'])) ?></div><div class="stat-note">قابلة للتحصيل</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">ذمم الموردين</div><div class="stat-value"><?= e(format_money((float)$summary['payables'])) ?></div><div class="stat-note">واجب سدادها</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">صافي التدفق النقدي</div><div class="stat-value"><?= e(format_money((float)$summary['net_cashflow'])) ?></div><div class="stat-note">مقبوضات - مدفوعات - مصروفات</div></div></div>
</section>
<?php if ($errors): ?>
<div class="alert alert-danger py-2 mb-4"><?php foreach ($errors as $error): ?><div><?= e($error) ?></div><?php endforeach; ?></div>
<?php endif; ?>
<section class="row g-4 mb-4">
<div class="col-xl-4">
<div class="panel-card h-100">
<div class="section-header compact"><div><h2 class="section-title">تسجيل مقبوضات عميل</h2><p class="section-copy">تُخصم تلقائيًا من رصيد العميل المستحق.</p></div></div>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="form_action" value="customer_payment">
<div>
<label class="form-label">العميل</label>
<select class="form-select" name="customer_id" required>
<option value="">اختر العميل</option>
<?php foreach ($customers as $customer): ?>
<option value="<?= (int)$customer['id'] ?>"><?= e($customer['name']) ?> — <?= e($customer['code']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row g-3">
<div class="col-md-6"><label class="form-label">المبلغ</label><input type="number" min="0.01" step="0.01" class="form-control" name="amount" required></div>
<div class="col-md-6"><label class="form-label">طريقة السداد</label><select class="form-select" name="payment_method"><?php foreach (payment_method_options() as $key => $label): ?><option value="<?= e($key) ?>"><?= e($label) ?></option><?php endforeach; ?></select></div>
</div>
<div><label class="form-label">ملاحظات</label><textarea class="form-control" name="notes" rows="3"></textarea></div>
<button class="btn btn-dark" type="submit">ترحيل المقبوضات</button>
</form>
</div>
</div>
<div class="col-xl-4">
<div class="panel-card h-100">
<div class="section-header compact"><div><h2 class="section-title">تسجيل دفعة مورد</h2><p class="section-copy">تُخصم من الذمم الدائنة على المورد مباشرة.</p></div></div>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="form_action" value="supplier_payment">
<div>
<label class="form-label">المورد</label>
<select class="form-select" name="supplier_id" required>
<option value="">اختر المورد</option>
<?php foreach ($suppliers as $supplier): ?>
<option value="<?= (int)$supplier['id'] ?>"><?= e($supplier['name']) ?> — <?= e($supplier['code']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row g-3">
<div class="col-md-6"><label class="form-label">المبلغ</label><input type="number" min="0.01" step="0.01" class="form-control" name="amount" required></div>
<div class="col-md-6"><label class="form-label">طريقة السداد</label><select class="form-select" name="payment_method"><?php foreach (payment_method_options() as $key => $label): ?><option value="<?= e($key) ?>"><?= e($label) ?></option><?php endforeach; ?></select></div>
</div>
<div><label class="form-label">ملاحظات</label><textarea class="form-control" name="notes" rows="3"></textarea></div>
<button class="btn btn-dark" type="submit">ترحيل الدفعة</button>
</form>
</div>
</div>
<div class="col-xl-4">
<div class="panel-card h-100">
<div class="section-header compact"><div><h2 class="section-title">إضافة مصروف</h2><p class="section-copy">يؤثر مباشرة على الربح المتوقع والتدفق النقدي.</p></div></div>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="form_action" value="expense_entry">
<div><label class="form-label">اسم المصروف</label><input type="text" class="form-control" name="title" required></div>
<div class="row g-3">
<div class="col-md-6"><label class="form-label">التصنيف</label><select class="form-select" name="category"><?php foreach (expense_category_options() as $key => $label): ?><option value="<?= e($key) ?>"><?= e($label) ?></option><?php endforeach; ?></select></div>
<div class="col-md-6"><label class="form-label">المبلغ</label><input type="number" min="0.01" step="0.01" class="form-control" name="amount" required></div>
</div>
<div><label class="form-label">ملاحظات</label><textarea class="form-control" name="notes" rows="3"></textarea></div>
<button class="btn btn-dark" type="submit">ترحيل المصروف</button>
</form>
</div>
</div>
</section>
<section class="row g-4 mb-4">
<div class="col-lg-6">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">كشف حساب العملاء</h2><p class="section-copy">الرصيد = فواتير المبيعات - المقبوضات المسجلة.</p></div></div>
<?php if ($customerStatements): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>العميل</th><th>الفواتير</th><th>المقبوضات</th><th>الرصيد</th></tr></thead>
<tbody>
<?php foreach ($customerStatements as $row): ?>
<tr>
<td><?= e($row['name']) ?><div class="small text-secondary"><?= e($row['code']) ?></div></td>
<td><?= e(format_money((float)$row['invoices'])) ?></td>
<td><?= e(format_money((float)$row['payments'])) ?></td>
<td><strong><?= e(format_money((float)$row['balance'])) ?></strong></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد بيانات عملاء لعرض كشف الحساب.</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-6">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">كشف حساب الموردين</h2><p class="section-copy">الرصيد = مشتريات مستلمة - مدفوعات الموردين.</p></div></div>
<?php if ($supplierStatements): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>المورد</th><th>المشتريات</th><th>المدفوعات</th><th>الرصيد</th></tr></thead>
<tbody>
<?php foreach ($supplierStatements as $row): ?>
<tr>
<td><?= e($row['name']) ?><div class="small text-secondary"><?= e($row['code']) ?></div></td>
<td><?= e(format_money((float)$row['purchases'])) ?></td>
<td><?= e(format_money((float)$row['payments'])) ?></td>
<td><strong><?= e(format_money((float)$row['balance'])) ?></strong></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد بيانات موردين لعرض كشف الحساب.</div>
<?php endif; ?>
</div>
</div>
</section>
<section class="row g-4 mb-4">
<div class="col-lg-5">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">آخر نشاط محاسبي</h2><p class="section-copy">يشمل المقبوضات والمدفوعات والمصروفات في سجل سريع.</p></div></div>
<?php if ($activity): ?>
<div class="vstack gap-2">
<?php foreach ($activity as $row): $payload = $row['payload_data']; ?>
<div class="list-row">
<div>
<strong><?= e($row['title']) ?></strong>
<div class="small text-secondary"><?= e($row['code']) ?> — <?= e(substr((string)($row['created_at'] ?? ''), 0, 16)) ?></div>
</div>
<div class="text-start">
<span class="badge <?= e(status_badge_class((string)$row['status'])) ?> mb-1"><?= e(order_status_label((string)$row['status'])) ?></span>
<div class="small"><?= e(format_money((float)($payload['amount'] ?? 0))) ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد قيود محاسبية مرحّلة بعد.</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-7">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">ملخص القيود المرحّلة</h2><p class="section-copy">آخر الحركات المالية المسجلة يدويًا داخل النظام.</p></div></div>
<?php if ($customerPayments || $supplierPayments || $expenses): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>النوع</th><th>المرجع</th><th>الطرف</th><th>التفصيل</th><th>المبلغ</th></tr></thead>
<tbody>
<?php foreach ($activity as $row): $payload = $row['payload_data']; ?>
<tr>
<td><?= e(match ((string)$row['record_type']) { 'customer_payment' => 'مقبوضات', 'supplier_payment' => 'مدفوعات', default => 'مصروف' }) ?></td>
<td><?= e($row['code']) ?></td>
<td><?= e($payload['customer_name'] ?? $payload['supplier_name'] ?? 'مصروف داخلي') ?></td>
<td><?= e($payload['payment_method_label'] ?? $payload['category_label'] ?? '') ?><?php if (!empty($payload['notes'])): ?><div class="small text-secondary"><?= e($payload['notes']) ?></div><?php endif; ?></td>
<td><?= e(format_money((float)($payload['amount'] ?? 0))) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لم يتم تسجيل مقبوضات أو مدفوعات أو مصروفات بعد.</div>
<?php endif; ?>
</div>
</div>
</section>
<?php render_footer(); ?>

1074
app.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,403 +1,459 @@
:root {
--app-bg: #f5f6f8;
--app-surface: #ffffff;
--app-surface-soft: #fafafb;
--app-border: #d9dde3;
--app-border-strong: #b9c0cb;
--app-text: #111827;
--app-muted: #6b7280;
--app-accent: #1f2937;
--app-accent-soft: #eef1f4;
--app-success: #0f766e;
--app-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
}
html, body {
background: var(--app-bg);
color: var(--app-text);
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Tahoma, sans-serif;
}
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;
min-height: 100vh;
}
.main-wrapper {
display: flex;
.page-shell {
min-height: calc(100vh - 72px);
}
.app-navbar {
backdrop-filter: blur(12px);
box-shadow: 0 1px 0 rgba(17, 24, 39, 0.04);
}
.navbar-brand {
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;
gap: 10px;
letter-spacing: -0.02em;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
.brand-dot {
width: 10px;
height: 10px;
border-radius: 2px;
background: var(--app-accent);
display: inline-block;
}
.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);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-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;
font-weight: 600;
transition: all 0.3s ease;
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
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);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@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;
.nav-link {
color: #374151;
border-radius: 8px;
transition: all 0.3s ease;
padding-inline: 12px !important;
padding-block: 8px !important;
}
.header-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
.nav-link.active,
.nav-link:hover {
background: var(--app-accent-soft);
color: var(--app-text);
}
/* 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;
.hero-panel,
.panel-card,
.stat-card,
.hero-side-card {
background: var(--app-surface);
border: 1px solid var(--app-border);
border-radius: var(--radius-lg);
box-shadow: var(--app-shadow);
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
.hero-panel {
padding: 28px;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
.hero-title {
font-size: clamp(2rem, 4vw, 3.25rem);
line-height: 1.04;
margin: 12px 0 14px;
letter-spacing: -0.04em;
max-width: 10ch;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
.hero-copy,
.section-copy,
.empty-state p,
.empty-inline,
.workflow-list,
.form-text,
.text-secondary {
color: var(--app-muted) !important;
}
.eyebrow,
.print-eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
letter-spacing: 0.08em;
color: #4b5563;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
.hero-side-card {
padding: 24px;
background: #fcfcfd;
}
.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-label,
.stat-label,
.detail-label {
font-size: 12px;
color: var(--app-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
.metric-value,
.stat-value {
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.03em;
margin: 10px 0 6px;
}
.small-money {
font-size: 1.45rem;
}
.stat-card {
padding: 20px;
height: 100%;
}
.stat-card-soft {
background: #f8fafb;
}
.stat-link,
.table-link {
color: var(--app-text);
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
}
.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;
.stat-note {
margin-top: 8px;
color: var(--app-muted);
font-size: 13px;
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
.panel-card {
padding: 22px;
}
.header-container {
.section-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 18px;
}
.header-links {
.section-header.compact {
margin-bottom: 14px;
}
.section-title {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.app-table {
--bs-table-bg: transparent;
margin-bottom: 0;
}
.app-table th {
font-size: 12px;
color: var(--app-muted);
font-weight: 600;
border-bottom-color: var(--app-border);
}
.app-table td {
border-bottom-color: #ebedf0;
vertical-align: middle;
}
.empty-state,
.empty-inline {
border: 1px dashed var(--app-border-strong);
background: var(--app-surface-soft);
border-radius: var(--radius-md);
padding: 24px;
}
.empty-inline {
padding: 18px;
}
.workflow-list {
margin: 0;
padding-right: 18px;
display: grid;
gap: 12px;
}
.list-row,
.summary-row {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.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);
.list-row {
padding: 12px 0;
border-bottom: 1px solid #edf0f2;
}
.admin-card h3 {
margin-top: 0;
margin-bottom: 1.5rem;
.list-row:last-child {
border-bottom: 0;
}
.form-control,
.form-select {
border-color: var(--app-border);
border-radius: 10px;
min-height: 44px;
padding-inline: 14px;
box-shadow: none !important;
}
textarea.form-control {
min-height: auto;
}
.form-control:focus,
.form-select:focus,
.btn:focus {
border-color: #6b7280;
box-shadow: 0 0 0 0.2rem rgba(107, 114, 128, 0.12) !important;
}
.btn {
border-radius: 10px;
min-height: 42px;
padding-inline: 16px;
font-weight: 600;
}
.btn-dark {
background: var(--app-accent);
border-color: var(--app-accent);
}
.btn-outline-secondary {
border-color: var(--app-border-strong);
color: #374151;
}
.sales-summary-box,
.subtle-card {
border: 1px solid var(--app-border);
background: #fbfcfd;
border-radius: var(--radius-md);
padding: 16px;
}
.summary-row {
padding: 8px 0;
font-size: 14px;
border-bottom: 1px solid #edf0f2;
}
.summary-row:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.summary-row.grand {
font-size: 15px;
font-weight: 700;
}
.btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 14px;
}
.btn-add {
background: #212529;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
.detail-block {
padding: 14px;
background: #fbfcfd;
border: 1px solid var(--app-border);
border-radius: var(--radius-md);
display: flex;
flex-direction: column;
gap: 6px;
}
.btn-save {
background: #0088cc;
color: white;
border: none;
padding: 0.8rem 1.5rem;
.app-alert {
border-radius: 12px;
cursor: pointer;
font-weight: 600;
width: 100%;
transition: all 0.3s ease;
border-color: transparent;
}
.webhook-url {
font-size: 0.85em;
color: #555;
margin-top: 0.5rem;
.signature-box {
border-top: 1px solid var(--app-border-strong);
padding-top: 10px;
min-height: 56px;
color: var(--app-muted);
}
.history-table-container {
overflow-x: auto;
background: rgba(255, 255, 255, 0.4);
padding: 1rem;
.print-body {
background: #eef1f4;
}
.print-sheet {
max-width: 960px;
background: #fff;
border: 1px solid var(--app-border);
border-radius: 18px;
padding: 32px;
box-shadow: var(--app-shadow);
}
@media (max-width: 991.98px) {
.hero-panel,
.panel-card,
.print-sheet {
padding: 20px;
}
.hero-title {
max-width: 100%;
}
}
@media print {
body,
.print-body {
background: #fff;
}
.print-sheet {
box-shadow: none;
border: 0;
border-radius: 0;
padding: 0;
max-width: none;
}
}
.auth-card {
max-width: 100%;
}
.user-chip {
display: flex;
flex-direction: column;
align-items: flex-end;
padding: 8px 12px;
border: 1px solid var(--app-border);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
background: #fafbfc;
line-height: 1.2;
}
.history-table {
width: 100%;
.user-chip span {
color: var(--app-muted);
font-size: 12px;
}
.history-table-time {
width: 15%;
white-space: nowrap;
font-size: 0.85em;
color: #555;
.quick-links {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.history-table-user {
width: 35%;
background: rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 8px;
.quick-link-card {
text-decoration: none;
color: var(--app-text);
border: 1px solid var(--app-border);
background: #fff;
border-radius: 14px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 4px;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.history-table-ai {
width: 50%;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
padding: 8px;
.quick-link-card:hover {
transform: translateY(-2px);
box-shadow: var(--app-shadow);
}
.no-messages {
text-align: center;
color: #777;
.quick-link-card span {
color: var(--app-muted);
font-size: 13px;
}
.app-progress {
height: 10px;
border-radius: 999px;
background: #edf0f4;
}
.app-progress .progress-bar {
background: var(--app-success);
border-radius: 999px;
}
.reports-callout {
background: linear-gradient(135deg, #ffffff 0%, #f5f7fb 100%);
}
.report-filter-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.kpi-card {
border: 1px solid var(--app-border);
border-radius: var(--radius-md);
background: #fbfcfd;
padding: 18px;
height: 100%;
}
.kpi-label {
color: var(--app-muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.kpi-value {
font-size: 1.6rem;
font-weight: 700;
margin-top: 10px;
letter-spacing: -0.03em;
}
.kpi-note {
margin-top: 8px;
color: var(--app-muted);
font-size: 13px;
}

View File

@ -1,39 +1,112 @@
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
const toastEls = document.querySelectorAll('.toast');
toastEls.forEach((el) => new bootstrap.Toast(el).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;
if (!window.erpData) {
return;
}
const customerSelect = document.getElementById('customerSelect');
const branchSelect = document.getElementById('branchSelect');
const productSelect = document.getElementById('productSelect');
const qtyInput = document.getElementById('qtyInput');
const unitPricePreview = document.getElementById('unitPricePreview');
const stockPreview = document.getElementById('stockPreview');
const subtotalPreview = document.getElementById('subtotalPreview');
const vatPreview = document.getElementById('vatPreview');
const grandPreview = document.getElementById('grandPreview');
const allowedHint = document.getElementById('allowedHint');
if (!customerSelect || !branchSelect || !productSelect || !qtyInput) {
return;
}
const customers = window.erpData.customers || [];
const products = window.erpData.products || [];
const currency = (value) => `${Number(value).toFixed(2)} ر.س`;
const getCustomer = () => customers.find((item) => String(item.id) === customerSelect.value);
const getProduct = () => products.find((item) => String(item.id) === productSelect.value);
const renderBranches = () => {
const customer = getCustomer();
branchSelect.innerHTML = '<option value="">اختر الفرع</option>';
if (!customer) {
branchSelect.innerHTML = '<option value="">اختر الفرع بعد العميل</option>';
return;
}
(customer.branches || []).forEach((branch, index) => {
const option = document.createElement('option');
option.value = branch;
option.textContent = branch;
if (index === 0) option.selected = true;
branchSelect.appendChild(option);
});
};
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
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 filterProducts = () => {
const customer = getCustomer();
const allowedSkus = customer?.allowed_skus || [];
Array.from(productSelect.options).forEach((option) => {
if (!option.value) {
option.hidden = false;
return;
}
const product = products.find((item) => String(item.id) === option.value);
if (!product) {
option.hidden = false;
return;
}
const allowed = allowedSkus.length === 0 || allowedSkus.includes(product.sku);
option.hidden = !allowed;
});
const selectedProduct = getProduct();
if (selectedProduct && selectedProduct.sku && allowedSkus.length && !allowedSkus.includes(selectedProduct.sku)) {
productSelect.value = '';
}
allowedHint.textContent = allowedSkus.length
? `الأصناف المسموح بها: ${allowedSkus.join('، ')}`
: 'هذا العميل لا يملك تقييدًا على الأصناف.';
};
const refreshSummary = () => {
const customer = getCustomer();
const product = getProduct();
const qty = Number(qtyInput.value || 0);
if (!product) {
unitPricePreview.value = currency(0);
stockPreview.textContent = '—';
subtotalPreview.textContent = currency(0);
vatPreview.textContent = currency(0);
grandPreview.textContent = currency(0);
return;
}
const overrides = customer?.price_overrides || {};
const unitPrice = overrides[product.sku] !== undefined ? Number(overrides[product.sku]) : Number(product.sale_price || 0);
const subtotal = qty * unitPrice;
const vat = subtotal * 0.15;
const grand = subtotal + vat;
unitPricePreview.value = currency(unitPrice);
stockPreview.textContent = `${product.stock_qty} ${product.unit}`;
subtotalPreview.textContent = currency(subtotal);
vatPreview.textContent = currency(vat);
grandPreview.textContent = currency(grand);
};
customerSelect.addEventListener('change', () => {
renderBranches();
filterProducts();
refreshSummary();
});
productSelect.addEventListener('change', refreshSummary);
qtyInput.addEventListener('input', refreshSummary);
renderBranches();
filterProducts();
refreshSummary();
});

205
customers.php Normal file
View File

@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('customers');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$name = trim((string)($_POST['name'] ?? ''));
$phone = trim((string)($_POST['phone'] ?? ''));
$email = trim((string)($_POST['email'] ?? ''));
$branchesRaw = trim((string)($_POST['branches'] ?? ''));
$allowedRaw = trim((string)($_POST['allowed_skus'] ?? ''));
$pricingRaw = trim((string)($_POST['price_overrides'] ?? ''));
$notes = trim((string)($_POST['notes'] ?? ''));
if ($name === '') {
$errors[] = 'اسم العميل مطلوب.';
}
$branches = array_values(array_filter(array_map('trim', explode(',', $branchesRaw))));
if (!$branches) {
$errors[] = 'أدخل فرعًا واحدًا على الأقل.';
}
$allowedSkus = array_values(array_filter(array_map('trim', explode(',', $allowedRaw))));
$priceOverrides = [];
if ($pricingRaw !== '') {
foreach (preg_split('/\r\n|\r|\n/', $pricingRaw) as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
[$sku, $price] = array_pad(array_map('trim', explode(':', $line, 2)), 2, '');
if ($sku !== '' && is_numeric($price)) {
$priceOverrides[$sku] = (float)$price;
}
}
}
if (!$errors) {
create_record('customer', $name, next_code('CUS', 'customer'), [
'phone' => $phone,
'email' => $email,
'branches' => $branches,
'allowed_skus' => $allowedSkus,
'price_overrides' => $priceOverrides,
'notes' => $notes,
]);
set_flash('success', 'تمت إضافة العميل بنجاح.');
redirect('customers.php');
}
}
$customers = fetch_records('customer');
$detail = isset($_GET['id']) ? fetch_record('customer', (int)$_GET['id']) : null;
render_header('إدارة العملاء', 'تعريف العملاء وفروعهم وأسعارهم الخاصة والأصناف المسموح بها.', 'customers');
?>
<div class="row g-4">
<div class="col-lg-5">
<div class="panel-card">
<div class="section-header compact">
<div>
<h1 class="section-title mb-1">إضافة عميل</h1>
<p class="section-copy">عرّف الفروع والأسعار الخاصة لتظهر تلقائيًا عند إنشاء أمر البيع.</p>
</div>
</div>
<?php if ($errors): ?>
<div class="alert alert-danger py-2">
<?php foreach ($errors as $error): ?>
<div><?= e($error) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<div>
<label class="form-label">اسم العميل</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">الهاتف</label>
<input type="text" class="form-control" name="phone">
</div>
<div class="col-md-6">
<label class="form-label">البريد الإلكتروني</label>
<input type="email" class="form-control" name="email">
</div>
</div>
<div>
<label class="form-label">الفروع</label>
<input type="text" class="form-control" name="branches" placeholder="الرياض, جدة" required>
<div class="form-text">افصل الفروع بفاصلة.</div>
</div>
<div>
<label class="form-label">الأصناف المسموح بها</label>
<input type="text" class="form-control" name="allowed_skus" placeholder="P-100, P-200">
</div>
<div>
<label class="form-label">الأسعار الخاصة</label>
<textarea class="form-control" name="price_overrides" rows="4" placeholder="P-100:95&#10;P-200:145"></textarea>
<div class="form-text">كل سطر بصيغة SKU:PRICE.</div>
</div>
<div>
<label class="form-label">ملاحظات</label>
<textarea class="form-control" name="notes" rows="3"></textarea>
</div>
<button class="btn btn-dark" type="submit">حفظ العميل</button>
</form>
</div>
</div>
<div class="col-lg-7">
<div class="panel-card mb-4">
<div class="section-header">
<div>
<h2 class="section-title">سجل العملاء</h2>
<p class="section-copy">اختر عميلًا لعرض الفروع والأسعار الخاصة والأصناف المتاحة له.</p>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead>
<tr>
<th>العميل</th>
<th>الفروع</th>
<th>الأصناف</th>
<th>تفاصيل</th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $customer): $payload = $customer['payload_data']; ?>
<tr>
<td>
<div class="fw-semibold"><?= e($customer['title']) ?></div>
<div class="small text-secondary"><?= e($customer['code']) ?></div>
</td>
<td><?= e(implode('، ', $payload['branches'] ?? [])) ?></td>
<td><?= e(implode('، ', $payload['allowed_skus'] ?? [])) ?></td>
<td><a class="btn btn-sm btn-outline-secondary" href="customers.php?id=<?= (int)$customer['id'] ?>">عرض</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="panel-card">
<div class="section-header compact">
<div>
<h2 class="section-title">تفاصيل العميل</h2>
</div>
</div>
<?php if ($detail): $payload = $detail['payload_data']; ?>
<div class="detail-grid">
<div class="detail-block">
<span class="detail-label">اسم العميل</span>
<strong><?= e($detail['title']) ?></strong>
</div>
<div class="detail-block">
<span class="detail-label">رقم العميل</span>
<strong><?= e($detail['code']) ?></strong>
</div>
<div class="detail-block">
<span class="detail-label">بيانات التواصل</span>
<strong><?= e(($payload['phone'] ?? '—') . ' / ' . ($payload['email'] ?? '—')) ?></strong>
</div>
<div class="detail-block">
<span class="detail-label">الفروع</span>
<strong><?= e(implode('، ', $payload['branches'] ?? [])) ?></strong>
</div>
</div>
<hr>
<div class="row g-3">
<div class="col-md-6">
<div class="subtle-card h-100">
<div class="detail-label mb-2">الأصناف المسموح بها</div>
<div><?= e(implode('، ', $payload['allowed_skus'] ?? [])) ?: '—' ?></div>
</div>
</div>
<div class="col-md-6">
<div class="subtle-card h-100">
<div class="detail-label mb-2">الأسعار الخاصة</div>
<?php if (!empty($payload['price_overrides'])): ?>
<?php foreach ($payload['price_overrides'] as $sku => $price): ?>
<div class="d-flex justify-content-between border-bottom py-1 small">
<span><?= e((string)$sku) ?></span>
<span><?= e(format_money((float)$price)) ?></span>
</div>
<?php endforeach; ?>
<?php else: ?>
<div>لا توجد أسعار خاصة.</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="mt-3 text-secondary small"><?= e($payload['notes'] ?? '') ?></div>
<?php else: ?>
<div class="empty-inline">اختر عميلًا من الجدول لعرض التفاصيل.</div>
<?php endif; ?>
</div>
</div>
</div>
<?php render_footer(); ?>

20
healthz.php Normal file
View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
header('Content-Type: application/json; charset=utf-8');
try {
db()->query('SELECT 1');
echo json_encode([
'status' => 'ok',
'app' => app_name(),
'time' => date('c'),
'db' => 'connected',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
'status' => 'error',
'time' => date('c'),
'db' => 'failed',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}

387
index.php
View File

@ -1,150 +1,247 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/app.php';
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
if (!is_logged_in()) {
render_header('بوابة ERP', 'نظام ERP ويب على PHP + MySQL مع تسجيل دخول وصلاحيات ومخزون ومشتريات ومبيعات وتصنيع ومحاسبة.', 'dashboard');
?>
<section class="hero-panel mb-4 mb-lg-5">
<div class="row g-4 align-items-center">
<div class="col-lg-8">
<span class="eyebrow">ERP Access Portal</span>
<h1 class="hero-title">نظام ERP ويب جاهز كبداية تشغيلية متكاملة.</h1>
<p class="hero-copy">تم تجهيز نواة تشغيلية تشمل تسجيل الدخول، الصلاحيات، العملاء، الموردين، الأصناف، المشتريات، المبيعات، التصنيع، المحاسبة، وسجل حركات المخزون. ادخل بحساب مناسب لفتح لوحة التحكم.</p>
<div class="d-flex flex-wrap gap-2 mt-4">
<a href="login.php" class="btn btn-dark px-4">تسجيل الدخول</a>
<a href="healthz.php" class="btn btn-outline-secondary px-4">Health API</a>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>
<div class="col-lg-4">
<div class="hero-side-card h-100">
<div class="metric-label">حسابات تجريبية</div>
<?php foreach (default_account_credentials() as $cred): ?>
<div class="small mb-2"><strong><?= e($cred['username']) ?></strong> / <?= e($cred['password']) ?></div>
<?php endforeach; ?>
<p class="text-secondary small mb-0">هذه الحسابات لأغراض البداية فقط.</p>
</div>
</div>
</div>
</section>
<?php
render_footer();
exit;
}
require_permission('dashboard');
$counts = fetch_counts();
$inventory = inventory_totals();
$financial = accounting_summary();
$salesToday = today_record_count('sales_order');
$purchasesToday = today_record_count('purchase_order');
$manufacturingToday = today_record_count('manufacturing_order');
$latestSales = recent_records('sales_order', 5);
$latestPurchases = recent_records('purchase_order', 5);
$latestManufacturing = recent_records('manufacturing_order', 5);
$movements = recent_records('stock_movement', 6);
$accountingActivity = recent_accounting_activity(6);
$lowStock = low_stock_products(5);
$current = current_user();
$rawPercent = $inventory['all_qty'] > 0 ? round(($inventory['raw_qty'] / $inventory['all_qty']) * 100, 1) : 0;
$finishedPercent = $inventory['all_qty'] > 0 ? round(($inventory['finished_qty'] / $inventory['all_qty']) * 100, 1) : 0;
$totalManufactured = 0.0;
foreach (fetch_records('manufacturing_order') as $order) {
$totalManufactured += (float)($order['payload_data']['produced_qty'] ?? 0);
}
render_header('لوحة ERP المركزية', 'لوحة تشغيل يومية للعملاء والموردين والأصناف والمبيعات والمشتريات والتصنيع والمحاسبة وحركات المخزون.', 'dashboard');
?>
<section class="hero-panel mb-4 mb-lg-5">
<div class="row g-4 align-items-center">
<div class="col-lg-8">
<span class="eyebrow">Phase 5 ERP Foundation</span>
<h1 class="hero-title">تشغيل يومي موحّد مع تقارير فورية لكل أقسام الـ ERP.</h1>
<p class="hero-copy">مرحبًا <?= e($current['full_name'] ?? '') ?> — هذه النسخة تدعم تسجيل الدخول والصلاحيات، الموردين، أوامر الشراء، مسار المبيعات الكامل، أوامر التصنيع، المحاسبة التشغيلية، وسجل حركات المخزون، مع صفحة تقارير جديدة لتجميع الأداء حسب الفترة.</p>
<div class="quick-links mt-4">
<?php foreach (nav_items() as $item): ?>
<?php if (can_access($item['area'])): ?>
<a href="<?= e($item['href']) ?>" class="quick-link-card">
<strong><?= e($item['label']) ?></strong>
<span>فتح القسم</span>
</a>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<div class="col-lg-4">
<div class="hero-side-card h-100">
<div class="metric-label">الدور الحالي</div>
<div class="metric-value"><?= e(role_label((string)($current['role'] ?? ''))) ?></div>
<p class="text-secondary mb-3">إجمالي السجلات التشغيلية: <?= e((string)(array_sum($counts))) ?></p>
<div class="small text-secondary">آخر تحديث: <?= e(date('Y-m-d H:i')) ?> UTC</div>
</div>
</div>
</div>
</section>
<section class="row g-3 mb-4">
<div class="col-12">
<div class="panel-card reports-callout">
<div class="section-header mb-0">
<div>
<h2 class="section-title">التقارير التنفيذية</h2>
<p class="section-copy mb-0">افتح صفحة التقارير لفلترة النتائج حسب التاريخ ومراجعة المبيعات والمشتريات والتصنيع والمحاسبة من شاشة واحدة.</p>
</div>
<?php if (can_access('reports')): ?>
<a href="reports.php" class="btn btn-dark">فتح التقارير</a>
<?php endif; ?>
</div>
</div>
</div>
</section>
<section class="row g-3 mb-4 mb-lg-5">
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">العملاء</div><div class="stat-value"><?= e((string)$counts['customer']) ?></div><div class="stat-note">سجل العملاء</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">الموردون</div><div class="stat-value"><?= e((string)$counts['supplier']) ?></div><div class="stat-note">ربط المشتريات</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">الأصناف</div><div class="stat-value"><?= e((string)$counts['product']) ?></div><div class="stat-note">مواد خام ونهائية</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">أوامر التصنيع</div><div class="stat-value"><?= e((string)($counts['manufacturing_order'] ?? 0)) ?></div><div class="stat-note"><?= e((string)$manufacturingToday) ?> اليوم</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">مبيعات اليوم</div><div class="stat-value"><?= e((string)$salesToday) ?></div><div class="stat-note"><?= e(format_money(today_sales_value())) ?></div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">مشتريات اليوم</div><div class="stat-value"><?= e((string)$purchasesToday) ?></div><div class="stat-note"><?= e(format_money(today_purchase_value())) ?></div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">المقبوضات</div><div class="stat-value"><?= e(format_money((float)$financial['customer_receipts'])) ?></div><div class="stat-note">اليوم <?= e(format_money((float)$financial['today_receipts'])) ?></div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">المصروفات</div><div class="stat-value"><?= e(format_money((float)$financial['expenses'])) ?></div><div class="stat-note">اليوم <?= e(format_money((float)$financial['today_expenses'])) ?></div></div></div>
</section>
<section class="row g-4 mb-4 mb-lg-5">
<div class="col-lg-7">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">الصورة التشغيلية والمالية</h2><p class="section-copy">مخزون، ذمم، ربح متوقع، وتدفق نقدي في ملخص واحد.</p></div></div>
<div class="row g-3">
<div class="col-md-6"><div class="stat-card stat-card-soft"><div class="stat-label">إجمالي المخزون</div><div class="stat-value"><?= e((string)$inventory['all_qty']) ?></div><div class="stat-note"><?= e((string)$inventory['sku_count']) ?> صنف نشط</div></div></div>
<div class="col-md-6"><div class="stat-card stat-card-soft"><div class="stat-label">إجمالي الإنتاج المرحّل</div><div class="stat-value"><?= e((string)$totalManufactured) ?></div><div class="stat-note">ناتج أوامر التصنيع المكتملة</div></div></div>
<div class="col-md-6"><div class="stat-card stat-card-soft"><div class="stat-label">ذمم العملاء</div><div class="stat-value"><?= e(format_money((float)$financial['receivables'])) ?></div><div class="stat-note">من فواتير المبيعات</div></div></div>
<div class="col-md-6"><div class="stat-card stat-card-soft"><div class="stat-label">ذمم الموردين</div><div class="stat-value"><?= e(format_money((float)$financial['payables'])) ?></div><div class="stat-note">من المشتريات المستلمة</div></div></div>
<div class="col-md-6"><div class="stat-card stat-card-soft"><div class="stat-label">الربح المتوقع</div><div class="stat-value"><?= e(format_money((float)$financial['expected_profit'])) ?></div><div class="stat-note">فواتير - مشتريات - مصروفات</div></div></div>
<div class="col-md-6"><div class="stat-card stat-card-soft"><div class="stat-label">صافي التدفق النقدي</div><div class="stat-value"><?= e(format_money((float)$financial['net_cashflow'])) ?></div><div class="stat-note">المقبوضات - المدفوعات - المصروفات</div></div></div>
</div>
<div class="mt-4">
<div class="summary-row mb-2"><span>الخامات (<?= e((string)$rawPercent) ?>%)</span><strong><?= e((string)$inventory['raw_qty']) ?></strong></div>
<div class="progress app-progress mb-3"><div class="progress-bar bg-secondary" style="width: <?= e((string)$rawPercent) ?>%"></div></div>
<div class="summary-row mb-2"><span>المنتجات النهائية (<?= e((string)$finishedPercent) ?>%)</span><strong><?= e((string)$inventory['finished_qty']) ?></strong></div>
<div class="progress app-progress"><div class="progress-bar bg-dark" style="width: <?= e((string)$finishedPercent) ?>%"></div></div>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="panel-card h-100">
<div class="section-header compact"><div><h2 class="section-title">تنبيهات المخزون المنخفض</h2><p class="section-copy">الأصناف الأقرب لإعادة الطلب.</p></div></div>
<?php if ($lowStock): ?>
<div class="vstack gap-2">
<?php foreach ($lowStock as $product): $payload = $product['payload_data']; ?>
<div class="subtle-card">
<div class="d-flex justify-content-between"><strong><?= e($product['title']) ?></strong><span><?= e($payload['sku'] ?? $product['code']) ?></span></div>
<div class="small text-secondary mt-1">المتوفر: <?= e((string)($payload['stock_qty'] ?? 0)) ?> — حد الطلب: <?= e((string)($payload['reorder_level'] ?? 0)) ?></div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد أصناف منخفضة حاليًا.</div>
<?php endif; ?>
</div>
</div>
</section>
<section class="row g-4 mb-4 mb-lg-5">
<div class="col-lg-6">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">آخر أوامر البيع</h2><p class="section-copy">أحدث عمليات البيع المؤكدة.</p></div></div>
<?php if ($latestSales): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>الرقم</th><th>العميل</th><th>الصنف</th><th>الإجمالي</th></tr></thead>
<tbody>
<?php foreach ($latestSales as $order): $payload = $order['payload_data']; ?>
<tr><td><?= e($order['code']) ?></td><td><?= e($payload['customer_name'] ?? '') ?></td><td><?= e($payload['product_name'] ?? '') ?></td><td><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></td></tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد أوامر بيع بعد.</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-6">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">آخر أوامر الشراء</h2><p class="section-copy">أحدث الأوامر الواردة من الموردين.</p></div></div>
<?php if ($latestPurchases): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>الرقم</th><th>المورد</th><th>الصنف</th><th>الإجمالي</th></tr></thead>
<tbody>
<?php foreach ($latestPurchases as $order): $payload = $order['payload_data']; ?>
<tr><td><?= e($order['code']) ?></td><td><?= e($payload['supplier_name'] ?? '') ?></td><td><?= e($payload['product_name'] ?? '') ?></td><td><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></td></tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد أوامر شراء بعد.</div>
<?php endif; ?>
</div>
</div>
</section>
<section class="row g-4">
<div class="col-lg-4">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">آخر أوامر التصنيع</h2><p class="section-copy">ملخص العمليات الإنتاجية المرحّلة.</p></div></div>
<?php if ($latestManufacturing): ?>
<div class="vstack gap-2">
<?php foreach ($latestManufacturing as $order): $payload = $order['payload_data']; ?>
<div class="list-row">
<div><strong><?= e($order['code']) ?></strong><div class="small text-secondary"><?= e($payload['finished_product_name'] ?? '') ?></div></div>
<div class="text-start small"><?= e((string)($payload['produced_qty'] ?? 0)) ?> <?= e($payload['finished_unit'] ?? '') ?></div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد أوامر تصنيع بعد.</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-4">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">أحدث حركات المخزون</h2><p class="section-copy">تشمل المبيعات والمشتريات والتصنيع في سجل واحد.</p></div></div>
<?php if ($movements): ?>
<div class="vstack gap-2">
<?php foreach ($movements as $movement): $payload = $movement['payload_data']; $delta = (float)($payload['qty_change'] ?? 0); ?>
<div class="list-row">
<div><strong><?= e($payload['product_name'] ?? '') ?></strong><div class="small text-secondary"><?= e($payload['movement_type'] ?? '') ?> — <?= e($payload['reference_code'] ?? '') ?></div></div>
<div class="text-start"><span class="badge <?= e(movement_badge_class($delta)) ?> mb-1"><?= e(movement_direction_label($delta)) ?></span><div class="small"><?= e((string)$delta) ?> / بعد الحركة <?= e((string)($payload['qty_after'] ?? 0)) ?></div></div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد حركات مخزون بعد.</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-4">
<div class="panel-card h-100">
<div class="section-header"><div><h2 class="section-title">آخر نشاط محاسبي</h2><p class="section-copy">مقبوضات ومدفوعات ومصروفات مرحّلة.</p></div></div>
<?php if ($accountingActivity): ?>
<div class="vstack gap-2">
<?php foreach ($accountingActivity as $row): $payload = $row['payload_data']; ?>
<div class="list-row">
<div><strong><?= e($row['title']) ?></strong><div class="small text-secondary"><?= e($row['code']) ?></div></div>
<div class="text-start small"><?= e(format_money((float)($payload['amount'] ?? 0))) ?></div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد قيود محاسبية بعد.</div>
<?php endif; ?>
</div>
</div>
</section>
<?php render_footer(); ?>

58
login.php Normal file
View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
if (is_logged_in()) {
redirect('index.php');
}
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$username = trim((string)($_POST['username'] ?? ''));
$password = (string)($_POST['password'] ?? '');
$error = login_attempt($username, $password);
if ($error === null) {
set_flash('success', 'تم تسجيل الدخول بنجاح.');
redirect('index.php');
}
}
render_header('تسجيل الدخول', 'بوابة الدخول إلى نظام ERP بصلاحيات حسب الدور.', 'auth');
?>
<div class="row justify-content-center">
<div class="col-lg-5 col-xl-4">
<div class="panel-card auth-card">
<div class="section-header compact">
<div>
<span class="eyebrow">Secure Access</span>
<h1 class="section-title mt-2">تسجيل الدخول إلى ERP</h1>
<p class="section-copy">كلمات المرور محفوظة بالتشفير bcrypt، والوصول محكوم حسب الدور.</p>
</div>
</div>
<?php if ($error): ?>
<div class="alert alert-danger py-2"><?= e($error) ?></div>
<?php endif; ?>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<div>
<label class="form-label">اسم المستخدم</label>
<input type="text" class="form-control" name="username" required autocomplete="username">
</div>
<div>
<label class="form-label">كلمة المرور</label>
<input type="password" class="form-control" name="password" required autocomplete="current-password">
</div>
<button class="btn btn-dark w-100" type="submit">دخول</button>
</form>
<div class="subtle-card mt-4">
<div class="detail-label mb-2">حسابات تجريبية جاهزة</div>
<?php foreach (default_account_credentials() as $cred): ?>
<div class="small mb-1"><strong><?= e($cred['username']) ?></strong> / <?= e($cred['password']) ?> — <?= e(role_label($cred['role'])) ?></div>
<?php endforeach; ?>
<div class="small text-secondary mt-2">للاستخدام الأولي فقط غيّر كلمات المرور لاحقًا عند إضافة إدارة المستخدمين.</div>
</div>
</div>
</div>
</div>
<?php render_footer(); ?>

7
logout.php Normal file
View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
logout_user();
session_start();
set_flash('success', 'تم تسجيل الخروج بنجاح.');
redirect('login.php');

241
manufacturing.php Normal file
View File

@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('manufacturing');
$errors = [];
$qualityOptions = manufacturing_quality_options();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$rawProductId = (int)($_POST['raw_product_id'] ?? 0);
$finishedProductId = (int)($_POST['finished_product_id'] ?? 0);
$finishedQty = (float)($_POST['finished_qty'] ?? 0);
$actualRawQty = (float)($_POST['actual_raw_qty'] ?? 0);
$qualityStatus = (string)($_POST['quality_status'] ?? 'accepted');
$status = (string)($_POST['status'] ?? 'completed');
$notes = trim((string)($_POST['notes'] ?? ''));
$rawProduct = fetch_record('product', $rawProductId);
$finishedProduct = fetch_record('product', $finishedProductId);
if (!$rawProduct || (($rawProduct['payload_data']['category'] ?? '') !== 'مواد خام')) {
$errors[] = 'اختر مادة خام صحيحة لعملية التصنيع.';
}
if (!$finishedProduct || (($finishedProduct['payload_data']['category'] ?? '') === 'مواد خام')) {
$errors[] = 'اختر منتجًا نهائيًا صحيحًا.';
}
if ($rawProductId > 0 && $rawProductId === $finishedProductId) {
$errors[] = 'لا يمكن أن تكون المادة الخام والمنتج النهائي نفس الصنف.';
}
if ($finishedQty <= 0 || $actualRawQty <= 0) {
$errors[] = 'أدخل كميات صحيحة للإنتاج والاستهلاك الفعلي.';
}
if (!array_key_exists($qualityStatus, $qualityOptions)) {
$errors[] = 'حالة الجودة غير صحيحة.';
}
if (!in_array($status, ['draft', 'completed'], true)) {
$errors[] = 'حالة أمر التصنيع غير صحيحة.';
}
if (!$errors && $rawProduct && $finishedProduct) {
$rawPayload = $rawProduct['payload_data'];
$finishedPayload = $finishedProduct['payload_data'];
$producedQty = $qualityStatus === 'rejected' ? 0.0 : $finishedQty;
db()->beginTransaction();
try {
$manufacturingId = create_record('manufacturing_order', 'أمر تصنيع ' . $finishedProduct['title'], next_code('MO', 'manufacturing_order'), [
'raw_product_id' => (int)$rawProduct['id'],
'raw_product_name' => $rawProduct['title'],
'raw_sku' => $rawPayload['sku'] ?? $rawProduct['code'],
'raw_unit' => $rawPayload['unit'] ?? 'وحدة',
'finished_product_id' => (int)$finishedProduct['id'],
'finished_product_name' => $finishedProduct['title'],
'finished_sku' => $finishedPayload['sku'] ?? $finishedProduct['code'],
'finished_unit' => $finishedPayload['unit'] ?? 'وحدة',
'finished_qty' => $finishedQty,
'actual_raw_qty' => $actualRawQty,
'produced_qty' => $producedQty,
'quality_status' => $qualityStatus,
'quality_label' => manufacturing_quality_label($qualityStatus),
'conversion_ratio' => $actualRawQty > 0 ? round($finishedQty / $actualRawQty, 4) : 0,
'notes' => $notes,
'created_date' => date('Y-m-d H:i'),
'completed_at' => $status === 'completed' ? date('Y-m-d H:i') : null,
'created_by' => current_user()['username'] ?? 'system',
], $status);
$order = fetch_record('manufacturing_order', $manufacturingId);
if ($status === 'completed' && $order) {
$rawStock = adjust_product_stock($rawProduct, -$actualRawQty, 'manufacturing_consume', $order['code'], 'manufacturing_order', $manufacturingId, 'استهلاك خامات لأمر التصنيع ' . $order['code']);
$payload = $order['payload_data'];
$payload['raw_stock_after'] = $rawStock['after'];
if ($producedQty > 0) {
$finishedStock = adjust_product_stock($finishedProduct, $producedQty, 'manufacturing_output', $order['code'], 'manufacturing_order', $manufacturingId, 'إضافة إنتاج نهائي لأمر التصنيع ' . $order['code']);
$payload['finished_stock_after'] = $finishedStock['after'];
} else {
$currentFinishedStock = (float)($finishedPayload['stock_qty'] ?? 0);
$payload['finished_stock_after'] = $currentFinishedStock;
}
update_record_payload($manufacturingId, $payload, 'completed');
}
db()->commit();
set_flash('success', $status === 'completed' ? 'تم إكمال أمر التصنيع وتحديث المخزون تلقائيًا.' : 'تم حفظ أمر التصنيع كمسودة.');
redirect('manufacturing.php?id=' . $manufacturingId);
} catch (Throwable $e) {
db()->rollBack();
$errors[] = 'تعذر حفظ أمر التصنيع، تأكد من توفر المخزون الخام ثم حاول مرة أخرى.';
}
}
}
$rawMaterials = raw_material_dataset();
$finishedProducts = finished_product_dataset();
$orders = fetch_records('manufacturing_order');
$detail = isset($_GET['id']) ? fetch_record('manufacturing_order', (int)$_GET['id']) : null;
$todayCount = today_record_count('manufacturing_order');
$completedCount = 0;
$draftCount = 0;
$totalProducedQty = 0.0;
foreach ($orders as $order) {
$payload = $order['payload_data'];
if (($order['status'] ?? '') === 'completed') {
$completedCount++;
}
if (($order['status'] ?? '') === 'draft') {
$draftCount++;
}
$totalProducedQty += (float)($payload['produced_qty'] ?? 0);
}
render_header('التصنيع', 'تسجيل أوامر تصنيع تخصم الخامات وتضيف المنتجات النهائية تلقائيًا مع متابعة الجودة.', 'manufacturing');
?>
<div class="row g-3 mb-4">
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">أوامر التصنيع</div><div class="stat-value"><?= e((string)count($orders)) ?></div><div class="stat-note">إجمالي السجل</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">اليوم</div><div class="stat-value"><?= e((string)$todayCount) ?></div><div class="stat-note">أوامر اليوم</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card"><div class="stat-label">مكتملة</div><div class="stat-value"><?= e((string)$completedCount) ?></div><div class="stat-note">تم ترحيلها للمخزون</div></div></div>
<div class="col-6 col-xl-3"><div class="stat-card stat-card-soft"><div class="stat-label">الإنتاج الناتج</div><div class="stat-value"><?= e((string)$totalProducedQty) ?></div><div class="stat-note">إجمالي الكمية المقبولة</div></div></div>
</div>
<div class="row g-4">
<div class="col-xl-5">
<div class="panel-card">
<div class="section-header compact"><div><h1 class="section-title mb-1">إنشاء أمر تصنيع</h1><p class="section-copy">اختر الخامة والمنتج النهائي وسجّل الكمية الفعلية المستخدمة وحالة الجودة.</p></div></div>
<?php if (!$rawMaterials || !$finishedProducts): ?>
<div class="empty-inline mb-3">يلزم وجود مادة خام ومنتج نهائي واحد على الأقل في صفحة الأصناف قبل بدء التصنيع.</div>
<?php endif; ?>
<?php if ($errors): ?>
<div class="alert alert-danger py-2"><?php foreach ($errors as $error): ?><div><?= e($error) ?></div><?php endforeach; ?></div>
<?php endif; ?>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<div>
<label class="form-label">المادة الخام</label>
<select class="form-select" name="raw_product_id" required>
<option value="">اختر مادة خام</option>
<?php foreach ($rawMaterials as $product): ?>
<option value="<?= (int)$product['id'] ?>"><?= e($product['name']) ?> — <?= e($product['sku']) ?> (<?= e((string)$product['stock_qty']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">المنتج النهائي</label>
<select class="form-select" name="finished_product_id" required>
<option value="">اختر منتجًا نهائيًا</option>
<?php foreach ($finishedProducts as $product): ?>
<option value="<?= (int)$product['id'] ?>"><?= e($product['name']) ?> — <?= e($product['sku']) ?> (<?= e((string)$product['stock_qty']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="row g-3">
<div class="col-md-6"><label class="form-label">الكمية المنتجة</label><input type="number" step="0.01" min="0.01" class="form-control" name="finished_qty" value="1" required></div>
<div class="col-md-6"><label class="form-label">الاستهلاك الفعلي للخامة</label><input type="number" step="0.01" min="0.01" class="form-control" name="actual_raw_qty" value="1" required></div>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">الجودة</label>
<select class="form-select" name="quality_status">
<?php foreach ($qualityOptions as $key => $label): ?>
<option value="<?= e($key) ?>"><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">الحالة</label>
<select class="form-select" name="status">
<option value="completed">إكمال الآن</option>
<option value="draft">مسودة</option>
</select>
</div>
</div>
<div>
<label class="form-label">ملاحظات الجودة أو التشغيل</label>
<textarea class="form-control" name="notes" rows="3"></textarea>
</div>
<button class="btn btn-dark" type="submit" <?= (!$rawMaterials || !$finishedProducts) ? 'disabled' : '' ?>>حفظ أمر التصنيع</button>
</form>
</div>
</div>
<div class="col-xl-7">
<div class="panel-card mb-4">
<div class="section-header"><div><h2 class="section-title">سجل أوامر التصنيع</h2><p class="section-copy">الأوامر المكتملة تخصم الخامات وتضيف المنتجات النهائية مباشرة.</p></div></div>
<?php if ($orders): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>الرقم</th><th>المنتج النهائي</th><th>الخامة</th><th>الناتج</th><th>الحالة</th></tr></thead>
<tbody>
<?php foreach ($orders as $order): $payload = $order['payload_data']; ?>
<tr>
<td><a class="table-link" href="manufacturing.php?id=<?= (int)$order['id'] ?>"><?= e($order['code']) ?></a></td>
<td><?= e($payload['finished_product_name'] ?? '') ?><div class="small text-secondary"><?= e((string)($payload['finished_qty'] ?? 0)) ?> <?= e($payload['finished_unit'] ?? '') ?></div></td>
<td><?= e($payload['raw_product_name'] ?? '') ?><div class="small text-secondary"><?= e((string)($payload['actual_raw_qty'] ?? 0)) ?> <?= e($payload['raw_unit'] ?? '') ?></div></td>
<td><?= e((string)($payload['produced_qty'] ?? 0)) ?> <?= e($payload['finished_unit'] ?? '') ?><div class="small text-secondary"><?= e(manufacturing_quality_label((string)($payload['quality_status'] ?? 'accepted'))) ?></div></td>
<td><span class="badge <?= e(status_badge_class((string)$order['status'])) ?>"><?= e(order_status_label((string)$order['status'])) ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد أوامر تصنيع بعد.</div>
<?php endif; ?>
</div>
<div class="panel-card">
<div class="section-header compact"><div><h2 class="section-title">تفاصيل أمر التصنيع</h2><p class="section-copy">يعرض أثر العملية على المخزون الفعلي لكل صنف.</p></div></div>
<?php if ($detail): $payload = $detail['payload_data']; ?>
<div class="detail-grid mb-3">
<div class="detail-block"><span class="detail-label">رقم الأمر</span><strong><?= e($detail['code']) ?></strong></div>
<div class="detail-block"><span class="detail-label">الحالة</span><strong><?= e(order_status_label((string)$detail['status'])) ?></strong></div>
<div class="detail-block"><span class="detail-label">الجودة</span><strong><?= e(manufacturing_quality_label((string)($payload['quality_status'] ?? 'accepted'))) ?></strong></div>
<div class="detail-block"><span class="detail-label">المنشئ</span><strong><?= e($payload['created_by'] ?? '') ?></strong></div>
</div>
<div class="subtle-card mb-3">
<div class="summary-row"><span>المادة الخام</span><strong><?= e($payload['raw_product_name'] ?? '') ?> — <?= e($payload['raw_sku'] ?? '') ?></strong></div>
<div class="summary-row"><span>الاستهلاك الفعلي</span><strong><?= e((string)($payload['actual_raw_qty'] ?? 0)) ?> <?= e($payload['raw_unit'] ?? '') ?></strong></div>
<div class="summary-row"><span>المخزون بعد الخصم</span><strong><?= e((string)($payload['raw_stock_after'] ?? '—')) ?></strong></div>
</div>
<div class="subtle-card">
<div class="summary-row"><span>المنتج النهائي</span><strong><?= e($payload['finished_product_name'] ?? '') ?> — <?= e($payload['finished_sku'] ?? '') ?></strong></div>
<div class="summary-row"><span>الكمية المنتجة</span><strong><?= e((string)($payload['finished_qty'] ?? 0)) ?> <?= e($payload['finished_unit'] ?? '') ?></strong></div>
<div class="summary-row"><span>الكمية المرحلة فعليًا</span><strong><?= e((string)($payload['produced_qty'] ?? 0)) ?> <?= e($payload['finished_unit'] ?? '') ?></strong></div>
<div class="summary-row"><span>المخزون بعد الإضافة</span><strong><?= e((string)($payload['finished_stock_after'] ?? '—')) ?></strong></div>
</div>
<div class="detail-grid mt-3">
<div class="detail-block"><span class="detail-label">تاريخ الإنشاء</span><strong><?= e($payload['created_date'] ?? '') ?></strong></div>
<div class="detail-block"><span class="detail-label">تاريخ الإكمال</span><strong><?= e($payload['completed_at'] ?? '—') ?></strong></div>
<div class="detail-block"><span class="detail-label">نسبة التحويل</span><strong><?= e((string)($payload['conversion_ratio'] ?? 0)) ?></strong></div>
<div class="detail-block"><span class="detail-label">مسودات</span><strong><?= e((string)$draftCount) ?></strong></div>
</div>
<?php if (!empty($payload['notes'])): ?><div class="mt-3 text-secondary small"><?= e($payload['notes']) ?></div><?php endif; ?>
<?php else: ?>
<div class="empty-inline">اختر أمر تصنيع من الجدول لعرض التفاصيل.</div>
<?php endif; ?>
</div>
</div>
</div>
<?php render_footer(); ?>

109
print_order.php Normal file
View File

@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('documents');
$document = isset($_GET['id']) ? fetch_sales_document_by_id((int)$_GET['id']) : null;
if (!$document) {
http_response_code(404);
exit('Document not found');
}
$payload = $document['payload_data'];
$documentLabel = sales_document_label((string)$document['record_type']);
$documentSubtitle = match ((string)$document['record_type']) {
'sales_quote' => 'عرض سعر قابل للطباعة والحفظ PDF من المتصفح',
'sales_order' => 'أمر بيع مؤكد وقابل للطباعة أو الحفظ PDF',
'delivery_note' => 'أمر تسليم جاهز للطباعة مع بيانات العميل والصنف',
'sales_invoice' => 'فاتورة مبيعات أولية قابلة للطباعة أو الحفظ PDF',
default => 'مستند مبيعات قابل للطباعة',
};
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ('Printable ' . $documentLabel);
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($document['code']) ?> | <?= e(app_name()) ?></title>
<meta name="description" content="<?= e($projectDescription) ?>">
<meta name="robots" content="noindex, nofollow">
<?php if ($projectDescription): ?>
<meta property="og:description" content="<?= e($projectDescription) ?>">
<meta property="twitter:description" content="<?= e($projectDescription) ?>">
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= e($projectImageUrl) ?>">
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
</head>
<body class="print-body">
<main class="container py-4">
<div class="print-sheet mx-auto">
<div class="d-flex justify-content-between align-items-start border-bottom pb-3 mb-4">
<div>
<div class="print-eyebrow">Printable Sales Document</div>
<h1 class="h3 mb-1"><?= e(app_name()) ?></h1>
<div class="text-secondary"><?= e($documentLabel) ?> — <?= e($documentSubtitle) ?></div>
</div>
<div class="text-start">
<div class="fw-semibold"><?= e($document['code']) ?></div>
<div class="text-secondary small"><?= e($payload['created_date'] ?? '') ?></div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="subtle-card h-100">
<div class="detail-label mb-2">بيانات العميل</div>
<div class="fw-semibold"><?= e($payload['customer_name'] ?? '') ?></div>
<div class="text-secondary small">الفرع: <?= e($payload['branch'] ?? '') ?></div>
</div>
</div>
<div class="col-md-6">
<div class="subtle-card h-100">
<div class="detail-label mb-2">بيانات المستند</div>
<div class="small">الحالة: <?= e(order_status_label((string)$document['status'])) ?></div>
<div class="small">نوع المستند: <?= e($documentLabel) ?></div>
<?php if (!empty($payload['source_document_code'])): ?>
<div class="small">المصدر: <?= e((string)$payload['source_document_label']) ?> — <?= e((string)$payload['source_document_code']) ?></div>
<?php endif; ?>
</div>
</div>
</div>
<div class="table-responsive mb-4">
<table class="table app-table mb-0">
<thead><tr><th>الصنف</th><th>SKU</th><th>الكمية</th><th>سعر الوحدة</th><th>الإجمالي</th></tr></thead>
<tbody>
<tr>
<td><?= e($payload['product_name'] ?? '') ?></td>
<td><?= e($payload['sku'] ?? '') ?></td>
<td><?= e((string)($payload['qty'] ?? 0)) ?> <?= e($payload['unit'] ?? '') ?></td>
<td><?= e(format_money((float)($payload['unit_price'] ?? 0))) ?></td>
<td><?= e(format_money((float)($payload['subtotal'] ?? 0))) ?></td>
</tr>
</tbody>
</table>
</div>
<div class="row justify-content-end mb-4">
<div class="col-md-5">
<div class="sales-summary-box">
<div class="summary-row"><span>الإجمالي قبل الضريبة</span><strong><?= e(format_money((float)($payload['subtotal'] ?? 0))) ?></strong></div>
<div class="summary-row"><span>الضريبة 15%</span><strong><?= e(format_money((float)($payload['vat'] ?? 0))) ?></strong></div>
<div class="summary-row grand"><span>الإجمالي النهائي</span><strong><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></strong></div>
</div>
</div>
</div>
<div class="row g-4 mt-4 pt-4 border-top"><div class="col-md-6"><div class="signature-box">توقيع المبيعات</div></div><div class="col-md-6"><div class="signature-box"><?= e($document['record_type'] === 'delivery_note' ? 'توقيع المستلم' : 'توقيع العميل') ?></div></div></div>
<?php if (!empty($payload['notes'])): ?><div class="subtle-card mt-4"><div class="detail-label mb-2">ملاحظات</div><div><?= e($payload['notes']) ?></div></div><?php endif; ?>
<div class="d-print-none mt-4 d-flex gap-2 justify-content-end"><a href="sales_orders.php?id=<?= (int)$document['id'] ?>" class="btn btn-outline-secondary">العودة</a><button class="btn btn-dark" onclick="window.print()">طباعة / حفظ PDF</button></div>
</div>
</main>
</body>
</html>

194
products.php Normal file
View File

@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('products');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$name = trim((string)($_POST['name'] ?? ''));
$sku = trim((string)($_POST['sku'] ?? ''));
$unit = trim((string)($_POST['unit'] ?? ''));
$category = trim((string)($_POST['category'] ?? ''));
$stockQty = trim((string)($_POST['stock_qty'] ?? ''));
$reorder = trim((string)($_POST['reorder_level'] ?? ''));
$salePrice = trim((string)($_POST['sale_price'] ?? ''));
$notes = trim((string)($_POST['notes'] ?? ''));
if ($name === '' || $sku === '' || $unit === '') {
$errors[] = 'الاسم وSKU والوحدة حقول مطلوبة.';
}
if (!is_numeric($stockQty) || !is_numeric($reorder) || !is_numeric($salePrice)) {
$errors[] = 'أدخل أرقامًا صحيحة للمخزون وحد الطلب والسعر.';
}
if (!$errors) {
create_record('product', $name, $sku, [
'sku' => $sku,
'unit' => $unit,
'category' => $category,
'stock_qty' => (float)$stockQty,
'reorder_level' => (float)$reorder,
'sale_price' => (float)$salePrice,
'notes' => $notes,
]);
set_flash('success', 'تمت إضافة الصنف بنجاح.');
redirect('products.php');
}
}
$products = fetch_records('product');
$detail = isset($_GET['id']) ? fetch_record('product', (int)$_GET['id']) : null;
render_header('الأصناف والمخزون', 'إدارة الأصناف الجاهزة للبيع ومتابعة المخزون الحالي وحدود إعادة الطلب.', 'products');
?>
<div class="row g-4">
<div class="col-lg-5">
<div class="panel-card">
<div class="section-header compact">
<div>
<h1 class="section-title mb-1">إضافة صنف</h1>
<p class="section-copy">هذا السجل يستخدم لاحقًا في المبيعات والمشتريات والتصنيع.</p>
</div>
</div>
<?php if ($errors): ?>
<div class="alert alert-danger py-2">
<?php foreach ($errors as $error): ?>
<div><?= e($error) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<div>
<label class="form-label">اسم الصنف</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">SKU</label>
<input type="text" class="form-control" name="sku" required>
</div>
<div class="col-md-6">
<label class="form-label">الوحدة</label>
<input type="text" class="form-control" name="unit" placeholder="عبوة" required>
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">الفئة</label>
<select class="form-select" name="category">
<option>منتج نهائي</option>
<option>مواد خام</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">المخزون الحالي</label>
<input type="number" step="0.01" class="form-control" name="stock_qty" required>
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">حد إعادة الطلب</label>
<input type="number" step="0.01" class="form-control" name="reorder_level" required>
</div>
<div class="col-md-6">
<label class="form-label">سعر البيع</label>
<input type="number" step="0.01" class="form-control" name="sale_price" required>
</div>
</div>
<div>
<label class="form-label">ملاحظات</label>
<textarea class="form-control" name="notes" rows="3"></textarea>
</div>
<button class="btn btn-dark" type="submit">حفظ الصنف</button>
</form>
</div>
</div>
<div class="col-lg-7">
<div class="panel-card mb-4">
<div class="section-header">
<div>
<h2 class="section-title">جدول الأصناف</h2>
<p class="section-copy">يعرض الكمية الحالية، التنبيه، والسعر الأساسي.</p>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead>
<tr>
<th>الصنف</th>
<th>الفئة</th>
<th>المخزون</th>
<th>السعر</th>
<th>تفاصيل</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $product): $payload = $product['payload_data']; $low = (float)($payload['stock_qty'] ?? 0) <= (float)($payload['reorder_level'] ?? 0); ?>
<tr>
<td>
<div class="fw-semibold"><?= e($product['title']) ?></div>
<div class="small text-secondary"><?= e($payload['sku'] ?? $product['code']) ?></div>
</td>
<td><?= e($payload['category'] ?? '') ?></td>
<td>
<?= e((string)($payload['stock_qty'] ?? 0)) ?>
<?php if ($low): ?><span class="badge text-bg-light border ms-2">منخفض</span><?php endif; ?>
</td>
<td><?= e(format_money((float)($payload['sale_price'] ?? 0))) ?></td>
<td><a class="btn btn-sm btn-outline-secondary" href="products.php?id=<?= (int)$product['id'] ?>">عرض</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="panel-card">
<div class="section-header compact">
<div>
<h2 class="section-title">تفاصيل الصنف</h2>
</div>
</div>
<?php if ($detail): $payload = $detail['payload_data']; ?>
<div class="detail-grid">
<div class="detail-block">
<span class="detail-label">اسم الصنف</span>
<strong><?= e($detail['title']) ?></strong>
</div>
<div class="detail-block">
<span class="detail-label">SKU</span>
<strong><?= e($payload['sku'] ?? $detail['code']) ?></strong>
</div>
<div class="detail-block">
<span class="detail-label">المخزون الحالي</span>
<strong><?= e((string)($payload['stock_qty'] ?? 0)) . ' ' . e($payload['unit'] ?? '') ?></strong>
</div>
<div class="detail-block">
<span class="detail-label">حد إعادة الطلب</span>
<strong><?= e((string)($payload['reorder_level'] ?? 0)) ?></strong>
</div>
</div>
<hr>
<div class="row g-3">
<div class="col-md-6">
<div class="subtle-card h-100">
<div class="detail-label mb-2">الفئة</div>
<div><?= e($payload['category'] ?? '') ?></div>
</div>
</div>
<div class="col-md-6">
<div class="subtle-card h-100">
<div class="detail-label mb-2">سعر البيع</div>
<div><?= e(format_money((float)($payload['sale_price'] ?? 0))) ?></div>
</div>
</div>
</div>
<div class="mt-3 text-secondary small"><?= e($payload['notes'] ?? '') ?></div>
<?php else: ?>
<div class="empty-inline">اختر صنفًا من الجدول لعرض تفاصيله.</div>
<?php endif; ?>
</div>
</div>
</div>
<?php render_footer(); ?>

171
purchases.php Normal file
View File

@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('purchases');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$supplierId = (int)($_POST['supplier_id'] ?? 0);
$productId = (int)($_POST['product_id'] ?? 0);
$qty = (float)($_POST['qty'] ?? 0);
$unitCost = (float)($_POST['unit_cost'] ?? 0);
$status = (string)($_POST['status'] ?? 'received');
$notes = trim((string)($_POST['notes'] ?? ''));
$supplier = fetch_record('supplier', $supplierId);
$product = fetch_record('product', $productId);
if (!$supplier) {
$errors[] = 'اختر موردًا صحيحًا.';
}
if (!$product) {
$errors[] = 'اختر صنفًا صحيحًا.';
}
if ($qty <= 0 || $unitCost < 0) {
$errors[] = 'أدخل كمية وسعر شراء صالحين.';
}
if (!in_array($status, ['draft', 'received'], true)) {
$errors[] = 'حالة أمر الشراء غير صحيحة.';
}
if (!$errors && $supplier && $product) {
$productPayload = $product['payload_data'];
$sku = (string)($productPayload['sku'] ?? $product['code']);
$subtotal = $qty * $unitCost;
$vat = $subtotal * 0.15;
$grand = $subtotal + $vat;
db()->beginTransaction();
try {
$purchaseId = create_record('purchase_order', 'أمر شراء ' . $supplier['title'], next_code('PO', 'purchase_order'), [
'supplier_id' => (int)$supplier['id'],
'supplier_name' => $supplier['title'],
'product_id' => (int)$product['id'],
'product_name' => $product['title'],
'sku' => $sku,
'unit' => $productPayload['unit'] ?? 'وحدة',
'qty' => $qty,
'unit_cost' => $unitCost,
'subtotal' => $subtotal,
'vat' => $vat,
'grand_total' => $grand,
'notes' => $notes,
'created_date' => date('Y-m-d H:i'),
'received_at' => $status === 'received' ? date('Y-m-d H:i') : null,
'created_by' => current_user()['username'] ?? 'system',
], $status);
$purchase = fetch_record('purchase_order', $purchaseId);
if ($status === 'received' && $purchase) {
$stock = adjust_product_stock($product, $qty, 'purchase_receive', $purchase['code'], 'purchase_order', $purchaseId, 'استلام شراء من المورد ' . $supplier['title']);
$payload = $purchase['payload_data'];
$payload['stock_after'] = $stock['after'];
update_record_payload($purchaseId, $payload, 'received');
}
db()->commit();
set_flash('success', $status === 'received' ? 'تم إنشاء أمر الشراء واستلامه وتحديث المخزون.' : 'تم حفظ أمر الشراء كمسودة.');
redirect('purchases.php?id=' . $purchaseId);
} catch (Throwable $e) {
db()->rollBack();
$errors[] = 'تعذر حفظ أمر الشراء، حاول مرة أخرى.';
}
}
}
$suppliers = supplier_dataset();
$products = product_dataset();
$orders = fetch_records('purchase_order');
$detail = isset($_GET['id']) ? fetch_record('purchase_order', (int)$_GET['id']) : null;
render_header('أوامر الشراء', 'إنشاء أوامر شراء وربطها بالموردين وتحديث المخزون عند الاستلام.', 'purchases');
?>
<div class="row g-4">
<div class="col-xl-5">
<div class="panel-card">
<div class="section-header compact"><div><h1 class="section-title mb-1">إنشاء أمر شراء</h1><p class="section-copy">اختر المورد والصنف ثم قرر هل هو استلام مباشر أم مسودة معلقة.</p></div></div>
<?php if ($errors): ?>
<div class="alert alert-danger py-2"><?php foreach ($errors as $error): ?><div><?= e($error) ?></div><?php endforeach; ?></div>
<?php endif; ?>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<div>
<label class="form-label">المورد</label>
<select class="form-select" name="supplier_id" required>
<option value="">اختر المورد</option>
<?php foreach ($suppliers as $supplier): ?>
<option value="<?= (int)$supplier['id'] ?>"><?= e($supplier['name']) ?> — <?= e($supplier['code']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">الصنف</label>
<select class="form-select" name="product_id" required>
<option value="">اختر الصنف</option>
<?php foreach ($products as $product): ?>
<option value="<?= (int)$product['id'] ?>"><?= e($product['name']) ?> — <?= e($product['sku']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row g-3">
<div class="col-md-4"><label class="form-label">الكمية</label><input type="number" step="0.01" min="0.01" class="form-control" name="qty" value="1" required></div>
<div class="col-md-4"><label class="form-label">سعر الشراء</label><input type="number" step="0.01" min="0" class="form-control" name="unit_cost" value="0" required></div>
<div class="col-md-4"><label class="form-label">الحالة</label><select class="form-select" name="status"><option value="received">استلام الآن</option><option value="draft">مسودة</option></select></div>
</div>
<div>
<label class="form-label">ملاحظات</label>
<textarea class="form-control" name="notes" rows="3"></textarea>
</div>
<button class="btn btn-dark" type="submit">حفظ أمر الشراء</button>
</form>
</div>
</div>
<div class="col-xl-7">
<div class="panel-card mb-4">
<div class="section-header"><div><h2 class="section-title">سجل أوامر الشراء</h2><p class="section-copy">الأوامر المستلمة ترفع المخزون تلقائيًا، والمسودات تبقى دون تأثير.</p></div></div>
<?php if ($orders): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>الرقم</th><th>المورد</th><th>الصنف</th><th>الإجمالي</th><th>الحالة</th></tr></thead>
<tbody>
<?php foreach ($orders as $order): $payload = $order['payload_data']; ?>
<tr>
<td><a class="table-link" href="purchases.php?id=<?= (int)$order['id'] ?>"><?= e($order['code']) ?></a></td>
<td><?= e($payload['supplier_name'] ?? '') ?></td>
<td><?= e($payload['product_name'] ?? '') ?><div class="small text-secondary"><?= e((string)($payload['qty'] ?? 0)) ?> <?= e($payload['unit'] ?? '') ?></div></td>
<td><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></td>
<td><span class="badge <?= e(status_badge_class((string)$order['status'])) ?>"><?= e(order_status_label((string)$order['status'])) ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد أوامر شراء بعد.</div>
<?php endif; ?>
</div>
<div class="panel-card">
<div class="section-header compact"><div><h2 class="section-title">تفاصيل أمر الشراء</h2></div></div>
<?php if ($detail): $payload = $detail['payload_data']; ?>
<div class="detail-grid mb-3">
<div class="detail-block"><span class="detail-label">رقم الأمر</span><strong><?= e($detail['code']) ?></strong></div>
<div class="detail-block"><span class="detail-label">الحالة</span><strong><?= e(order_status_label((string)$detail['status'])) ?></strong></div>
<div class="detail-block"><span class="detail-label">المورد</span><strong><?= e($payload['supplier_name'] ?? '') ?></strong></div>
<div class="detail-block"><span class="detail-label">تاريخ الإنشاء</span><strong><?= e($payload['created_date'] ?? '') ?></strong></div>
</div>
<div class="subtle-card">
<div class="summary-row"><span>الصنف</span><strong><?= e($payload['product_name'] ?? '') ?> — <?= e($payload['sku'] ?? '') ?></strong></div>
<div class="summary-row"><span>الكمية</span><strong><?= e((string)($payload['qty'] ?? 0)) ?> <?= e($payload['unit'] ?? '') ?></strong></div>
<div class="summary-row"><span>سعر الشراء</span><strong><?= e(format_money((float)($payload['unit_cost'] ?? 0))) ?></strong></div>
<div class="summary-row"><span>الإجمالي</span><strong><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></strong></div>
<div class="summary-row"><span>المخزون بعد الاستلام</span><strong><?= e((string)($payload['stock_after'] ?? '—')) ?></strong></div>
</div>
<?php if (!empty($payload['notes'])): ?><div class="mt-3 text-secondary small"><?= e($payload['notes']) ?></div><?php endif; ?>
<?php else: ?>
<div class="empty-inline">اختر أمر شراء من الجدول لعرض تفاصيله.</div>
<?php endif; ?>
</div>
</div>
</div>
<?php render_footer(); ?>

263
reports.php Normal file
View File

@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('reports');
$today = date('Y-m-d');
$defaultStart = date('Y-m-01');
$startDate = trim((string)($_GET['start_date'] ?? $defaultStart));
$endDate = trim((string)($_GET['end_date'] ?? $today));
if ($startDate !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
$startDate = $defaultStart;
}
if ($endDate !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
$endDate = $today;
}
if ($startDate !== '' && $endDate !== '' && $startDate > $endDate) {
[$startDate, $endDate] = [$endDate, $startDate];
}
$salesInvoices = filter_records_by_date(fetch_records('sales_invoice'), $startDate, $endDate);
$purchaseOrders = filter_records_by_date(fetch_records('purchase_order'), $startDate, $endDate);
$receivedPurchases = array_values(array_filter($purchaseOrders, static fn (array $row): bool => (string)($row['status'] ?? '') === 'received'));
$manufacturingOrders = filter_records_by_date(fetch_records('manufacturing_order'), $startDate, $endDate);
$completedManufacturing = array_values(array_filter($manufacturingOrders, static fn (array $row): bool => (string)($row['status'] ?? '') === 'completed'));
$customerPayments = filter_records_by_date(fetch_records('customer_payment'), $startDate, $endDate);
$supplierPayments = filter_records_by_date(fetch_records('supplier_payment'), $startDate, $endDate);
$expenses = filter_records_by_date(fetch_records('expense_entry'), $startDate, $endDate);
$stockMovements = filter_records_by_date(fetch_records('stock_movement'), $startDate, $endDate);
$salesTotal = 0.0;
$topCustomers = [];
foreach ($salesInvoices as $invoice) {
$payload = $invoice['payload_data'];
$amount = (float)($payload['grand_total'] ?? 0);
$salesTotal += $amount;
$customerName = (string)($payload['customer_name'] ?? 'عميل غير محدد');
if (!isset($topCustomers[$customerName])) {
$topCustomers[$customerName] = 0.0;
}
$topCustomers[$customerName] += $amount;
}
arsort($topCustomers);
$topCustomers = array_slice($topCustomers, 0, 5, true);
$purchaseTotal = 0.0;
foreach ($receivedPurchases as $purchase) {
$purchaseTotal += (float)($purchase['payload_data']['grand_total'] ?? 0);
}
$manufacturedQty = 0.0;
foreach ($completedManufacturing as $order) {
$manufacturedQty += (float)($order['payload_data']['produced_qty'] ?? 0);
}
$receiptsTotal = 0.0;
foreach ($customerPayments as $payment) {
$receiptsTotal += (float)($payment['payload_data']['amount'] ?? 0);
}
$supplierPaymentsTotal = 0.0;
foreach ($supplierPayments as $payment) {
$supplierPaymentsTotal += (float)($payment['payload_data']['amount'] ?? 0);
}
$expensesTotal = 0.0;
$expenseBreakdown = [];
foreach ($expenses as $expense) {
$payload = $expense['payload_data'];
$amount = (float)($payload['amount'] ?? 0);
$expensesTotal += $amount;
$category = expense_category_label((string)($payload['category'] ?? 'other'));
if (!isset($expenseBreakdown[$category])) {
$expenseBreakdown[$category] = 0.0;
}
$expenseBreakdown[$category] += $amount;
}
arsort($expenseBreakdown);
$productActivity = [];
foreach ($stockMovements as $movement) {
$payload = $movement['payload_data'];
$productName = (string)($payload['product_name'] ?? 'صنف غير محدد');
if (!isset($productActivity[$productName])) {
$productActivity[$productName] = 0.0;
}
$productActivity[$productName] += abs((float)($payload['qty_change'] ?? 0));
}
arsort($productActivity);
$productActivity = array_slice($productActivity, 0, 5, true);
$profitEstimate = $salesTotal - $purchaseTotal - $expensesTotal;
$netCashflow = $receiptsTotal - $supplierPaymentsTotal - $expensesTotal;
render_header('التقارير التنفيذية', 'تقارير ERP تشغيلية موحدة للمبيعات والمشتريات والتصنيع والمحاسبة مع فلترة حسب التاريخ.', 'reports');
?>
<div class="panel-card mb-4">
<div class="section-header">
<div>
<h1 class="section-title">التقارير التنفيذية</h1>
<p class="section-copy">شاشة موحدة لمراجعة أداء الفترة بين <?= e($startDate) ?> و<?= e($endDate) ?>.</p>
</div>
</div>
<form method="get" class="report-filter-grid">
<div>
<label class="form-label">من تاريخ</label>
<input type="date" class="form-control" name="start_date" value="<?= e($startDate) ?>">
</div>
<div>
<label class="form-label">إلى تاريخ</label>
<input type="date" class="form-control" name="end_date" value="<?= e($endDate) ?>">
</div>
<div class="d-flex align-items-end gap-2">
<button class="btn btn-dark w-100" type="submit">تحديث التقرير</button>
</div>
</form>
</div>
<section class="row g-3 mb-4">
<div class="col-6 col-xl-3"><div class="kpi-card"><div class="kpi-label">فواتير المبيعات</div><div class="kpi-value"><?= e(format_money($salesTotal)) ?></div><div class="kpi-note"><?= e((string)count($salesInvoices)) ?> فاتورة خلال الفترة</div></div></div>
<div class="col-6 col-xl-3"><div class="kpi-card"><div class="kpi-label">مشتريات مستلمة</div><div class="kpi-value"><?= e(format_money($purchaseTotal)) ?></div><div class="kpi-note"><?= e((string)count($receivedPurchases)) ?> أمر شراء مستلم</div></div></div>
<div class="col-6 col-xl-3"><div class="kpi-card"><div class="kpi-label">إنتاج مكتمل</div><div class="kpi-value"><?= e(number_format($manufacturedQty, 2)) ?></div><div class="kpi-note"><?= e((string)count($completedManufacturing)) ?> أمر تصنيع مكتمل</div></div></div>
<div class="col-6 col-xl-3"><div class="kpi-card"><div class="kpi-label">حركات المخزون</div><div class="kpi-value"><?= e((string)count($stockMovements)) ?></div><div class="kpi-note">إجمالي الإضافات والخصومات</div></div></div>
<div class="col-6 col-xl-3"><div class="kpi-card"><div class="kpi-label">مقبوضات العملاء</div><div class="kpi-value"><?= e(format_money($receiptsTotal)) ?></div><div class="kpi-note"><?= e((string)count($customerPayments)) ?> حركة قبض</div></div></div>
<div class="col-6 col-xl-3"><div class="kpi-card"><div class="kpi-label">مدفوعات الموردين</div><div class="kpi-value"><?= e(format_money($supplierPaymentsTotal)) ?></div><div class="kpi-note"><?= e((string)count($supplierPayments)) ?> حركة دفع</div></div></div>
<div class="col-6 col-xl-3"><div class="kpi-card"><div class="kpi-label">المصروفات</div><div class="kpi-value"><?= e(format_money($expensesTotal)) ?></div><div class="kpi-note"><?= e((string)count($expenses)) ?> قيد مصروف</div></div></div>
<div class="col-6 col-xl-3"><div class="kpi-card"><div class="kpi-label">صافي النقدية</div><div class="kpi-value"><?= e(format_money($netCashflow)) ?></div><div class="kpi-note">مقبوضات - مدفوعات - مصروفات</div></div></div>
</section>
<section class="row g-4">
<div class="col-xl-7">
<div class="panel-card mb-4">
<div class="section-header compact"><div><h2 class="section-title">أحدث فواتير المبيعات</h2><p class="section-copy">آخر 8 فواتير داخل الفترة المحددة.</p></div></div>
<?php if ($salesInvoices): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>الرقم</th><th>العميل</th><th>الإجمالي</th><th>التاريخ</th></tr></thead>
<tbody>
<?php foreach (array_slice($salesInvoices, 0, 8) as $invoice): $payload = $invoice['payload_data']; ?>
<tr>
<td><a class="table-link" href="sales_orders.php?id=<?= (int)$invoice['id'] ?>"><?= e($invoice['code']) ?></a></td>
<td><?= e($payload['customer_name'] ?? '') ?></td>
<td><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></td>
<td><?= e(record_created_date($invoice)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد فواتير مبيعات ضمن الفترة المحددة.</div>
<?php endif; ?>
</div>
<div class="panel-card mb-4">
<div class="section-header compact"><div><h2 class="section-title">المشتريات والتصنيع</h2><p class="section-copy">ملخص آخر الأوامر المستلمة والمكتملة.</p></div></div>
<div class="row g-3">
<div class="col-lg-6">
<div class="subtle-card h-100">
<div class="section-title mb-3">أوامر شراء مستلمة</div>
<?php if ($receivedPurchases): ?>
<?php foreach (array_slice($receivedPurchases, 0, 5) as $purchase): $payload = $purchase['payload_data']; ?>
<div class="summary-row"><span><?= e($purchase['code']) ?></span><strong><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></strong></div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-inline">لا توجد مشتريات مستلمة في هذه الفترة.</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-6">
<div class="subtle-card h-100">
<div class="section-title mb-3">أوامر تصنيع مكتملة</div>
<?php if ($completedManufacturing): ?>
<?php foreach (array_slice($completedManufacturing, 0, 5) as $order): $payload = $order['payload_data']; ?>
<div class="summary-row"><span><?= e($order['code']) ?></span><strong><?= e((string)($payload['produced_qty'] ?? 0)) ?> <?= e($payload['finished_unit'] ?? '') ?></strong></div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-inline">لا توجد أوامر تصنيع مكتملة في هذه الفترة.</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="panel-card">
<div class="section-header compact"><div><h2 class="section-title">الحركة المالية</h2><p class="section-copy">آخر المقبوضات والمدفوعات والمصروفات خلال الفترة.</p></div></div>
<?php $activity = array_merge($customerPayments, $supplierPayments, $expenses); ?>
<?php usort($activity, static fn (array $a, array $b): int => strcmp((string)($b['created_at'] ?? ''), (string)($a['created_at'] ?? ''))); ?>
<?php $activity = array_slice($activity, 0, 10); ?>
<?php if ($activity): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>النوع</th><th>الوصف</th><th>القيمة</th><th>التاريخ</th></tr></thead>
<tbody>
<?php foreach ($activity as $row): $payload = $row['payload_data']; ?>
<tr>
<td><?= e(match ((string)$row['record_type']) { 'customer_payment' => 'مقبوض', 'supplier_payment' => 'مدفوع', 'expense_entry' => 'مصروف', default => (string)$row['record_type'] }) ?></td>
<td><?= e($row['title']) ?></td>
<td><?= e(format_money((float)($payload['amount'] ?? 0))) ?></td>
<td><?= e(record_created_date($row)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد حركة مالية ضمن الفترة المحددة.</div>
<?php endif; ?>
</div>
</div>
<div class="col-xl-5">
<div class="panel-card mb-4">
<div class="section-header compact"><div><h2 class="section-title">مؤشرات الربحية</h2></div></div>
<div class="subtle-card">
<div class="summary-row"><span>الربح المتوقع</span><strong><?= e(format_money($profitEstimate)) ?></strong></div>
<div class="summary-row"><span>صافي التدفق النقدي</span><strong><?= e(format_money($netCashflow)) ?></strong></div>
<div class="summary-row"><span>المبيعات - المشتريات</span><strong><?= e(format_money($salesTotal - $purchaseTotal)) ?></strong></div>
<div class="summary-row"><span>إجمالي المصروفات</span><strong><?= e(format_money($expensesTotal)) ?></strong></div>
</div>
</div>
<div class="panel-card mb-4">
<div class="section-header compact"><div><h2 class="section-title">أفضل العملاء</h2><p class="section-copy">حسب قيمة الفواتير داخل الفترة.</p></div></div>
<?php if ($topCustomers): ?>
<div class="subtle-card">
<?php foreach ($topCustomers as $customerName => $amount): ?>
<div class="summary-row"><span><?= e($customerName) ?></span><strong><?= e(format_money($amount)) ?></strong></div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد بيانات مبيعات كافية لحساب أفضل العملاء.</div>
<?php endif; ?>
</div>
<div class="panel-card mb-4">
<div class="section-header compact"><div><h2 class="section-title">أكثر الأصناف حركة</h2><p class="section-copy">حسب إجمالي الكميات المتحركة في المخزون.</p></div></div>
<?php if ($productActivity): ?>
<div class="subtle-card">
<?php foreach ($productActivity as $productName => $qty): ?>
<div class="summary-row"><span><?= e($productName) ?></span><strong><?= e(number_format($qty, 2)) ?></strong></div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد حركات مخزون ضمن الفترة المحددة.</div>
<?php endif; ?>
</div>
<div class="panel-card">
<div class="section-header compact"><div><h2 class="section-title">تحليل المصروفات</h2><p class="section-copy">تجميع حسب الفئة.</p></div></div>
<?php if ($expenseBreakdown): ?>
<div class="subtle-card">
<?php foreach ($expenseBreakdown as $category => $amount): ?>
<div class="summary-row"><span><?= e($category) ?></span><strong><?= e(format_money($amount)) ?></strong></div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد مصروفات ضمن الفترة المحددة.</div>
<?php endif; ?>
</div>
</div>
</section>
<?php render_footer(); ?>

375
sales_orders.php Normal file
View File

@ -0,0 +1,375 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('orders');
function sales_unit_price(array $customer, array $product): float {
$customerPayload = $customer['payload_data'];
$productPayload = $product['payload_data'];
$sku = (string)($productPayload['sku'] ?? $product['code']);
$overrides = $customerPayload['price_overrides'] ?? [];
if (isset($overrides[$sku])) {
return (float)$overrides[$sku];
}
return (float)($productPayload['sale_price'] ?? 0);
}
function validate_sales_input(?array $customer, ?array $product, string $branch, float $qty, array &$errors): void {
if (!$customer) {
$errors[] = 'اختر عميلًا صحيحًا.';
}
if (!$product) {
$errors[] = 'اختر صنفًا صحيحًا.';
}
if ($qty <= 0) {
$errors[] = 'أدخل كمية أكبر من صفر.';
}
if ($customer) {
$customerPayload = $customer['payload_data'];
$branches = array_values($customerPayload['branches'] ?? []);
if ($branches && $branch === '') {
$errors[] = 'اختر فرع العميل.';
}
$allowedSkus = array_values($customerPayload['allowed_skus'] ?? []);
if ($product && $allowedSkus) {
$sku = (string)(($product['payload_data']['sku'] ?? null) ?: $product['code']);
if (!in_array($sku, $allowedSkus, true)) {
$errors[] = 'هذا الصنف غير مسموح لهذا العميل.';
}
}
}
}
function build_sales_payload(array $customer, array $product, string $branch, float $qty, string $notes, string $documentType, ?array $source = null): array {
$productPayload = $product['payload_data'];
$unitPrice = sales_unit_price($customer, $product);
$subtotal = $qty * $unitPrice;
$vat = $subtotal * 0.15;
$grand = $subtotal + $vat;
return [
'customer_id' => (int)$customer['id'],
'customer_name' => $customer['title'],
'branch' => $branch,
'product_id' => (int)$product['id'],
'product_name' => $product['title'],
'sku' => $productPayload['sku'] ?? $product['code'],
'unit' => $productPayload['unit'] ?? 'وحدة',
'qty' => $qty,
'unit_price' => $unitPrice,
'subtotal' => $subtotal,
'vat' => $vat,
'grand_total' => $grand,
'notes' => $notes,
'document_type' => $documentType,
'document_label' => sales_document_label($documentType),
'created_date' => date('Y-m-d H:i'),
'created_by' => current_user()['username'] ?? 'system',
'source_document_id' => (int)($source['id'] ?? 0),
'source_document_code' => $source['code'] ?? null,
'source_document_type' => $source['record_type'] ?? null,
'source_document_label' => $source ? sales_document_label((string)$source['record_type']) : null,
'inventory_effect' => $documentType === 'sales_order' ? 'deducted' : 'none',
];
}
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = (string)($_POST['form_action'] ?? 'create_document');
if ($action === 'create_document') {
$documentType = (string)($_POST['document_type'] ?? 'sales_order');
$customerId = (int)($_POST['customer_id'] ?? 0);
$branch = trim((string)($_POST['branch'] ?? ''));
$productId = (int)($_POST['product_id'] ?? 0);
$qty = (float)($_POST['qty'] ?? 0);
$notes = trim((string)($_POST['notes'] ?? ''));
if (!in_array($documentType, ['sales_quote', 'sales_order'], true)) {
$errors[] = 'نوع المستند غير صحيح.';
}
$customer = fetch_record('customer', $customerId);
$product = fetch_record('product', $productId);
validate_sales_input($customer, $product, $branch, $qty, $errors);
if (!$errors && $customer && $product) {
db()->beginTransaction();
try {
$payload = build_sales_payload($customer, $product, $branch, $qty, $notes, $documentType);
$status = $documentType === 'sales_quote' ? 'draft' : 'confirmed';
$documentId = create_record(
$documentType,
sales_document_label($documentType) . ' ' . $customer['title'],
next_code(sales_document_prefix($documentType), $documentType),
$payload,
$status
);
if ($documentType === 'sales_order') {
$document = fetch_sales_document_by_id($documentId);
if ($document) {
$stock = adjust_product_stock($product, -$qty, 'sales_confirm', $document['code'], 'sales_order', $documentId, 'بيع للعميل ' . $customer['title']);
$payload = $document['payload_data'];
$payload['stock_after'] = $stock['after'];
$payload['inventory_effect'] = 'deducted';
update_record_payload($documentId, $payload, 'confirmed');
}
}
db()->commit();
set_flash('success', $documentType === 'sales_quote' ? 'تم إنشاء عرض السعر بنجاح.' : 'تم إنشاء أمر البيع وتحديث المخزون بنجاح.');
redirect('sales_orders.php?id=' . $documentId);
} catch (Throwable $e) {
db()->rollBack();
$errors[] = $e instanceof RuntimeException ? 'المخزون غير كافٍ لتأكيد أمر البيع.' : 'تعذر حفظ المستند، حاول مرة أخرى.';
}
}
}
if ($action === 'convert_document') {
$sourceId = (int)($_POST['source_id'] ?? 0);
$targetType = (string)($_POST['target_type'] ?? '');
$source = fetch_sales_document_by_id($sourceId);
if (!$source) {
$errors[] = 'المستند المصدر غير موجود.';
} elseif (!in_array($targetType, sales_document_conversion_targets((string)$source['record_type']), true)) {
$errors[] = 'التحويل المطلوب غير مسموح لهذه المرحلة.';
} elseif (sales_document_child_exists($sourceId, $targetType)) {
$errors[] = 'تم إنشاء هذا المستند مسبقًا من نفس المصدر.';
}
if (!$errors && $source) {
$payload = $source['payload_data'];
$customer = fetch_record('customer', (int)($payload['customer_id'] ?? 0));
$product = fetch_record('product', (int)($payload['product_id'] ?? 0));
$branch = (string)($payload['branch'] ?? '');
$qty = (float)($payload['qty'] ?? 0);
$notes = trim((string)($payload['notes'] ?? ''));
validate_sales_input($customer, $product, $branch, $qty, $errors);
if (!$errors && $customer && $product) {
db()->beginTransaction();
try {
$newPayload = build_sales_payload($customer, $product, $branch, $qty, $notes, $targetType, $source);
if ($targetType === 'delivery_note') {
$newPayload['delivered_at'] = date('Y-m-d H:i');
}
if ($targetType === 'sales_invoice') {
$newPayload['invoiced_at'] = date('Y-m-d H:i');
$newPayload['payment_status'] = 'unpaid';
}
$newId = create_record(
$targetType,
sales_document_label($targetType) . ' ' . $customer['title'],
next_code(sales_document_prefix($targetType), $targetType),
$newPayload,
'confirmed'
);
if ($targetType === 'sales_order') {
$document = fetch_sales_document_by_id($newId);
if ($document) {
$stock = adjust_product_stock($product, -$qty, 'sales_convert', $document['code'], 'sales_order', $newId, 'تحويل من عرض سعر إلى أمر بيع للعميل ' . $customer['title']);
$newPayload = $document['payload_data'];
$newPayload['stock_after'] = $stock['after'];
$newPayload['inventory_effect'] = 'deducted';
update_record_payload($newId, $newPayload, 'confirmed');
}
}
db()->commit();
set_flash('success', 'تم إنشاء ' . sales_document_label($targetType) . ' بنجاح.');
redirect('sales_orders.php?id=' . $newId);
} catch (Throwable $e) {
db()->rollBack();
$errors[] = $e instanceof RuntimeException ? 'المخزون غير كافٍ لإتمام التحويل.' : 'تعذر تنفيذ التحويل، حاول مرة أخرى.';
}
}
}
}
}
$customers = customer_dataset();
$products = product_dataset();
$documents = fetch_sales_documents();
$detail = isset($_GET['id']) ? fetch_sales_document_by_id((int)$_GET['id']) : null;
$detailChildren = $detail ? sales_document_children((int)$detail['id']) : [];
$summaryCounts = [];
foreach (sales_document_record_types() as $type) {
$summaryCounts[$type] = record_count($type);
}
render_header('المبيعات والوثائق', 'إدارة عروض الأسعار وأوامر البيع وأوامر التسليم والفواتير وربطها في مسار واحد قابل للطباعة.', 'orders');
?>
<div class="row g-3 mb-4">
<div class="col-sm-6 col-xl-3"><div class="stat-card"><div class="stat-label">عروض الأسعار</div><div class="stat-value"><?= e((string)$summaryCounts['sales_quote']) ?></div><div class="stat-note">مرحلة التسعير قبل خصم المخزون</div></div></div>
<div class="col-sm-6 col-xl-3"><div class="stat-card"><div class="stat-label">أوامر البيع</div><div class="stat-value"><?= e((string)$summaryCounts['sales_order']) ?></div><div class="stat-note">تؤكد البيع وتخصم المخزون</div></div></div>
<div class="col-sm-6 col-xl-3"><div class="stat-card"><div class="stat-label">أوامر التسليم</div><div class="stat-value"><?= e((string)$summaryCounts['delivery_note']) ?></div><div class="stat-note">جاهزة للطباعة والتسليم</div></div></div>
<div class="col-sm-6 col-xl-3"><div class="stat-card"><div class="stat-label">الفواتير</div><div class="stat-value"><?= e((string)$summaryCounts['sales_invoice']) ?></div><div class="stat-note">مرتبطة بالعميل وبالمحاسبة لاحقًا</div></div></div>
</div>
<div class="row g-4">
<div class="col-xl-5">
<div class="panel-card">
<div class="section-header compact"><div><h1 class="section-title mb-1">إنشاء مستند مبيعات</h1><p class="section-copy">ابدأ بعرض سعر أو أنشئ أمر بيع مباشر. عند تأكيد أمر البيع يتم خصم المخزون تلقائيًا.</p></div></div>
<?php if ($errors): ?>
<div class="alert alert-danger py-2"><?php foreach ($errors as $error): ?><div><?= e($error) ?></div><?php endforeach; ?></div>
<?php endif; ?>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="form_action" value="create_document">
<div>
<label class="form-label">نوع المستند</label>
<select class="form-select" name="document_type">
<option value="sales_quote">عرض سعر</option>
<option value="sales_order" selected>أمر بيع مباشر</option>
</select>
<div class="form-text">عرض السعر لا يغير المخزون، بينما أمر البيع يخصم الكمية مباشرة.</div>
</div>
<div>
<label class="form-label">العميل</label>
<select class="form-select" name="customer_id" id="customerSelect" required>
<option value="">اختر العميل</option>
<?php foreach ($customers as $customer): ?>
<option value="<?= (int)$customer['id'] ?>"><?= e($customer['name']) ?> — <?= e($customer['code']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">الفرع</label>
<select class="form-select" name="branch" id="branchSelect"><option value="">اختر الفرع بعد العميل</option></select>
</div>
<div>
<label class="form-label">الصنف</label>
<select class="form-select" name="product_id" id="productSelect" required>
<option value="">اختر الصنف</option>
<?php foreach ($products as $product): ?>
<option value="<?= (int)$product['id'] ?>"><?= e($product['name']) ?> — <?= e($product['sku']) ?></option>
<?php endforeach; ?>
</select>
<div class="form-text" id="allowedHint">اختر العميل أولًا لمعرفة قيود الأصناف.</div>
</div>
<div class="row g-3">
<div class="col-md-6"><label class="form-label">الكمية</label><input type="number" min="1" step="1" class="form-control" name="qty" id="qtyInput" value="1" required></div>
<div class="col-md-6"><label class="form-label">السعر / الوحدة</label><input type="text" class="form-control" id="unitPricePreview" value="0.00 ر.س" readonly></div>
</div>
<div class="sales-summary-box">
<div class="summary-row"><span>المخزون المتاح</span><strong id="stockPreview"></strong></div>
<div class="summary-row"><span>الإجمالي قبل الضريبة</span><strong id="subtotalPreview">0.00 ر.س</strong></div>
<div class="summary-row"><span>ضريبة القيمة المضافة 15%</span><strong id="vatPreview">0.00 ر.س</strong></div>
<div class="summary-row grand"><span>الإجمالي النهائي</span><strong id="grandPreview">0.00 ر.س</strong></div>
</div>
<div><label class="form-label">ملاحظات</label><textarea class="form-control" name="notes" rows="3"></textarea></div>
<button class="btn btn-dark" type="submit">إنشاء المستند</button>
</form>
</div>
</div>
<div class="col-xl-7">
<div class="panel-card mb-4">
<div class="section-header"><div><h2 class="section-title">سجل وثائق المبيعات</h2><p class="section-copy">المسار الآن يدعم: عرض سعر أمر بيع أمر تسليم / فاتورة.</p></div></div>
<?php if ($documents): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>الرقم</th><th>النوع</th><th>العميل</th><th>الصنف</th><th>الإجمالي</th><th>المصدر</th><th>الطباعة</th></tr></thead>
<tbody>
<?php foreach ($documents as $document): $payload = $document['payload_data']; ?>
<tr>
<td><a class="table-link" href="sales_orders.php?id=<?= (int)$document['id'] ?>"><?= e($document['code']) ?></a><div class="small text-secondary"><?= e(order_status_label((string)$document['status'])) ?></div></td>
<td><?= e(sales_document_label((string)$document['record_type'])) ?></td>
<td><?= e($payload['customer_name'] ?? '') ?><div class="small text-secondary"><?= e($payload['branch'] ?? '') ?></div></td>
<td><?= e($payload['product_name'] ?? '') ?><div class="small text-secondary"><?= e((string)($payload['qty'] ?? 0)) ?> <?= e($payload['unit'] ?? '') ?></div></td>
<td><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></td>
<td><?= e($payload['source_document_code'] ?? '—') ?></td>
<td><a class="btn btn-sm btn-outline-secondary" href="print_order.php?id=<?= (int)$document['id'] ?>" target="_blank">طباعة</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد وثائق مبيعات بعد.</div>
<?php endif; ?>
</div>
<div class="panel-card">
<div class="section-header compact"><div><h2 class="section-title">تفاصيل المستند</h2></div></div>
<?php if ($detail): $payload = $detail['payload_data']; ?>
<div class="detail-grid mb-3">
<div class="detail-block"><span class="detail-label">رقم المستند</span><strong><?= e($detail['code']) ?></strong></div>
<div class="detail-block"><span class="detail-label">النوع</span><strong><?= e(sales_document_label((string)$detail['record_type'])) ?></strong></div>
<div class="detail-block"><span class="detail-label">الحالة</span><strong><?= e(order_status_label((string)$detail['status'])) ?></strong></div>
<div class="detail-block"><span class="detail-label">العميل / الفرع</span><strong><?= e(($payload['customer_name'] ?? '') . ' — ' . ($payload['branch'] ?? '')) ?></strong></div>
<div class="detail-block"><span class="detail-label">تاريخ الإنشاء</span><strong><?= e($payload['created_date'] ?? '') ?></strong></div>
<div class="detail-block"><span class="detail-label">المصدر</span><strong><?= e(($payload['source_document_code'] ?? '') ?: 'مباشر') ?></strong></div>
</div>
<div class="subtle-card mb-3">
<div class="d-flex justify-content-between border-bottom pb-2 mb-2"><strong><?= e($payload['product_name'] ?? '') ?></strong><span><?= e($payload['sku'] ?? '') ?></span></div>
<div class="summary-row"><span>الكمية</span><strong><?= e((string)($payload['qty'] ?? 0)) ?> <?= e($payload['unit'] ?? '') ?></strong></div>
<div class="summary-row"><span>سعر الوحدة</span><strong><?= e(format_money((float)($payload['unit_price'] ?? 0))) ?></strong></div>
<div class="summary-row"><span>الإجمالي قبل الضريبة</span><strong><?= e(format_money((float)($payload['subtotal'] ?? 0))) ?></strong></div>
<div class="summary-row"><span>الضريبة</span><strong><?= e(format_money((float)($payload['vat'] ?? 0))) ?></strong></div>
<div class="summary-row grand"><span>الإجمالي النهائي</span><strong><?= e(format_money((float)($payload['grand_total'] ?? 0))) ?></strong></div>
<?php if (isset($payload['stock_after'])): ?>
<div class="summary-row"><span>المخزون بعد العملية</span><strong><?= e((string)$payload['stock_after']) ?></strong></div>
<?php endif; ?>
<?php if (!empty($payload['payment_status'])): ?>
<div class="summary-row"><span>حالة الدفع</span><strong><?= e($payload['payment_status']) ?></strong></div>
<?php endif; ?>
</div>
<div class="d-flex flex-wrap gap-2 mb-3">
<a href="print_order.php?id=<?= (int)$detail['id'] ?>" target="_blank" class="btn btn-dark">فتح صفحة الطباعة</a>
<a href="products.php?id=<?= (int)($payload['product_id'] ?? 0) ?>" class="btn btn-outline-secondary">عرض الصنف</a>
<?php foreach (sales_document_conversion_targets((string)$detail['record_type']) as $targetType): ?>
<?php if (!sales_document_child_exists((int)$detail['id'], $targetType)): ?>
<form method="post" class="d-inline-block m-0">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="form_action" value="convert_document">
<input type="hidden" name="source_id" value="<?= (int)$detail['id'] ?>">
<input type="hidden" name="target_type" value="<?= e($targetType) ?>">
<button class="btn btn-outline-secondary" type="submit">إنشاء <?= e(sales_document_label($targetType)) ?></button>
</form>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php if ($detailChildren): ?>
<div class="subtle-card mb-3">
<div class="detail-label mb-2">الوثائق المرتبطة</div>
<div class="vstack gap-2">
<?php foreach ($detailChildren as $child): $childPayload = $child['payload_data']; ?>
<div class="list-row py-0 border-0">
<div>
<strong><?= e(sales_document_label((string)$child['record_type'])) ?></strong>
<div class="small text-secondary"><?= e($child['code']) ?> — <?= e($childPayload['created_date'] ?? '') ?></div>
</div>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-outline-secondary" href="sales_orders.php?id=<?= (int)$child['id'] ?>">فتح</a>
<a class="btn btn-sm btn-outline-secondary" href="print_order.php?id=<?= (int)$child['id'] ?>" target="_blank">طباعة</a>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($payload['notes'])): ?><div class="text-secondary small"><?= e($payload['notes']) ?></div><?php endif; ?>
<?php else: ?>
<div class="empty-inline">أنشئ مستندًا جديدًا أو اختر واحدًا من الجدول لعرض تفاصيله.</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
window.erpData = {
customers: <?= json_encode($customers, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
products: <?= json_encode($products, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
};
</script>
<?php render_footer(); ?>

38
stock_movements.php Normal file
View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('stock');
$movements = fetch_records('stock_movement');
render_header('حركات المخزون', 'سجل مركزي لكل خصم وإضافة على المخزون من المبيعات والمشتريات.', 'stock');
?>
<div class="panel-card">
<div class="section-header">
<div>
<h1 class="section-title">سجل حركات المخزون</h1>
<p class="section-copy">كل حركة تعرض الكمية قبل وبعد العملية مع المرجع المرتبط بها.</p>
</div>
</div>
<?php if ($movements): ?>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>المرجع</th><th>الصنف</th><th>نوع الحركة</th><th>التغيير</th><th>قبل / بعد</th><th>المنشئ</th></tr></thead>
<tbody>
<?php foreach ($movements as $movement): $payload = $movement['payload_data']; $delta = (float)($payload['qty_change'] ?? 0); ?>
<tr>
<td><div class="fw-semibold"><?= e($payload['reference_code'] ?? $movement['code']) ?></div><div class="small text-secondary"><?= e(substr((string)($movement['created_at'] ?? ''), 0, 16)) ?></div></td>
<td><?= e($payload['product_name'] ?? '') ?><div class="small text-secondary"><?= e($payload['sku'] ?? '') ?></div></td>
<td><?= e($payload['movement_type'] ?? '') ?></td>
<td><span class="badge <?= e(movement_badge_class($delta)) ?>"><?= e(movement_direction_label($delta)) ?></span><div class="small mt-1"><?= e((string)$delta) ?> <?= e($payload['unit'] ?? '') ?></div></td>
<td><?= e((string)($payload['qty_before'] ?? 0)) ?> → <?= e((string)($payload['qty_after'] ?? 0)) ?></td>
<td><?= e($payload['created_by'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="empty-inline">لا توجد حركات مخزون بعد. أنشئ أمر شراء أو أمر بيع لتظهر هنا.</div>
<?php endif; ?>
</div>
<?php render_footer(); ?>

108
suppliers.php Normal file
View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
require_permission('suppliers');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$name = trim((string)($_POST['name'] ?? ''));
$phone = trim((string)($_POST['phone'] ?? ''));
$email = trim((string)($_POST['email'] ?? ''));
$suppliedRaw = trim((string)($_POST['supplied_skus'] ?? ''));
$notes = trim((string)($_POST['notes'] ?? ''));
$suppliedSkus = parse_csv_list($suppliedRaw);
if ($name === '') {
$errors[] = 'اسم المورد مطلوب.';
}
if (!$errors) {
create_record('supplier', $name, next_code('SUP', 'supplier'), [
'phone' => $phone,
'email' => $email,
'supplied_skus' => $suppliedSkus,
'notes' => $notes,
]);
set_flash('success', 'تمت إضافة المورد بنجاح.');
redirect('suppliers.php');
}
}
$suppliers = fetch_records('supplier');
$detail = isset($_GET['id']) ? fetch_record('supplier', (int)$_GET['id']) : null;
render_header('إدارة الموردين', 'إضافة الموردين وربط الأصناف التي يوردونها مع قسم المشتريات.', 'suppliers');
?>
<div class="row g-4">
<div class="col-lg-5">
<div class="panel-card">
<div class="section-header compact">
<div>
<h1 class="section-title mb-1">إضافة مورد</h1>
<p class="section-copy">عرّف المورد وبياناته والأصناف التي يوردها لربطه لاحقًا بأوامر الشراء.</p>
</div>
</div>
<?php if ($errors): ?>
<div class="alert alert-danger py-2"><?php foreach ($errors as $error): ?><div><?= e($error) ?></div><?php endforeach; ?></div>
<?php endif; ?>
<form method="post" class="vstack gap-3">
<input type="hidden" name="csrf_token" value="<?= e(csrf_token()) ?>">
<div>
<label class="form-label">اسم المورد</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="row g-3">
<div class="col-md-6"><label class="form-label">الهاتف</label><input type="text" class="form-control" name="phone"></div>
<div class="col-md-6"><label class="form-label">البريد الإلكتروني</label><input type="email" class="form-control" name="email"></div>
</div>
<div>
<label class="form-label">الأصناف الموردة</label>
<input type="text" class="form-control" name="supplied_skus" placeholder="RAW-001, P-100">
<div class="form-text">افصل الـ SKU بفاصلة.</div>
</div>
<div>
<label class="form-label">ملاحظات</label>
<textarea class="form-control" name="notes" rows="3"></textarea>
</div>
<button class="btn btn-dark" type="submit">حفظ المورد</button>
</form>
</div>
</div>
<div class="col-lg-7">
<div class="panel-card mb-4">
<div class="section-header"><div><h2 class="section-title">سجل الموردين</h2><p class="section-copy">اعرض الموردين وربطهم بالأصناف الموردة.</p></div></div>
<div class="table-responsive">
<table class="table align-middle app-table">
<thead><tr><th>المورد</th><th>التواصل</th><th>الأصناف</th><th>تفاصيل</th></tr></thead>
<tbody>
<?php foreach ($suppliers as $supplier): $payload = $supplier['payload_data']; ?>
<tr>
<td><div class="fw-semibold"><?= e($supplier['title']) ?></div><div class="small text-secondary"><?= e($supplier['code']) ?></div></td>
<td><?= e($payload['phone'] ?? '') ?><div class="small text-secondary"><?= e($payload['email'] ?? '') ?></div></td>
<td><?= e(implode('، ', $payload['supplied_skus'] ?? [])) ?></td>
<td><a class="btn btn-sm btn-outline-secondary" href="suppliers.php?id=<?= (int)$supplier['id'] ?>">عرض</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="panel-card">
<div class="section-header compact"><div><h2 class="section-title">تفاصيل المورد</h2></div></div>
<?php if ($detail): $payload = $detail['payload_data']; ?>
<div class="detail-grid mb-3">
<div class="detail-block"><span class="detail-label">اسم المورد</span><strong><?= e($detail['title']) ?></strong></div>
<div class="detail-block"><span class="detail-label">الرقم</span><strong><?= e($detail['code']) ?></strong></div>
<div class="detail-block"><span class="detail-label">الهاتف</span><strong><?= e($payload['phone'] ?? '') ?></strong></div>
<div class="detail-block"><span class="detail-label">البريد</span><strong><?= e($payload['email'] ?? '') ?></strong></div>
</div>
<div class="subtle-card"><div class="detail-label mb-2">الأصناف الموردة</div><div><?= e(implode('، ', $payload['supplied_skus'] ?? [])) ?: '—' ?></div></div>
<div class="mt-3 text-secondary small"><?= e($payload['notes'] ?? '') ?></div>
<?php else: ?>
<div class="empty-inline">اختر موردًا من الجدول لعرض التفاصيل.</div>
<?php endif; ?>
</div>
</div>
</div>
<?php render_footer(); ?>