Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b3e4e72af |
340
accounting.php
Normal file
340
accounting.php
Normal 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(); ?>
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
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;
|
||||
});
|
||||
|
||||
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 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
205
customers.php
Normal 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 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
20
healthz.php
Normal 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
387
index.php
@ -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>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
<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
58
login.php
Normal 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
7
logout.php
Normal 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
241
manufacturing.php
Normal 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
109
print_order.php
Normal 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
194
products.php
Normal 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
171
purchases.php
Normal 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
263
reports.php
Normal 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
375
sales_orders.php
Normal 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
38
stock_movements.php
Normal 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
108
suppliers.php
Normal 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(); ?>
|
||||
Loading…
x
Reference in New Issue
Block a user