# 🚀 دليل البدء السريع - نسخ النظام إلى نظامك **خريطة الطريق الفعلية خطوة بخطوة** --- ## 📅 المرحلة 1: إعداد البيانات (الأسبوع الأول) ### الخطوة 1: أنشئ الجداول **انسخ هذه الأوامر:** ```sql -- 1. جدول العقود الرئيسي CREATE TABLE contracts ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL, guarantor_id BIGINT, vault_id BIGINT, contract_date DATE NOT NULL, original_price DECIMAL(15,2) NOT NULL, installment_price DECIMAL(15,2) NOT NULL, installment_count INT NOT NULL, installment_value DECIMAL(15,2), advance_payment DECIMAL(15,2) DEFAULT 0, first_installment_date DATE, installment_interval INT DEFAULT 30, profit_percentage DECIMAL(5,2), status ENUM('active','finished','suspended','cashed') DEFAULT 'active', notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(customer_id) REFERENCES customers(id), FOREIGN KEY(vault_id) REFERENCES vaults(id), INDEX idx_status (status), INDEX idx_customer (customer_id), INDEX idx_date (contract_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 2. جدول الأقساط التفصيلية CREATE TABLE installments ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_id BIGINT NOT NULL, due_date DATE NOT NULL, value DECIMAL(15,2) NOT NULL, paid_amount DECIMAL(15,2) DEFAULT 0, payment_date DATE, is_paid BOOLEAN DEFAULT FALSE, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(contract_id) REFERENCES contracts(id) ON DELETE CASCADE, INDEX idx_contract (contract_id), INDEX idx_due_date (due_date), INDEX idx_is_paid (is_paid), UNIQUE KEY uk_contract_date (contract_id, due_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 3. جدول المدفوعات CREATE TABLE payments ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL, contract_id BIGINT, installment_id BIGINT, vault_id BIGINT, amount DECIMAL(15,2) NOT NULL, payment_date DATE NOT NULL, payment_method VARCHAR(50), notes TEXT, status ENUM('completed','pending','failed') DEFAULT 'completed', created_by BIGINT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(customer_id) REFERENCES customers(id), FOREIGN KEY(contract_id) REFERENCES contracts(id), FOREIGN KEY(vault_id) REFERENCES vaults(id), INDEX idx_contract (contract_id), INDEX idx_date (payment_date), INDEX idx_customer (customer_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 4. جدول التكييش والتدقيق CREATE TABLE contract_cashouts ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_id BIGINT NOT NULL, payment_id BIGINT, executed_by BIGINT, cashout_type ENUM('simple','overdue_settlement'), total_amount DECIMAL(15,2) NOT NULL, discount_profit DECIMAL(15,2), overdue_amount DECIMAL(15,2), future_amount DECIMAL(15,2), overdue_count INT, future_count INT, cashout_date DATE, snapshot LONGTEXT, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(contract_id) REFERENCES contracts(id), FOREIGN KEY(payment_id) REFERENCES payments(id), INDEX idx_contract (contract_id), INDEX idx_date (cashout_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` ### الخطوة 2: تحقق من الجداول ```sql -- تأكد من الإنشاء SHOW TABLES LIKE '%contract%'; SHOW TABLES LIKE '%installment%'; SHOW TABLES LIKE '%payment%'; ``` --- ## 💻 المرحلة 2: البرمجة (الأسبوع الثاني) ### الخطوة 1: أنشئ Models #### Laravel: ```php // app/Models/Contract.php hasMany(Installment::class); } public function payments() { return $this->hasMany(Payment::class); } public function cashouts() { return $this->hasMany(ContractCashout::class); } // الخاصية المحسوبة public function getTotalPaidAttribute() { return $this->payments()->where('status', 'completed')->sum('amount'); } public function getUnpaidTotalAttribute() { return $this->installment_price - $this->total_paid; } } // app/Models/Installment.php belongsTo(Contract::class); } } // app/Models/Payment.php belongsTo(Contract::class); } } // app/Models/ContractCashout.php 'array', ]; public function contract() { return $this->belongsTo(Contract::class); } } ``` ### الخطوة 2: أنشئ الـ Service ```php // app/Services/ContractCashoutService.php startOfDay() : now()->startOfDay(); $profitTotal = max($contract->installment_price - $contract->original_price, 0); $profitPerInstallment = round($profitTotal / max($contract->installment_count, 1), 2); $unpaidInstallments = $contract->installments() ->where('is_paid', false) ->orderBy('due_date') ->get(); $overdueAmount = 0; $futureAmount = 0; $discountAmount = 0; $futureCount = 0; $overdueCount = 0; foreach ($unpaidInstallments as $inst) { $remaining = max($inst->value - ($inst->paid_amount ?? 0), 0); if ($remaining <= 0) continue; $isFuture = \Carbon\Carbon::parse($inst->due_date)->startOfDay()->gte($asOfDate); if ($isFuture) { $discount = min($profitPerInstallment, $remaining); $futureAmount += $remaining - $discount; $discountAmount += $discount; $futureCount++; } else { if ($includeOverdue) $overdueAmount += $remaining; $overdueCount++; } } return [ 'cashout_total' => round($overdueAmount + $futureAmount, 2), 'discount_total' => round($discountAmount, 2), 'future_amount' => round($futureAmount, 2), 'overdue_amount' => round($overdueAmount, 2), 'future_count' => $futureCount, 'overdue_count' => $overdueCount, 'profit_per_installment' => $profitPerInstallment, ]; } public function execute( Contract $contract, $vaultId, $paymentMethod = 'cash', $notes = null, $paymentDate = null, $includeOverdue = false ) { return DB::transaction(function () use ($contract, $vaultId, $paymentMethod, $notes, $paymentDate, $includeOverdue) { $lockedContract = Contract::lockForUpdate()->find($contract->id); $snapshot = $this->calculate($lockedContract, $paymentDate, $includeOverdue); if ($snapshot['cashout_total'] <= 0) { throw new \Exception('لا يوجد مبلغ متبقي'); } // تحديث الأقساط $futureInstallments = $lockedContract->installments() ->where('is_paid', false) ->where('due_date', '>=', $paymentDate ?? now()) ->lockForUpdate() ->get(); foreach ($futureInstallments as $inst) { $remaining = max($inst->value - ($inst->paid_amount ?? 0), 0); $discount = min($snapshot['profit_per_installment'], $remaining); $inst->update(['value' => max($inst->value - $discount, 0)]); } // إنشاء الدفع والتكييش $payment = Payment::create([ 'customer_id' => $lockedContract->customer_id, 'contract_id' => $contract->id, 'vault_id' => $vaultId, 'amount' => $snapshot['cashout_total'], 'payment_date' => $paymentDate ?? now(), 'payment_method' => $paymentMethod, 'status' => 'completed', 'notes' => "تكييش - خصم {$snapshot['discount_total']}" ]); ContractCashout::create([ 'contract_id' => $contract->id, 'payment_id' => $payment->id, 'cashout_type' => $includeOverdue ? 'overdue_settlement' : 'simple', 'total_amount' => $snapshot['cashout_total'], 'discount_profit' => $snapshot['discount_total'], 'overdue_amount' => $snapshot['overdue_amount'], 'future_amount' => $snapshot['future_amount'], 'overdue_count' => $snapshot['overdue_count'], 'future_count' => $snapshot['future_count'], 'cashout_date' => $paymentDate ?? now(), 'snapshot' => $snapshot, ]); if (!$lockedContract->installments()->where('is_paid', false)->exists()) { $lockedContract->update(['status' => 'cashed']); } return $payment; }); } } ``` --- ## 🎬 المرحلة 3: الواجهات (الأسبوع الثالث) ### Filament Resource (Laravel) ```php // app/Filament/Resources/Contracts/Pages/ViewContract.php label('تكييش العقد') ->icon('heroicon-o-banknote') ->color('success') ->form([ \Filament\Forms\Components\Select::make('vault_id') ->label('الخزينة') ->required(), \Filament\Forms\Components\TextInput::make('payment_method') ->label('طريقة الدفع') ->default('cash'), \Filament\Forms\Components\DatePicker::make('payment_date') ->label('تاريخ الدفع'), ]) ->action(function (array $data) { try { $service = app(ContractCashoutService::class); $payment = $service->execute( $this->record, $data['vault_id'], $data['payment_method'], paymentDate: $data['payment_date'] ?? null, includeOverdue: false ); Notification::make()->success()->title('تم التكييش بنجاح')->send(); redirect(route('filament.admin.resources.contracts.view', $payment->contract_id)); } catch (\Exception $e) { Notification::make()->danger()->title($e->getMessage())->send(); } }), Action::make('cashout_overdue') ->label('تسوية متأخرات + تكييش') ->icon('heroicon-o-calculator') ->color('info') ->form([ \Filament\Forms\Components\Select::make('vault_id')->label('الخزينة')->required(), \Filament\Forms\Components\TextInput::make('payment_method')->label('الطريقة')->default('cash'), ]) ->action(function (array $data) { try { $service = app(ContractCashoutService::class); $payment = $service->execute( $this->record, $data['vault_id'], $data['payment_method'], includeOverdue: true ); Notification::make()->success()->title('تم التسوية والتكييش')->send(); } catch (\Exception $e) { Notification::make()->danger()->title($e->getMessage())->send(); } }), ]; } } ``` --- ## 📊 المرحلة 4: التقارير ### Query الأقساط المتبقية ```sql -- الأقساط المستحقة اليوم SELECT c.id, c.customer_id, i.due_date, i.value, i.paid_amount FROM installments i JOIN contracts c ON i.contract_id = c.id WHERE i.due_date = CURDATE() AND i.is_paid = 0 ORDER BY i.due_date; -- المتأخرات SELECT c.id, c.customer_id, COUNT(*) as count, SUM(i.value - i.paid_amount) as total FROM installments i JOIN contracts c ON i.contract_id = c.id WHERE i.due_date < CURDATE() AND i.is_paid = 0 GROUP BY c.id ORDER BY total DESC; -- الأقساط المكيشة SELECT c.id, cc.cashout_date, cc.total_amount, cc.discount_profit FROM contract_cashouts cc JOIN contracts c ON cc.contract_id = c.id WHERE MONTH(cc.cashout_date) = MONTH(CURDATE()) ORDER BY cc.cashout_date DESC; ``` ### Query الإيرادات ```sql -- إجمالي الأرباح المنتظرة SELECT SUM(installment_price - original_price) as total_profit FROM contracts WHERE status IN ('active', 'finished'); -- الأرباح المحققة SELECT SUM(discount_profit) as realized_profit FROM contract_cashouts WHERE MONTH(cashout_date) = MONTH(CURDATE()); -- الأرباح المعلقة SELECT SUM(i.value - i.paid_amount) as pending_profit FROM installments i WHERE i.is_paid = 0; ``` --- ## ✅ اختبار شامل ### أسرع اختبار: ``` 1. إنشاء عقد جديد: - عميل: أحمد - السعر: 100,000 - مع الربح: 140,000 - 10 أقساط 2. دفع 6 أقساط (اترك 4 أقساط) 3. اضغط "تكييش العقد" 4. تحقق من: ✓ تحديث الأقساط بقيم جديدة ✓ إنشاء سجل في payments ✓ إنشاء سجل في contract_cashouts ✓ حالة العقد تغيرت إلى cashed ``` --- ## 🐛 استكشاف الأخطاء | المشكلة | الحل | |--------|-----| | معي خطأ قاعدة بيانات | تأكد من أسماء الجداول والأعمدة | | الحسابات غير صحيحة | تحقق من profit_per_installment | | العقد لا يتحول إلى cashed | تأكد من أن جميع الأقساط مدفوعة/محذوفة | | لا يظهر الزر | تحقق من Permissions والـ Roles | --- ## 📞 جاهز للأسئلة؟ - **أسألني عن:** أي خطأ تواجهه - **أسألني عن:** شرح أي جزء - **أسألني عن:** إضافة feature جديدة - **أسألني عن:** دمج مع نظام آخر --- **✨ هذا هو كل ما تحتاجه للبدء!**