Commit d7e46851 authored by DevPilot's avatar DevPilot

chore(accounting): تنضيف بقايا شاشات كشف الحساب القديمة + ضبط دقة البحث

- شاشتين وميثودين وداليتين قراءة بقوا ميتين بعد توحيد كشوف الحسابات.
  AccountStatementService فضلت لتسجيل الحركات (١٦ مكان بيستخدمها).
- البحث بالحروف المتتابعة كان فضفاض: «recon» كانت بتلاقي «شئون العضوية»
  لأن حروفها متفرقة في النص. اتضاف شرط تقارب — الحروف لازم تكون في مدى
  ضعف طول اللي اتكتب.
parent 7783bd1e
......@@ -7,71 +7,14 @@ use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Accounting\Services\AccountStatementService;
class AccountStatementController extends Controller
{
public function customerStatement(Request $request): Response
{
$this->authorize('accounting.statements.view');
$db = App::getInstance()->db();
$memberId = (int) $request->get('member_id', 0);
$dateFrom = $request->get('date_from', '');
$dateTo = $request->get('date_to', '');
$statement = null;
$member = null;
if ($memberId > 0) {
$member = $db->selectOne("SELECT id, full_name_ar, membership_number, credit_limit, credit_balance FROM members WHERE id = ?", [$memberId]);
$statement = AccountStatementService::getCustomerStatement(
$memberId,
$dateFrom ?: null,
$dateTo ?: null
);
}
$members = $db->select("SELECT id, full_name_ar, membership_number FROM members WHERE is_archived = 0 ORDER BY full_name_ar LIMIT 1000");
return $this->view('Accounting.Views.statements.customer', [
'members' => $members,
'member' => $member,
'statement' => $statement,
'filters' => ['member_id' => $memberId, 'date_from' => $dateFrom, 'date_to' => $dateTo],
]);
}
public function supplierStatement(Request $request): Response
{
$this->authorize('accounting.statements.view');
$db = App::getInstance()->db();
$supplierId = (int) $request->get('supplier_id', 0);
$dateFrom = $request->get('date_from', '');
$dateTo = $request->get('date_to', '');
$statement = null;
$supplier = null;
if ($supplierId > 0) {
$supplier = $db->selectOne("SELECT id, name_ar, code, credit_limit, credit_balance FROM suppliers WHERE id = ?", [$supplierId]);
$statement = AccountStatementService::getSupplierStatement(
$supplierId,
$dateFrom ?: null,
$dateTo ?: null
);
}
$suppliers = $db->select("SELECT id, name_ar, code FROM suppliers WHERE is_archived = 0 ORDER BY name_ar");
return $this->view('Accounting.Views.statements.supplier', [
'suppliers' => $suppliers,
'supplier' => $supplier,
'statement' => $statement,
'filters' => ['supplier_id' => $supplierId, 'date_from' => $dateFrom, 'date_to' => $dateTo],
]);
}
/*
* كشف حساب العميل والمورد اتنقلوا لـ PartyStatementController — شاشة
* واحدة موحّدة بتقارن دفتر الأستاذ بالحساب المساعد. الروابط القديمة
* بتحوّل عليها، والكلاس ده فضل للنقدية اليومية بس.
*/
public function dailyCash(Request $request): Response
{
......
......@@ -7,111 +7,7 @@ use App\Core\App;
final class AccountStatementService
{
public static function getCustomerStatement(int $memberId, ?string $dateFrom = null, ?string $dateTo = null): array
{
$db = App::getInstance()->db();
$where = 'member_id = ?';
$params = [$memberId];
if ($dateFrom) {
$where .= ' AND transaction_date >= ?';
$params[] = $dateFrom;
}
if ($dateTo) {
$where .= ' AND transaction_date <= ?';
$params[] = $dateTo;
}
$transactions = $db->select(
"SELECT * FROM customer_transactions WHERE {$where} ORDER BY transaction_date ASC, id ASC",
$params
);
$runningBalance = '0.00';
if ($dateFrom) {
$priorRow = $db->selectOne(
"SELECT COALESCE(SUM(debit) - SUM(credit), 0) as balance
FROM customer_transactions WHERE member_id = ? AND transaction_date < ?",
[$memberId, $dateFrom]
);
$runningBalance = (string) ($priorRow['balance'] ?? '0.00');
}
$result = [];
foreach ($transactions as $tx) {
$runningBalance = bcadd(bcsub($runningBalance, (string) $tx['credit'], 2), (string) $tx['debit'], 2);
$tx['running_balance'] = $runningBalance;
$result[] = $tx;
}
$totalDebit = '0.00';
$totalCredit = '0.00';
foreach ($transactions as $tx) {
$totalDebit = bcadd($totalDebit, (string) $tx['debit'], 2);
$totalCredit = bcadd($totalCredit, (string) $tx['credit'], 2);
}
return [
'transactions' => $result,
'opening_balance' => $dateFrom ? (string) ($priorRow['balance'] ?? '0.00') : '0.00',
'total_debit' => $totalDebit,
'total_credit' => $totalCredit,
'closing_balance' => $runningBalance,
];
}
public static function getSupplierStatement(int $supplierId, ?string $dateFrom = null, ?string $dateTo = null): array
{
$db = App::getInstance()->db();
$where = 'supplier_id = ?';
$params = [$supplierId];
if ($dateFrom) {
$where .= ' AND transaction_date >= ?';
$params[] = $dateFrom;
}
if ($dateTo) {
$where .= ' AND transaction_date <= ?';
$params[] = $dateTo;
}
$transactions = $db->select(
"SELECT * FROM supplier_transactions WHERE {$where} ORDER BY transaction_date ASC, id ASC",
$params
);
$runningBalance = '0.00';
if ($dateFrom) {
$priorRow = $db->selectOne(
"SELECT COALESCE(SUM(credit) - SUM(debit), 0) as balance
FROM supplier_transactions WHERE supplier_id = ? AND transaction_date < ?",
[$supplierId, $dateFrom]
);
$runningBalance = (string) ($priorRow['balance'] ?? '0.00');
}
$result = [];
foreach ($transactions as $tx) {
$runningBalance = bcadd(bcsub($runningBalance, (string) $tx['debit'], 2), (string) $tx['credit'], 2);
$tx['running_balance'] = $runningBalance;
$result[] = $tx;
}
$totalDebit = '0.00';
$totalCredit = '0.00';
foreach ($transactions as $tx) {
$totalDebit = bcadd($totalDebit, (string) $tx['debit'], 2);
$totalCredit = bcadd($totalCredit, (string) $tx['credit'], 2);
}
return [
'transactions' => $result,
'opening_balance' => $dateFrom ? (string) ($priorRow['balance'] ?? '0.00') : '0.00',
'total_debit' => $totalDebit,
'total_credit' => $totalCredit,
'closing_balance' => $runningBalance,
];
}
public static function recordCustomerTransaction(array $data): int
{
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>كشف حساب عميل<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" action="/accounting/statements/customer" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">العضو</label>
<select name="member_id" class="form-select">
<option value="">— اختر عضو —</option>
<?php foreach ($members as $m): ?>
<option value="<?= (int)$m['id'] ?>" <?= (int)($filters['member_id'] ?? 0) === (int)$m['id'] ? 'selected' : '' ?>><?= e($m['full_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" class="form-input" value="<?= e($filters['date_from'] ?? '') ?>" dir="ltr">
</div>
<div>
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" class="form-input" value="<?= e($filters['date_to'] ?? '') ?>" dir="ltr">
</div>
<button type="submit" class="btn btn-primary">عرض</button>
</form>
</div>
</div>
<?php if (!empty($member) && !empty($statement)): ?>
<!-- Statement -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;">كشف حساب: <?= e($member['full_name_ar']) ?></h3>
<button type="button" class="btn btn-outline" onclick="window.print()">طباعة</button>
</div>
<!-- Opening Balance -->
<div style="padding:10px 20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<strong>الرصيد الافتتاحي:</strong>
<span style="font-weight:700;direction:ltr;display:inline-block;"><?= money($statement['opening_balance'] ?? 0) ?></span>
</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>التاريخ</th>
<th>نوع المستند</th>
<th>البيان</th>
<th>مدين</th>
<th>دائن</th>
<th>الرصيد</th>
</tr>
</thead>
<tbody>
<?php foreach ($statement['entries'] ?? [] as $entry): ?>
<tr>
<td style="direction:ltr;text-align:right;"><?= e($entry['date']) ?></td>
<td><?= e($entry['doc_type'] ?? '—') ?></td>
<td><?= e($entry['description'] ?? '') ?></td>
<td style="direction:ltr;text-align:right;color:#DC2626;"><?= (float)($entry['debit'] ?? 0) > 0 ? money($entry['debit']) : '' ?></td>
<td style="direction:ltr;text-align:right;color:#059669;"><?= (float)($entry['credit'] ?? 0) > 0 ? money($entry['credit']) : '' ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($entry['balance'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($statement['entries'])): ?>
<tr><td colspan="6" style="text-align:center;color:#6B7280;padding:30px;">لا توجد حركات</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr style="font-weight:700;background:#F9FAFB;">
<td colspan="3">الإجمالي</td>
<td style="direction:ltr;text-align:right;color:#DC2626;"><?= money($statement['total_debit'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;color:#059669;"><?= money($statement['total_credit'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;"><?= money($statement['closing_balance'] ?? 0) ?></td>
</tr>
</tfoot>
</table>
</div>
<!-- Closing Balance -->
<div style="padding:10px 20px;background:#F0FDF4;border-top:1px solid #E5E7EB;">
<strong>الرصيد الختامي:</strong>
<span style="font-weight:700;direction:ltr;display:inline-block;"><?= money($statement['closing_balance'] ?? 0) ?></span>
</div>
</div>
<?php elseif (!empty($filters['member_id'])): ?>
<div class="card">
<div style="padding:30px;text-align:center;color:#6B7280;">لا توجد بيانات للعرض</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>كشف حساب مورد<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;">
<form method="GET" action="/accounting/statements/supplier" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">المورد</label>
<select name="supplier_id" class="form-select">
<option value="">— اختر مورد —</option>
<?php foreach ($suppliers as $s): ?>
<option value="<?= (int)$s['id'] ?>" <?= (int)($filters['supplier_id'] ?? 0) === (int)$s['id'] ? 'selected' : '' ?>><?= e($s['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" class="form-input" value="<?= e($filters['date_from'] ?? '') ?>" dir="ltr">
</div>
<div>
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" class="form-input" value="<?= e($filters['date_to'] ?? '') ?>" dir="ltr">
</div>
<button type="submit" class="btn btn-primary">عرض</button>
</form>
</div>
</div>
<?php if (!empty($supplier) && !empty($statement)): ?>
<!-- Statement -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;">كشف حساب: <?= e($supplier['name_ar']) ?></h3>
<button type="button" class="btn btn-outline" onclick="window.print()">طباعة</button>
</div>
<!-- Opening Balance -->
<div style="padding:10px 20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<strong>الرصيد الافتتاحي:</strong>
<span style="font-weight:700;direction:ltr;display:inline-block;"><?= money($statement['opening_balance'] ?? 0) ?></span>
</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>التاريخ</th>
<th>نوع المستند</th>
<th>البيان</th>
<th>مدين</th>
<th>دائن</th>
<th>الرصيد</th>
</tr>
</thead>
<tbody>
<?php foreach ($statement['entries'] ?? [] as $entry): ?>
<tr>
<td style="direction:ltr;text-align:right;"><?= e($entry['date']) ?></td>
<td><?= e($entry['doc_type'] ?? '—') ?></td>
<td><?= e($entry['description'] ?? '') ?></td>
<td style="direction:ltr;text-align:right;color:#DC2626;"><?= (float)($entry['debit'] ?? 0) > 0 ? money($entry['debit']) : '' ?></td>
<td style="direction:ltr;text-align:right;color:#059669;"><?= (float)($entry['credit'] ?? 0) > 0 ? money($entry['credit']) : '' ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($entry['balance'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($statement['entries'])): ?>
<tr><td colspan="6" style="text-align:center;color:#6B7280;padding:30px;">لا توجد حركات</td></tr>
<?php endif; ?>
</tbody>
<tfoot>
<tr style="font-weight:700;background:#F9FAFB;">
<td colspan="3">الإجمالي</td>
<td style="direction:ltr;text-align:right;color:#DC2626;"><?= money($statement['total_debit'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;color:#059669;"><?= money($statement['total_credit'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;"><?= money($statement['closing_balance'] ?? 0) ?></td>
</tr>
</tfoot>
</table>
</div>
<!-- Closing Balance -->
<div style="padding:10px 20px;background:#F0FDF4;border-top:1px solid #E5E7EB;">
<strong>الرصيد الختامي:</strong>
<span style="font-weight:700;direction:ltr;display:inline-block;"><?= money($statement['closing_balance'] ?? 0) ?></span>
</div>
</div>
<?php elseif (!empty($filters['supplier_id'])): ?>
<div class="card">
<div style="padding:30px;text-align:center;color:#6B7280;">لا توجد بيانات للعرض</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
......@@ -94,8 +94,10 @@
if (words[i].indexOf(query) === 0) return 700;
}
// كل حروف ما كتبه بالترتيب جوه النص (زي بحث الملفات في المحررات)
if (subsequence(query, haystack)) best = 400;
// كل حروف ما كتبه بالترتيب جوه النص (زي بحث الملفات في المحررات).
// بشرط إنها تبقى متقاربة — من غير الشرط ده «recon» بتلاقي أي نص
// فيه الحروف دي متفرقة على أربعين حرف، والنتايج بتبقى ضوضاء.
if (query.length >= 3 && subsequence(query, haystack)) best = 400;
// مطابقة تقريبية بالكلمة — بتمسك الأخطاء المطبعية
var tol = tolerance(query.length);
......@@ -113,12 +115,21 @@
return best;
}
/**
* هل حروف needle موجودة بالترتيب في hay، ومتقاربة؟
* المسافة المسموحة بين أول وآخر حرف = ضعف طول ما اتكتب + ٤.
*/
function subsequence(needle, hay) {
var i = 0;
var i = 0, first = -1, last = -1;
for (var j = 0; j < hay.length && i < needle.length; j++) {
if (hay[j] === needle[i]) i++;
if (hay[j] === needle[i]) {
if (first === -1) first = j;
last = j;
i++;
}
}
return i === needle.length;
if (i !== needle.length) return false;
return (last - first + 1) <= needle.length * 2 + 4;
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment