Commit e6a79887 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(rentals): add VAT, utilities, late fees, and monthly invoices

- Contracts now store vat_percentage (1%), utilities_percentage, late_fee_type
  (none/daily/weekly/monthly), late_fee_rate, and grand_total
- New rental_invoices table with per-invoice breakdown: base, utilities, VAT,
  late_fee, total; late fee is calculated at payment time based on days overdue
- RentalInvoiceService handles generation, late-fee calc, and mark-paid
- Accounting auto-posts on rental.invoice_paid: Dr. Cash, Cr. RentalRevenue
  (410521) + ServiceRevenue (410515) + TaxPayable (230804) + FineRevenue (410512)
- contract_form has live preview calculator for monthly invoice totals
- contract_show shows full financial breakdown and invoices table
- Migrations: Phase_95_001 (alter contracts), Phase_95_002 (create invoices)
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent ba9d9f81
......@@ -775,6 +775,120 @@ final class AccountingIntegrationService
}
}
/**
* Post journal entry when a rental invoice is paid.
*
* Dr. Cash / Bank (base + utilities + late_fee + vat) — full payment received
* Cr. Rental Revenue (410521) base amount
* Cr. Service Revenue (410515) utilities amount (if > 0)
* Cr. Tax Payable (230804) VAT amount
* Cr. Fine Revenue (410512) late fee amount (if > 0)
*/
public static function onRentalInvoicePaid(array $data): void
{
$db = App::getInstance()->db();
$invoiceId = (int) ($data['invoice_id'] ?? 0);
$contractId = (int) ($data['contract_id'] ?? 0);
$paymentId = (int) ($data['payment_id'] ?? 0);
$baseAmount = (string) ($data['base_amount'] ?? '0.00');
$utilsAmount = (string) ($data['utilities_amount'] ?? '0.00');
$vatAmount = (string) ($data['vat_amount'] ?? '0.00');
$lateFee = (string) ($data['late_fee_amount'] ?? '0.00');
$totalAmount = (string) ($data['total_amount'] ?? '0.00');
if ($invoiceId <= 0 || bccomp($totalAmount, '0.00', 2) <= 0) {
return;
}
$invoice = $db->selectOne("SELECT * FROM rental_invoices WHERE id = ?", [$invoiceId]);
$contract = $db->selectOne("SELECT * FROM rental_contracts WHERE id = ?", [$contractId]);
$invoiceNum = $invoice ? ($invoice['invoice_number'] ?? $invoiceId) : $invoiceId;
$contractNum = $contract ? ($contract['contract_number'] ?? $contractId) : $contractId;
// Determine debit account — try to match payment method from payments table
$payment = $paymentId > 0 ? $db->selectOne("SELECT * FROM payments WHERE id = ?", [$paymentId]) : null;
$payMethod = $payment ? ($payment['payment_method'] ?? 'cash') : 'cash';
$debitCode = AccountCodes::debitAccountForMethod($payMethod);
$debitAccount = self::getAccountByCode($debitCode);
$rentalRevenue = self::getAccountByCode(AccountCodes::RENTAL_REVENUE);
$serviceRevenue = self::getAccountByCode(AccountCodes::SERVICE_REVENUE);
$taxPayable = self::getAccountByCode(AccountCodes::TAX_PAYABLE);
$fineRevenue = self::getAccountByCode(AccountCodes::FINE_REVENUE);
if (!$debitAccount || !$rentalRevenue || !$taxPayable) {
Logger::error('Rental invoice paid: missing accounts', ['invoice_id' => $invoiceId]);
return;
}
$lines = [];
// Dr. Cash/Bank — full amount received
$lines[] = [
'account_id' => (int) $debitAccount['id'],
'debit' => $totalAmount,
'credit' => '0.00',
'description_ar' => 'تحصيل فاتورة إيجار ' . $invoiceNum,
];
// Cr. Rental Revenue (base)
if (bccomp($baseAmount, '0.00', 2) > 0) {
$lines[] = [
'account_id' => (int) $rentalRevenue['id'],
'debit' => '0.00',
'credit' => $baseAmount,
'description_ar' => 'إيجار — ' . $invoiceNum . ' — عقد ' . $contractNum,
];
}
// Cr. Service Revenue (utilities)
if (bccomp($utilsAmount, '0.00', 2) > 0 && $serviceRevenue) {
$lines[] = [
'account_id' => (int) $serviceRevenue['id'],
'debit' => '0.00',
'credit' => $utilsAmount,
'description_ar' => 'مرافق إيجار — ' . $invoiceNum,
];
}
// Cr. Tax Payable (VAT)
if (bccomp($vatAmount, '0.00', 2) > 0) {
$lines[] = [
'account_id' => (int) $taxPayable['id'],
'debit' => '0.00',
'credit' => $vatAmount,
'description_ar' => 'ضريبة قيمة مضافة — ' . $invoiceNum,
];
}
// Cr. Fine Revenue (late fee)
if (bccomp($lateFee, '0.00', 2) > 0 && $fineRevenue) {
$lines[] = [
'account_id' => (int) $fineRevenue['id'],
'debit' => '0.00',
'credit' => $lateFee,
'description_ar' => 'غرامة تأخير إيجار — ' . $invoiceNum,
];
}
$result = JournalService::createEntry([
'entry_date' => date('Y-m-d'),
'description_ar' => 'تحصيل فاتورة إيجار ' . $invoiceNum . ' — عقد ' . $contractNum,
'description_en' => 'Rental invoice collected ' . $invoiceNum,
'reference_type' => 'rental_invoice',
'reference_id' => $invoiceId,
'source_module' => 'rentals',
'is_auto_generated' => 1,
], $lines, true);
if ($result['success'] && !empty($result['entry_id'])) {
$db->update('rental_invoices', ['journal_entry_id' => (int) $result['entry_id']], 'id = ?', [$invoiceId]);
} else {
Logger::error('Rental invoice journal entry failed', ['invoice_id' => $invoiceId, 'error' => $result['error'] ?? '']);
}
}
// ────────────────────────────────────────────────────────────
// PROCUREMENT MODULE
// ────────────────────────────────────────────────────────────
......
......@@ -350,6 +350,15 @@ EventBus::listen('rental.deposit_refunded', function (array $data): void {
}
}, 50);
// When a rental invoice is paid, post: Dr. Cash, Cr. Rental Revenue + Utilities + VAT + Late Fee
EventBus::listen('rental.invoice_paid', function (array $data): void {
try {
AccountingIntegrationService::onRentalInvoicePaid($data);
} catch (\Throwable $e) {
\App\Core\Logger::error('Accounting auto-post failed (rental.invoice_paid): ' . $e->getMessage());
}
}, 50);
// ── Facility Entry ──────────────────────────────────────────
// When a facility entry payment is recorded (pool/gym access)
EventBus::listen('facility.entry_recorded', function (array $data): void {
......
......@@ -12,6 +12,8 @@ use App\Modules\Rentals\Models\RentalContract;
use App\Modules\Rentals\Models\RentalBooking;
use App\Modules\Rentals\Services\RentalContractService;
use App\Modules\Rentals\Services\RentalDepositService;
use App\Modules\Rentals\Services\RentalInvoiceService;
use App\Modules\Rentals\Models\RentalInvoice;
use App\Modules\Facilities\Models\Facility;
class RentalController extends Controller
......@@ -201,16 +203,20 @@ class RentalController extends Controller
*/
public function storeContract(Request $request): Response
{
$entityId = (int) $request->post('entity_id', 0);
$facilityId = (int) $request->post('facility_id', 0);
$activityType = trim((string) $request->post('activity_type', ''));
$startDate = trim((string) $request->post('start_date', ''));
$endDate = trim((string) $request->post('end_date', ''));
$totalUnits = (int) $request->post('total_units', 0);
$unitRate = (float) $request->post('unit_rate', 0);
$timeTier = trim((string) $request->post('time_tier', 'AM'));
$depositPercentage = (float) $request->post('deposit_percentage', 0);
$notes = trim((string) $request->post('notes', ''));
$entityId = (int) $request->post('entity_id', 0);
$facilityId = (int) $request->post('facility_id', 0);
$activityType = trim((string) $request->post('activity_type', ''));
$startDate = trim((string) $request->post('start_date', ''));
$endDate = trim((string) $request->post('end_date', ''));
$totalUnits = (int) $request->post('total_units', 0);
$unitRate = (float) $request->post('unit_rate', 0);
$timeTier = trim((string) $request->post('time_tier', 'AM'));
$depositPercentage = (float) $request->post('deposit_percentage', 0);
$vatPercentage = (float) $request->post('vat_percentage', 1.00);
$utilitiesPercentage = (float) $request->post('utilities_percentage', 0.00);
$lateFeeType = trim((string) $request->post('late_fee_type', 'none'));
$lateFeeRate = (float) $request->post('late_fee_rate', 0.00);
$notes = trim((string) $request->post('notes', ''));
// Validation
$errors = [];
......@@ -247,16 +253,20 @@ class RentalController extends Controller
}
$contract = RentalContractService::createContract([
'entity_id' => $entityId,
'facility_id' => $facilityId,
'activity_type' => $activityType ?: null,
'start_date' => $startDate,
'end_date' => $endDate,
'total_units' => $totalUnits,
'unit_rate' => $unitRate,
'time_tier' => $timeTier,
'deposit_percentage' => $depositPercentage,
'notes' => $notes ?: null,
'entity_id' => $entityId,
'facility_id' => $facilityId,
'activity_type' => $activityType ?: null,
'start_date' => $startDate,
'end_date' => $endDate,
'total_units' => $totalUnits,
'unit_rate' => $unitRate,
'time_tier' => $timeTier,
'vat_percentage' => $vatPercentage,
'utilities_percentage' => $utilitiesPercentage,
'late_fee_type' => $lateFeeType,
'late_fee_rate' => $lateFeeRate,
'deposit_percentage' => $depositPercentage,
'notes' => $notes ?: null,
]);
return $this->redirect('/rentals/contracts/' . $contract->id)->withSuccess('تم إنشاء العقد بنجاح');
......@@ -272,18 +282,20 @@ class RentalController extends Controller
return $this->redirect('/rentals')->withError('العقد غير موجود');
}
$entityId = (int) ($contract->entity_id ?? $contract['entity_id']);
$facilityId = (int) ($contract->facility_id ?? $contract['facility_id']);
$entityId = (int) $contract->entity_id;
$facilityId = (int) $contract->facility_id;
$entity = RentalEntity::find($entityId);
$facility = Facility::find($facilityId);
$bookings = RentalBooking::getForContract((int) $id);
$invoices = RentalInvoice::getForContract((int) $id);
return $this->view('Rentals.Views.contract_show', [
'contract' => $contract,
'entity' => $entity,
'facility' => $facility,
'bookings' => $bookings,
'invoices' => $invoices,
]);
}
......@@ -344,6 +356,106 @@ class RentalController extends Controller
}
}
// ─── Invoices ────────────────────────────────────────────────
/**
* Show form to generate a new invoice for a contract.
*/
public function createInvoice(Request $request, string $id): Response
{
$contract = RentalContract::find((int) $id);
if (!$contract) {
return $this->redirect('/rentals')->withError('العقد غير موجود');
}
$monthly = RentalInvoiceService::calcBase($contract);
$utils = RentalInvoiceService::calcUtilities($contract, $monthly);
$vat = RentalInvoiceService::calcVat($contract, $monthly + $utils);
return $this->view('Rentals.Views.invoice_form', [
'contract' => $contract,
'monthly_base' => $monthly,
'monthly_utils' => $utils,
'monthly_vat' => $vat,
'monthly_total' => round($monthly + $utils + $vat, 2),
]);
}
/**
* Generate and store a new invoice.
*/
public function storeInvoice(Request $request, string $id): Response
{
$contract = RentalContract::find((int) $id);
if (!$contract) {
return $this->redirect('/rentals')->withError('العقد غير موجود');
}
$periodStart = trim((string) $request->post('period_start', ''));
$periodEnd = trim((string) $request->post('period_end', ''));
$dueDate = trim((string) $request->post('due_date', ''));
$notes = trim((string) $request->post('notes', '')) ?: null;
$errors = [];
if (!$periodStart || !$periodEnd || !$dueDate) {
$errors[] = 'تاريخ الفترة وتاريخ الاستحقاق مطلوبة';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
return $this->redirect('/rentals/contracts/' . $id . '/invoices/create');
}
try {
$invoice = RentalInvoiceService::generateInvoice((int) $id, $periodStart, $periodEnd, $dueDate, $notes);
return $this->redirect('/rentals/invoices/' . $invoice->id)->withSuccess('تم إنشاء الفاتورة بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/rentals/contracts/' . $id)->withError($e->getMessage());
}
}
/**
* Show invoice detail.
*/
public function showInvoice(Request $request, string $id): Response
{
$invoice = RentalInvoice::find((int) $id);
if (!$invoice) {
return $this->redirect('/rentals')->withError('الفاتورة غير موجودة');
}
$contractId = (int) $invoice->contract_id;
$contract = RentalContract::find($contractId);
$entity = $contract ? RentalEntity::find((int) $contract->entity_id) : null;
return $this->view('Rentals.Views.invoice_show', [
'invoice' => $invoice,
'contract' => $contract,
'entity' => $entity,
]);
}
/**
* Mark invoice as paid.
*/
public function payInvoice(Request $request, string $id): Response
{
$paymentId = (int) $request->post('payment_id', 0);
$paidAt = trim((string) $request->post('paid_at', date('Y-m-d H:i:s')));
if ($paymentId <= 0) {
return $this->redirect('/rentals/invoices/' . $id)->withError('رقم الدفعة مطلوب');
}
try {
RentalInvoiceService::markPaid((int) $id, $paymentId, $paidAt);
return $this->redirect('/rentals/invoices/' . $id)->withSuccess('تم تسجيل الدفع بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/rentals/invoices/' . $id)->withError($e->getMessage());
}
}
// ─── Private helpers ─────────────────────────────────────────
/**
......
......@@ -28,6 +28,13 @@ class RentalContract extends Model
'discount_percentage',
'discount_amount',
'total_amount',
'vat_percentage',
'vat_amount',
'utilities_percentage',
'utilities_amount',
'grand_total',
'late_fee_type',
'late_fee_rate',
'deposit_percentage',
'deposit_amount',
'deposit_status',
......@@ -40,6 +47,45 @@ class RentalContract extends Model
'notes',
];
/**
* Late fee types with Arabic labels.
*/
public static function getLateFeeTtypes(): array
{
return [
'none' => 'لا يوجد',
'daily' => 'يومي',
'weekly' => 'أسبوعي',
'monthly' => 'شهري',
];
}
/**
* Calculate monthly invoice amounts from a contract row.
* Returns [base, utilities, vat, total].
*/
public static function calcMonthlyInvoice(object|array $c): array
{
$total = (float) (is_object($c) ? $c->total_amount : ($c['total_amount'] ?? 0));
$months = (float) (is_object($c) ? ($c->contract_months ?? 1) : ($c['contract_months'] ?? 1));
$utilsPct = (float) (is_object($c) ? $c->utilities_percentage : ($c['utilities_percentage'] ?? 0));
$vatPct = (float) (is_object($c) ? $c->vat_percentage : ($c['vat_percentage'] ?? 1));
$startDate = is_object($c) ? ($c->start_date ?? '') : ($c['start_date'] ?? '');
$endDate = is_object($c) ? ($c->end_date ?? '') : ($c['end_date'] ?? '');
if ($startDate && $endDate) {
$diff = (new \DateTimeImmutable($startDate))->diff(new \DateTimeImmutable($endDate));
$months = max(1, ($diff->y * 12) + $diff->m + ($diff->d > 0 ? 1 : 0));
}
$base = round($total / max(1, $months), 2);
$utilities = round($base * ($utilsPct / 100), 2);
$vat = round(($base + $utilities) * ($vatPct / 100), 2);
$invoiceTotal = round($base + $utilities + $vat, 2);
return compact('base', 'utilities', 'vat', 'invoiceTotal');
}
/**
* Get all contract statuses with Arabic labels.
*/
......
<?php
declare(strict_types=1);
namespace App\Modules\Rentals\Models;
use App\Core\Model;
use App\Core\App;
class RentalInvoice extends Model
{
protected static string $table = 'rental_invoices';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'invoice_number',
'contract_id',
'entity_id',
'period_start',
'period_end',
'due_date',
'base_amount',
'utilities_amount',
'vat_amount',
'late_fee_amount',
'total_amount',
'status',
'paid_at',
'payment_id',
'notes',
'journal_entry_id',
];
public static function getStatuses(): array
{
return [
'unpaid' => 'غير مدفوعة',
'paid' => 'مدفوعة',
'overdue' => 'متأخرة',
'cancelled' => 'ملغاة',
];
}
public static function getStatusLabel(string $status): string
{
return self::getStatuses()[$status] ?? $status;
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'paid' => '#059669',
'overdue' => '#DC2626',
'cancelled' => '#6B7280',
default => '#D97706',
};
}
public static function generateNumber(): string
{
$prefix = 'RI-' . date('Ym') . '-';
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT invoice_number FROM rental_invoices WHERE invoice_number LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
$nextSeq = 1;
if ($row) {
$number = is_object($row) ? $row->invoice_number : ($row['invoice_number'] ?? '');
$parts = explode('-', (string) $number);
$nextSeq = ((int) ($parts[2] ?? 0)) + 1;
}
return $prefix . str_pad((string) $nextSeq, 4, '0', STR_PAD_LEFT);
}
public static function getForContract(int $contractId): array
{
return static::query()
->where('contract_id', '=', $contractId)
->orderBy('period_start', 'ASC')
->get();
}
}
......@@ -15,4 +15,10 @@ return [
['POST', '/rentals/contracts/{id:\d+}/approve', 'Rentals\Controllers\RentalController@approveContract', ['auth', 'csrf'], 'rental.approve'],
['POST', '/rentals/contracts/{id:\d+}/deposit/collect', 'Rentals\Controllers\RentalController@collectDeposit', ['auth', 'csrf'], 'rental.manage_deposit'],
['POST', '/rentals/contracts/{id:\d+}/deposit/refund', 'Rentals\Controllers\RentalController@refundDeposit', ['auth', 'csrf'], 'rental.manage_deposit'],
// Invoices
['GET', '/rentals/contracts/{id:\d+}/invoices/create', 'Rentals\Controllers\RentalController@createInvoice', ['auth'], 'rental.manage_contract'],
['POST', '/rentals/contracts/{id:\d+}/invoices', 'Rentals\Controllers\RentalController@storeInvoice', ['auth', 'csrf'], 'rental.manage_contract'],
['GET', '/rentals/invoices/{id:\d+}', 'Rentals\Controllers\RentalController@showInvoice', ['auth'], 'rental.view'],
['POST', '/rentals/invoices/{id:\d+}/pay', 'Rentals\Controllers\RentalController@payInvoice', ['auth', 'csrf'], 'rental.manage_contract'],
];
......@@ -37,13 +37,28 @@ final class RentalContractService
$months = ($diff->y * 12) + $diff->m + ($diff->d > 0 ? 1 : 0);
}
$discountPercentage = self::calculateBulkDiscount($totalUnits, $months);
$discountAmount = round($subtotal * ($discountPercentage / 100), 2);
$totalAmount = $subtotal - $discountAmount;
$discountPercentage = self::calculateBulkDiscount($totalUnits, $months);
$discountAmount = round($subtotal * ($discountPercentage / 100), 2);
$totalAmount = $subtotal - $discountAmount;
// Calculate deposit
$depositPercentage = (float) ($data['deposit_percentage'] ?? 0);
$depositAmount = round($totalAmount * ($depositPercentage / 100), 2);
// VAT — always 1% of (base + utilities) per invoice, stored on contract for reference
$vatPercentage = (float) ($data['vat_percentage'] ?? 1.00);
// Utilities — optional % of the monthly base amount
$utilitiesPercentage = (float) ($data['utilities_percentage'] ?? 0.00);
// Grand total across whole contract = total + utilities_total + vat_total
$vatAmount = round($totalAmount * ($vatPercentage / 100), 2);
$utilitiesAmount = round($totalAmount * ($utilitiesPercentage / 100), 2);
$grandTotal = $totalAmount + $utilitiesAmount + $vatAmount;
// Late fee
$lateFeeType = $data['late_fee_type'] ?? 'none';
$lateFeeRate = (float) ($data['late_fee_rate'] ?? 0.00);
// Deposit — % of grand total
$depositPercentage = (float) ($data['deposit_percentage'] ?? 0);
$depositAmount = round($grandTotal * ($depositPercentage / 100), 2);
$contract = RentalContract::create([
'contract_number' => $contractNumber,
......@@ -59,6 +74,13 @@ final class RentalContractService
'discount_percentage' => $discountPercentage,
'discount_amount' => $discountAmount,
'total_amount' => $totalAmount,
'vat_percentage' => $vatPercentage,
'vat_amount' => $vatAmount,
'utilities_percentage' => $utilitiesPercentage,
'utilities_amount' => $utilitiesAmount,
'grand_total' => $grandTotal,
'late_fee_type' => $lateFeeType,
'late_fee_rate' => $lateFeeRate,
'deposit_percentage' => $depositPercentage,
'deposit_amount' => $depositAmount,
'deposit_status' => 'pending',
......@@ -110,7 +132,7 @@ final class RentalContractService
throw new \RuntimeException('Contract not found');
}
$facilityId = (int) ($contract->facility_id ?? $contract['facility_id']);
$facilityId = (int) $contract->facility_id;
foreach ($schedule as $slot) {
RentalBooking::create([
......
<?php
declare(strict_types=1);
namespace App\Modules\Rentals\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Rentals\Models\RentalContract;
use App\Modules\Rentals\Models\RentalInvoice;
final class RentalInvoiceService
{
/**
* Generate a single monthly invoice for a contract.
*
* Calculates: base (monthly share) + utilities (% of base) + VAT (1% of base+utilities).
* Late fee is NOT added at generation — it is added at payment time if overdue.
*/
public static function generateInvoice(int $contractId, string $periodStart, string $periodEnd, string $dueDate, ?string $notes = null): object
{
$contract = RentalContract::find($contractId);
if (!$contract) {
throw new \RuntimeException('Contract not found');
}
$status = is_object($contract) ? $contract->status : ($contract['status'] ?? '');
if (!in_array($status, ['approved', 'active'], true)) {
throw new \RuntimeException('Cannot generate invoice for a contract that is not approved or active');
}
$base = self::calcBase($contract);
$utils = self::calcUtilities($contract, $base);
$vat = self::calcVat($contract, $base + $utils);
$total = round($base + $utils + $vat, 2);
$entityId = (int) (is_object($contract) ? $contract->entity_id : ($contract['entity_id'] ?? 0));
$invoice = RentalInvoice::create([
'invoice_number' => RentalInvoice::generateNumber(),
'contract_id' => $contractId,
'entity_id' => $entityId,
'period_start' => $periodStart,
'period_end' => $periodEnd,
'due_date' => $dueDate,
'base_amount' => $base,
'utilities_amount' => $utils,
'vat_amount' => $vat,
'late_fee_amount' => 0.00,
'total_amount' => $total,
'status' => 'unpaid',
'notes' => $notes,
]);
Logger::info('Rental invoice generated', [
'invoice_id' => $invoice->id,
'contract_id' => $contractId,
'total' => $total,
]);
return $invoice;
}
/**
* Mark an invoice as paid and apply late fee if applicable.
*
* Late fee is calculated from due_date to paid_at based on contract late_fee_type/rate.
* Journal entry is dispatched via event.
*/
public static function markPaid(int $invoiceId, int $paymentId, string $paidAt): void
{
$invoice = RentalInvoice::find($invoiceId);
if (!$invoice) {
throw new \RuntimeException('Invoice not found');
}
$currentStatus = is_object($invoice) ? $invoice->status : ($invoice['status'] ?? '');
if ($currentStatus === 'paid') {
throw new \RuntimeException('Invoice is already paid');
}
$contractId = (int) (is_object($invoice) ? $invoice->contract_id : ($invoice['contract_id'] ?? 0));
$contract = RentalContract::find($contractId);
$lateFee = $contract ? self::calcLateFee($invoice, $contract, $paidAt) : 0.00;
$baseAmount = (float) (is_object($invoice) ? $invoice->base_amount : ($invoice['base_amount'] ?? 0));
$utilsAmount = (float) (is_object($invoice) ? $invoice->utilities_amount : ($invoice['utilities_amount'] ?? 0));
$vatAmount = (float) (is_object($invoice) ? $invoice->vat_amount : ($invoice['vat_amount'] ?? 0));
$newTotal = round($baseAmount + $utilsAmount + $vatAmount + $lateFee, 2);
$invoice->update([
'status' => 'paid',
'paid_at' => $paidAt,
'payment_id' => $paymentId,
'late_fee_amount' => $lateFee,
'total_amount' => $newTotal,
]);
EventBus::dispatch('rental.invoice_paid', [
'invoice_id' => $invoiceId,
'contract_id' => $contractId,
'payment_id' => $paymentId,
'base_amount' => $baseAmount,
'utilities_amount'=> $utilsAmount,
'vat_amount' => $vatAmount,
'late_fee_amount' => $lateFee,
'total_amount' => $newTotal,
'paid_at' => $paidAt,
]);
Logger::info('Rental invoice paid', [
'invoice_id' => $invoiceId,
'total' => $newTotal,
'late_fee' => $lateFee,
]);
}
/**
* Calculate the monthly base amount (contract total / number of months).
*/
public static function calcBase(object|array $contract): float
{
$totalAmount = (float) (is_object($contract) ? $contract->total_amount : ($contract['total_amount'] ?? 0));
$startDate = is_object($contract) ? ($contract->start_date ?? '') : ($contract['start_date'] ?? '');
$endDate = is_object($contract) ? ($contract->end_date ?? '') : ($contract['end_date'] ?? '');
$months = 1;
if ($startDate && $endDate) {
$diff = (new \DateTimeImmutable($startDate))->diff(new \DateTimeImmutable($endDate));
$months = max(1, ($diff->y * 12) + $diff->m + ($diff->d > 0 ? 1 : 0));
}
return round($totalAmount / $months, 2);
}
/**
* Calculate utilities surcharge on the monthly base.
*/
public static function calcUtilities(object|array $contract, float $base): float
{
$pct = (float) (is_object($contract) ? $contract->utilities_percentage : ($contract['utilities_percentage'] ?? 0));
return round($base * ($pct / 100), 2);
}
/**
* Calculate VAT (default 1%) on (base + utilities).
*/
public static function calcVat(object|array $contract, float $taxableAmount): float
{
$pct = (float) (is_object($contract) ? $contract->vat_percentage : ($contract['vat_percentage'] ?? 1.00));
return round($taxableAmount * ($pct / 100), 2);
}
/**
* Calculate late fee based on contract late_fee_type and days overdue.
*
* late_fee_type: none | daily | weekly | monthly
* late_fee_rate: % of invoice total per period.
*
* Examples:
* daily 2% → 2% of total per overdue day
* weekly 5% → 5% of total per overdue week (or fraction)
* monthly 10% → 10% of total per overdue month (or fraction)
*/
public static function calcLateFee(object|array $invoice, object|array $contract, string $paidAt): float
{
$lateFeeType = is_object($contract) ? ($contract->late_fee_type ?? 'none') : ($contract['late_fee_type'] ?? 'none');
$lateFeeRate = (float) (is_object($contract) ? ($contract->late_fee_rate ?? 0) : ($contract['late_fee_rate'] ?? 0));
if ($lateFeeType === 'none' || $lateFeeRate <= 0) {
return 0.00;
}
$dueDate = is_object($invoice) ? ($invoice->due_date ?? '') : ($invoice['due_date'] ?? '');
$invoiceBase = (float) (is_object($invoice) ? ($invoice->base_amount ?? 0) : ($invoice['base_amount'] ?? 0));
$invoiceUtils = (float) (is_object($invoice) ? ($invoice->utilities_amount ?? 0) : ($invoice['utilities_amount'] ?? 0));
$invoiceVat = (float) (is_object($invoice) ? ($invoice->vat_amount ?? 0) : ($invoice['vat_amount'] ?? 0));
$invoiceTotal = $invoiceBase + $invoiceUtils + $invoiceVat;
if (!$dueDate || $invoiceTotal <= 0) {
return 0.00;
}
$due = new \DateTimeImmutable($dueDate);
$paid = new \DateTimeImmutable($paidAt);
if ($paid <= $due) {
return 0.00;
}
$overdueDays = (int) $due->diff($paid)->days;
$periods = match ($lateFeeType) {
'daily' => $overdueDays,
'weekly' => (int) ceil($overdueDays / 7),
'monthly' => (int) ceil($overdueDays / 30),
default => 0,
};
return round($invoiceTotal * ($lateFeeRate / 100) * $periods, 2);
}
}
......@@ -9,9 +9,10 @@ $__template->layout('Layout.main');
<?php $__template->section('content'); ?>
<form method="POST" action="/rentals/contracts">
<form method="POST" action="/rentals/contracts" id="contractForm">
<?= \App\Core\CSRF::field() ?>
<!-- ── بيانات العقد ── -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">بيانات العقد</h3>
......@@ -42,7 +43,7 @@ $__template->layout('Layout.main');
<label class="form-label">نوع النشاط</label>
<select name="activity_type" class="form-input">
<option value="">-- اختر --</option>
<option value="practice" <?= old('activity_type') === 'practice' ? 'selected' : '' ?>>تدريب</option>
<option value="practice" <?= old('activity_type') === 'practice' ? 'selected' : '' ?>>تدريب</option>
<option value="competitive" <?= old('activity_type') === 'competitive' ? 'selected' : '' ?>>تنافسي</option>
</select>
</div>
......@@ -57,39 +58,122 @@ $__template->layout('Layout.main');
<div>
<label class="form-label">تاريخ البدء <span style="color:#DC2626;">*</span></label>
<input type="date" name="start_date" value="<?= e(old('start_date')) ?>" class="form-input" required>
<input type="date" name="start_date" id="start_date" value="<?= e(old('start_date')) ?>" class="form-input" required>
</div>
<div>
<label class="form-label">تاريخ الانتهاء <span style="color:#DC2626;">*</span></label>
<input type="date" name="end_date" value="<?= e(old('end_date')) ?>" class="form-input" required>
<input type="date" name="end_date" id="end_date" value="<?= e(old('end_date')) ?>" class="form-input" required>
</div>
<div>
<label class="form-label">عدد الوحدات <span style="color:#DC2626;">*</span></label>
<input type="number" name="total_units" value="<?= e(old('total_units')) ?>" class="form-input" min="1" required>
<input type="number" name="total_units" id="total_units" value="<?= e(old('total_units')) ?>" class="form-input" min="1" required>
</div>
<div>
<label class="form-label">سعر الوحدة <span style="color:#DC2626;">*</span></label>
<input type="number" name="unit_rate" value="<?= e(old('unit_rate')) ?>" class="form-input" step="0.01" min="0.01" required>
<label class="form-label">سعر الوحدة (جنيه) <span style="color:#DC2626;">*</span></label>
<input type="number" name="unit_rate" id="unit_rate" value="<?= e(old('unit_rate')) ?>" class="form-input" step="0.01" min="0.01" required>
</div>
<div>
<label class="form-label">نسبة التأمين (%)</label>
<input type="number" name="deposit_percentage" value="<?= e(old('deposit_percentage')) ?>" class="form-input" step="0.01" min="0" placeholder="10%">
<input type="number" name="deposit_percentage" id="deposit_percentage" value="<?= e(old('deposit_percentage', '10')) ?>" class="form-input" step="0.01" min="0" placeholder="10">
</div>
<div></div>
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" style="resize:vertical;"><?= e(old('notes')) ?></textarea>
<textarea name="notes" class="form-input" rows="2" style="resize:vertical;"><?= e(old('notes')) ?></textarea>
</div>
</div>
</div>
<!-- ── الرسوم الإضافية ── -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">الرسوم الإضافية</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">نسبة القيمة المضافة (%)</label>
<input type="number" name="vat_percentage" id="vat_percentage" value="<?= e(old('vat_percentage', '1')) ?>" class="form-input" step="0.01" min="0" max="100" placeholder="1">
<small style="color:#6B7280;">تُحتسب على الإيجار الشهري + المرافق</small>
</div>
<div>
<label class="form-label">نسبة المرافق (%) — اختياري</label>
<input type="number" name="utilities_percentage" id="utilities_percentage" value="<?= e(old('utilities_percentage', '0')) ?>" class="form-input" step="0.01" min="0" max="100" placeholder="0">
<small style="color:#6B7280;">نسبة من الإيجار الشهري مقابل المرافق</small>
</div>
</div>
</div>
<!-- ── غرامات التأخير ── -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">غرامات التأخير</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">نوع الغرامة</label>
<select name="late_fee_type" id="late_fee_type" class="form-input" onchange="toggleLateFeeRate()">
<option value="none" <?= old('late_fee_type', 'none') === 'none' ? 'selected' : '' ?>>لا يوجد</option>
<option value="daily" <?= old('late_fee_type', 'none') === 'daily' ? 'selected' : '' ?>>يومي</option>
<option value="weekly" <?= old('late_fee_type', 'none') === 'weekly' ? 'selected' : '' ?>>أسبوعي</option>
<option value="monthly" <?= old('late_fee_type', 'none') === 'monthly' ? 'selected' : '' ?>>شهري</option>
</select>
</div>
<div id="late_fee_rate_wrap" style="display:<?= in_array(old('late_fee_type', 'none'), ['daily','weekly','monthly']) ? 'block' : 'none' ?>;">
<label class="form-label">نسبة الغرامة (%)</label>
<input type="number" name="late_fee_rate" id="late_fee_rate" value="<?= e(old('late_fee_rate', '0')) ?>" class="form-input" step="0.01" min="0" max="100" placeholder="0">
<small style="color:#6B7280;">% من إجمالي الفاتورة لكل فترة تأخير</small>
</div>
</div>
</div>
<!-- ── ملخص تقديري ── -->
<div class="card" style="margin-bottom:20px;background:#F0FDF4;border:1px solid #BBF7D0;">
<div style="padding:15px 20px;border-bottom:1px solid #BBF7D0;">
<h3 style="margin:0;color:#059669;">ملخص الفاتورة الشهرية التقديري</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;text-align:center;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الإيجار الشهري</div>
<div id="preview_base" style="font-size:20px;font-weight:700;color:#0D7377;"></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">المرافق</div>
<div id="preview_utils" style="font-size:20px;font-weight:700;color:#7C3AED;"></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">القيمة المضافة</div>
<div id="preview_vat" style="font-size:20px;font-weight:700;color:#D97706;"></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">إجمالي الفاتورة</div>
<div id="preview_total" style="font-size:20px;font-weight:700;color:#059669;"></div>
</div>
</div>
<div style="padding:0 20px 15px;display:grid;grid-template-columns:1fr 1fr;gap:15px;text-align:center;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">عدد الأشهر</div>
<div id="preview_months" style="font-size:16px;font-weight:600;color:#374151;"></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">التأمين المطلوب</div>
<div id="preview_deposit" style="font-size:16px;font-weight:600;color:#374151;"></div>
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">إنشاء العقد</button>
<a href="/rentals" class="btn btn-outline">إلغاء</a>
......@@ -99,6 +183,67 @@ $__template->layout('Layout.main');
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
const ids = ['total_units','unit_rate','vat_percentage','utilities_percentage','deposit_percentage','start_date','end_date'];
ids.forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', calcPreview);
});
calcPreview();
});
function toggleLateFeeRate() {
const type = document.getElementById('late_fee_type').value;
const wrap = document.getElementById('late_fee_rate_wrap');
wrap.style.display = ['daily','weekly','monthly'].includes(type) ? 'block' : 'none';
}
function calcPreview() {
const units = parseFloat(document.getElementById('total_units')?.value) || 0;
const rate = parseFloat(document.getElementById('unit_rate')?.value) || 0;
const vatPct = parseFloat(document.getElementById('vat_percentage')?.value) || 1;
const utilsPct = parseFloat(document.getElementById('utilities_percentage')?.value) || 0;
const depositPct = parseFloat(document.getElementById('deposit_percentage')?.value) || 0;
const startVal = document.getElementById('start_date')?.value;
const endVal = document.getElementById('end_date')?.value;
if (!units || !rate) { clearPreview(); return; }
const subtotal = units * rate;
let months = 1;
if (startVal && endVal) {
const s = new Date(startVal), e = new Date(endVal);
if (e > s) {
const diffMs = e - s;
const diffDays = diffMs / (1000 * 60 * 60 * 24);
months = Math.max(1, Math.ceil(diffDays / 30));
}
}
const monthlyBase = Math.round((subtotal / months) * 100) / 100;
const monthlyUtils = Math.round(monthlyBase * (utilsPct / 100) * 100) / 100;
const monthlyVat = Math.round((monthlyBase + monthlyUtils) * (vatPct / 100) * 100) / 100;
const monthlyTotal = Math.round((monthlyBase + monthlyUtils + monthlyVat) * 100) / 100;
const totalWithUtils = subtotal + (subtotal * utilsPct / 100);
const totalVat = subtotal * (vatPct / 100);
const grandTotal = Math.round((subtotal + subtotal * utilsPct / 100 + totalVat) * 100) / 100;
const deposit = Math.round(grandTotal * (depositPct / 100) * 100) / 100;
const fmt = v => v.toLocaleString('ar-EG', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' ج';
document.getElementById('preview_base').textContent = fmt(monthlyBase);
document.getElementById('preview_utils').textContent = fmt(monthlyUtils);
document.getElementById('preview_vat').textContent = fmt(monthlyVat);
document.getElementById('preview_total').textContent = fmt(monthlyTotal);
document.getElementById('preview_months').textContent = months + ' شهر';
document.getElementById('preview_deposit').textContent = fmt(deposit);
}
function clearPreview() {
['preview_base','preview_utils','preview_vat','preview_total','preview_months','preview_deposit']
.forEach(id => { document.getElementById(id).textContent = '—'; });
}
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Rentals\Models\RentalContract;
use App\Modules\Rentals\Models\RentalBooking;
use App\Modules\Rentals\Models\RentalInvoice;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($contract->contract_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (in_array($contract->status ?? 'draft', ['approved','active']) && can('rental.manage_contract')): ?>
<a href="/rentals/contracts/<?= (int) $contract->id ?>/invoices/create" class="btn btn-primary" style="margin-left:8px;">
<i data-lucide="file-plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> فاتورة جديدة
</a>
<?php endif; ?>
<a href="/rentals" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العودة للعقود</a>
<?php $__template->endSection(); ?>
......@@ -16,9 +21,10 @@ $cStatus = $contract->status ?? 'draft';
$depositStatus = $contract->deposit_status ?? 'pending';
$activityTypes = ['practice' => 'تدريب', 'competitive' => 'تنافسي'];
$timeTiers = ['AM' => 'صباحي', 'PM' => 'مسائي'];
$lateFeeTypes = ['none' => 'لا يوجد', 'daily' => 'يومي', 'weekly' => 'أسبوعي', 'monthly' => 'شهري'];
?>
<!-- Header Card -->
<!-- Header -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
<div>
......@@ -42,45 +48,62 @@ $timeTiers = ['AM' => 'صباحي', 'PM' => 'مسائي'];
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;"><i data-lucide="calculator" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> الملخص المالي</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;">
<div style="padding:20px;display:grid;grid-template-columns:repeat(3,1fr);gap:15px;">
<div style="background:#F9FAFB;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">عدد الوحدات</div>
<div style="font-size:22px;font-weight:700;color:#374151;"><?= (int) ($contract->total_units ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">إجمالي الوحدات × السعر</div>
<div style="font-size:18px;font-weight:700;color:#374151;"><?= money((float) ($contract->subtotal ?? 0)) ?></div>
</div>
<div style="background:#F9FAFB;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">سعر الوحدة</div>
<div style="font-size:22px;font-weight:700;color:#374151;"><?= money((float) ($contract->unit_rate ?? 0)) ?></div>
<?php if ((float) ($contract->discount_amount ?? 0) > 0): ?>
<div style="background:#FEF2F2;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">خصم (<?= (float) ($contract->discount_percentage ?? 0) ?>%)</div>
<div style="font-size:18px;font-weight:700;color:#DC2626;">-<?= money((float) ($contract->discount_amount ?? 0)) ?></div>
</div>
<?php endif; ?>
<div style="background:#F9FAFB;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">المجموع الفرعي</div>
<div style="font-size:22px;font-weight:700;color:#374151;"><?= money((float) ($contract->subtotal ?? 0)) ?></div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">صافي الإيجار</div>
<div style="font-size:18px;font-weight:700;color:#374151;"><?= money((float) ($contract->total_amount ?? 0)) ?></div>
</div>
<?php
$discountPct = (float) ($contract->discount_percentage ?? 0);
$discountAmt = (float) ($contract->discount_amount ?? 0);
?>
<?php if ($discountPct > 0 || $discountAmt > 0): ?>
<div style="background:#FEF2F2;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الخصم <?php if ($discountPct > 0): ?>(<?= $discountPct ?>%)<?php endif; ?></div>
<div style="font-size:22px;font-weight:700;color:#DC2626;">-<?= money($discountAmt) ?></div>
<div style="background:#F5F3FF;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">مرافق (<?= (float) ($contract->utilities_percentage ?? 0) ?>%)</div>
<div style="font-size:18px;font-weight:700;color:#7C3AED;"><?= money((float) ($contract->utilities_amount ?? 0)) ?></div>
</div>
<div style="background:#FFFBEB;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">القيمة المضافة (<?= (float) ($contract->vat_percentage ?? 1) ?>%)</div>
<div style="font-size:18px;font-weight:700;color:#D97706;"><?= money((float) ($contract->vat_amount ?? 0)) ?></div>
</div>
<div style="background:#0D7377;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:rgba(255,255,255,0.8);margin-bottom:4px;">الإجمالي الكلي</div>
<div style="font-size:22px;font-weight:700;color:#fff;"><?= money((float) ($contract->grand_total ?? 0)) ?></div>
</div>
</div>
<!-- Monthly preview row -->
<?php
use App\Modules\Rentals\Services\RentalInvoiceService;
$mBase = RentalInvoiceService::calcBase($contract);
$mUtils = RentalInvoiceService::calcUtilities($contract, $mBase);
$mVat = RentalInvoiceService::calcVat($contract, $mBase + $mUtils);
$mTotal = round($mBase + $mUtils + $mVat, 2);
?>
<div style="padding:0 20px 15px;">
<div style="background:#F0FDF4;border:1px solid #BBF7D0;border-radius:8px;padding:12px 20px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;">
<span style="font-size:13px;font-weight:600;color:#059669;">الفاتورة الشهرية:</span>
<span style="font-size:13px;color:#374151;">إيجار <strong><?= money($mBase) ?></strong></span>
<?php if ($mUtils > 0): ?>
<span style="font-size:13px;color:#374151;">+ مرافق <strong><?= money($mUtils) ?></strong></span>
<?php endif; ?>
<span style="font-size:13px;color:#374151;">+ VAT <strong><?= money($mVat) ?></strong></span>
<span style="font-size:14px;font-weight:700;color:#059669;">= <?= money($mTotal) ?></span>
</div>
<?php endif; ?>
</div>
<!-- Deposit row -->
<div style="padding:0 20px 20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div style="background:#0D7377;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:rgba(255,255,255,0.8);margin-bottom:4px;">الإجمالي</div>
<div style="font-size:26px;font-weight:700;color:#fff;"><?= money((float) ($contract->total_amount ?? 0)) ?></div>
</div>
<div style="background:#F9FAFB;padding:15px;border-radius:8px;text-align:center;border:1px solid #E5E7EB;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">
التأمين (<?= (float) ($contract->deposit_percentage ?? 0) ?>%)
</div>
<div style="font-size:22px;font-weight:700;color:#374151;margin-bottom:6px;"><?= money((float) ($contract->deposit_amount ?? 0)) ?></div>
<span class="badge" style="background:<?= RentalContract::getDepositStatusColor($depositStatus) ?>15;color:<?= RentalContract::getDepositStatusColor($depositStatus) ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e(RentalContract::getDepositStatusLabel($depositStatus)) ?>
</span>
</div>
<div style="background:#F9FAFB;border:1px solid #E5E7EB;border-radius:8px;padding:12px 20px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
<span style="font-size:13px;color:#374151;">التأمين (<?= (float) ($contract->deposit_percentage ?? 0) ?>%): <strong><?= money((float) ($contract->deposit_amount ?? 0)) ?></strong></span>
<span class="badge" style="background:<?= RentalContract::getDepositStatusColor($depositStatus) ?>15;color:<?= RentalContract::getDepositStatusColor($depositStatus) ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e(RentalContract::getDepositStatusLabel($depositStatus)) ?>
</span>
</div>
</div>
</div>
......@@ -90,22 +113,32 @@ $timeTiers = ['AM' => 'صباحي', 'PM' => 'مسائي'];
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin:0 0 15px;">تفاصيل العقد</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:8px 0;color:#6B7280;width:40%;">تاريخ البدء</td><td style="padding:8px 0;"><?= e($contract->start_date ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">تاريخ الانتهاء</td><td style="padding:8px 0;"><?= e($contract->end_date ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">نوع النشاط</td><td style="padding:8px 0;"><?= e($activityTypes[$contract->activity_type ?? ''] ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الفترة الزمنية</td><td style="padding:8px 0;"><?= e($timeTiers[$contract->time_tier ?? ''] ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">التقرير الفني</td><td style="padding:8px 0;"><?= ($contract->technical_report_submitted ?? 0) ? '<span style="color:#059669;font-weight:600;">نعم</span>' : '<span style="color:#DC2626;">لا</span>' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;width:45%;">تاريخ البدء</td><td><?= e($contract->start_date ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الانتهاء</td><td><?= e($contract->end_date ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">نوع النشاط</td><td><?= e($activityTypes[$contract->activity_type ?? ''] ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الفترة الزمنية</td><td><?= e($timeTiers[$contract->time_tier ?? ''] ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">التقرير الفني</td><td><?= ($contract->technical_report_submitted ?? 0) ? '<span style="color:#059669;font-weight:600;">نعم</span>' : '<span style="color:#DC2626;">لا</span>' ?></td></tr>
</table>
</div>
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin:0 0 15px;">بيانات الاعتماد</h4>
<h4 style="color:#0D7377;margin:0 0 15px;">الغرامات والاعتماد</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:8px 0;color:#6B7280;width:40%;">اعتمد بواسطة</td><td style="padding:8px 0;"><?= e($contract->approved_by ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">تاريخ الاعتماد</td><td style="padding:8px 0;"><?= e($contract->approved_at ?? '—') ?></td></tr>
<tr>
<td style="padding:6px 0;color:#6B7280;width:45%;">نوع الغرامة</td>
<td><?= e($lateFeeTypes[$contract->late_fee_type ?? 'none'] ?? 'لا يوجد') ?></td>
</tr>
<?php if (($contract->late_fee_type ?? 'none') !== 'none'): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">نسبة الغرامة</td>
<td><?= (float) ($contract->late_fee_rate ?? 0) ?>%</td>
</tr>
<?php endif; ?>
<tr><td style="padding:6px 0;color:#6B7280;">اعتمد بواسطة</td><td><?= e($contract->approved_by ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الاعتماد</td><td><?= e($contract->approved_at ?? '—') ?></td></tr>
</table>
<?php if (!empty($contract->notes)): ?>
<div style="margin-top:15px;padding-top:12px;border-top:1px solid #E5E7EB;">
<h4 style="color:#6B7280;font-size:13px;margin:0 0 6px;">ملاحظات</h4>
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #E5E7EB;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">ملاحظات</div>
<p style="font-size:14px;margin:0;color:#374151;"><?= nl2br(e($contract->notes)) ?></p>
</div>
<?php endif; ?>
......@@ -127,8 +160,8 @@ $timeTiers = ['AM' => 'صباحي', 'PM' => 'مسائي'];
<?php if (in_array($cStatus, ['approved', 'active']) && $depositStatus === 'pending' && can('rental.manage_deposit')): ?>
<div class="card" style="margin-bottom:20px;padding:20px;display:flex;align-items:center;gap:15px;background:#F0FDF4;border:1px solid #BBF7D0;">
<i data-lucide="banknote" style="width:20px;height:20px;color:#059669;"></i>
<span style="font-size:14px;color:#166534;flex:1;">التأمين قيد التحصيل — أدخل رقم الدفعة لتسجيل التحصيل</span>
<form method="POST" action="/rentals/contracts/<?= (int) $contract->id ?>/deposit" style="margin:0;display:flex;gap:8px;align-items:center;">
<span style="font-size:14px;color:#166534;flex:1;">التأمين قيد التحصيل</span>
<form method="POST" action="/rentals/contracts/<?= (int) $contract->id ?>/deposit/collect" style="margin:0;display:flex;gap:8px;align-items:center;">
<?= \App\Core\CSRF::field() ?>
<input type="number" name="payment_id" class="form-input" placeholder="رقم الدفعة" required style="width:150px;">
<button type="submit" class="btn btn-primary">تحصيل التأمين</button>
......@@ -140,13 +173,74 @@ $timeTiers = ['AM' => 'صباحي', 'PM' => 'مسائي'];
<div class="card" style="margin-bottom:20px;padding:20px;display:flex;align-items:center;gap:15px;background:#EFF6FF;border:1px solid #BFDBFE;">
<i data-lucide="undo-2" style="width:20px;height:20px;color:#0284C7;"></i>
<span style="font-size:14px;color:#1E40AF;flex:1;">تم تحصيل التأمين — يمكنك استرداد المبلغ</span>
<form method="POST" action="/rentals/contracts/<?= (int) $contract->id ?>/refund" style="margin:0;">
<form method="POST" action="/rentals/contracts/<?= (int) $contract->id ?>/deposit/refund" style="margin:0;">
<?= \App\Core\CSRF::field() ?>
<button type="submit" class="btn btn-outline" style="color:#0284C7;border-color:#0284C7;"><i data-lucide="undo-2" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> استرداد التأمين</button>
<button type="submit" class="btn btn-outline" style="color:#0284C7;border-color:#0284C7;">استرداد التأمين</button>
</form>
</div>
<?php endif; ?>
<!-- Invoices Table -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<h3 style="margin:0;color:#1A1A2E;"><i data-lucide="receipt" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> الفواتير</h3>
<?php if (in_array($cStatus, ['approved','active']) && can('rental.manage_contract')): ?>
<a href="/rentals/contracts/<?= (int) $contract->id ?>/invoices/create" class="btn btn-primary" style="font-size:13px;padding:6px 12px;">+ فاتورة جديدة</a>
<?php endif; ?>
</div>
<?php if (!empty($invoices)): ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;">رقم الفاتورة</th>
<th style="padding:12px 16px;text-align:right;">الفترة</th>
<th style="padding:12px 16px;text-align:right;">الاستحقاق</th>
<th style="padding:12px 16px;text-align:left;">إيجار</th>
<th style="padding:12px 16px;text-align:left;">مرافق</th>
<th style="padding:12px 16px;text-align:left;">VAT</th>
<th style="padding:12px 16px;text-align:left;">غرامة</th>
<th style="padding:12px 16px;text-align:left;">الإجمالي</th>
<th style="padding:12px 16px;text-align:center;">الحالة</th>
<th style="padding:12px 16px;text-align:center;">إجراء</th>
</tr>
</thead>
<tbody>
<?php foreach ($invoices as $inv):
$inv = (object) $inv;
$iStatus = $inv->status ?? 'unpaid';
$iColor = RentalInvoice::getStatusColor($iStatus);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 16px;"><a href="/rentals/invoices/<?= (int) $inv->id ?>" style="color:#0D7377;font-weight:600;"><?= e($inv->invoice_number ?? '') ?></a></td>
<td style="padding:10px 16px;font-size:12px;"><?= e($inv->period_start ?? '') ?><?= e($inv->period_end ?? '') ?></td>
<td style="padding:10px 16px;"><?= e($inv->due_date ?? '') ?></td>
<td style="padding:10px 16px;"><?= money((float) ($inv->base_amount ?? 0)) ?></td>
<td style="padding:10px 16px;"><?= money((float) ($inv->utilities_amount ?? 0)) ?></td>
<td style="padding:10px 16px;"><?= money((float) ($inv->vat_amount ?? 0)) ?></td>
<td style="padding:10px 16px;color:#DC2626;"><?= money((float) ($inv->late_fee_amount ?? 0)) ?></td>
<td style="padding:10px 16px;font-weight:700;"><?= money((float) ($inv->total_amount ?? 0)) ?></td>
<td style="padding:10px 16px;text-align:center;">
<span class="badge" style="background:<?= $iColor ?>15;color:<?= $iColor ?>;font-size:12px;padding:3px 10px;border-radius:10px;font-weight:600;">
<?= e(RentalInvoice::getStatusLabel($iStatus)) ?>
</span>
</td>
<td style="padding:10px 16px;text-align:center;">
<a href="/rentals/invoices/<?= (int) $inv->id ?>" style="color:#0D7377;font-size:12px;">عرض</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;">
<i data-lucide="file-x" style="width:40px;height:40px;color:#D1D5DB;display:block;margin:0 auto 10px;"></i>
<p style="color:#9CA3AF;font-size:14px;margin:0;">لا توجد فواتير بعد</p>
</div>
<?php endif; ?>
</div>
<!-- Bookings Table -->
<div class="card" style="padding:0;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
......@@ -157,36 +251,26 @@ $timeTiers = ['AM' => 'صباحي', 'PM' => 'مسائي'];
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">التاريخ</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">من</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">إلى</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">الحالة</th>
<th style="padding:12px 16px;text-align:right;">التاريخ</th>
<th style="padding:12px 16px;text-align:right;">من</th>
<th style="padding:12px 16px;text-align:right;">إلى</th>
<th style="padding:12px 16px;text-align:center;">الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($bookings as $b):
$bStatus = $b['status'] ?? 'pending';
$bStatusColors = [
'pending' => '#D97706',
'confirmed' => '#059669',
'cancelled' => '#DC2626',
'completed' => '#0284C7',
];
$bStatusLabels = [
'pending' => 'قيد الانتظار',
'confirmed' => 'مؤكد',
'cancelled' => 'ملغى',
'completed' => 'مكتمل',
];
$bColor = $bStatusColors[$bStatus] ?? '#6B7280';
$bLabel = $bStatusLabels[$bStatus] ?? $bStatus;
$bStatus = $b['status'] ?? 'scheduled';
$bColors = ['scheduled'=>'#D97706','completed'=>'#059669','cancelled'=>'#DC2626','no_show'=>'#6B7280'];
$bLabels = ['scheduled'=>'مجدول','completed'=>'مكتمل','cancelled'=>'ملغى','no_show'=>'لم يحضر'];
$bColor = $bColors[$bStatus] ?? '#6B7280';
$bLabel = $bLabels[$bStatus] ?? $bStatus;
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;"><?= e($b['booking_date'] ?? '') ?></td>
<td style="padding:12px 16px;white-space:nowrap;"><?= e(substr($b['start_time'] ?? '', 0, 5)) ?></td>
<td style="padding:12px 16px;white-space:nowrap;"><?= e(substr($b['end_time'] ?? '', 0, 5)) ?></td>
<td style="padding:12px 16px;text-align:center;">
<span class="badge" style="background:<?= $bColor ?>15;color:<?= $bColor ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<td style="padding:10px 16px;"><?= e($b['booking_date'] ?? '') ?></td>
<td style="padding:10px 16px;"><?= e(substr($b['start_time'] ?? '', 0, 5)) ?></td>
<td style="padding:10px 16px;"><?= e(substr($b['end_time'] ?? '', 0, 5)) ?></td>
<td style="padding:10px 16px;text-align:center;">
<span class="badge" style="background:<?= $bColor ?>15;color:<?= $bColor ?>;font-size:12px;padding:3px 10px;border-radius:10px;font-weight:600;">
<?= e($bLabel) ?>
</span>
</td>
......@@ -197,8 +281,8 @@ $timeTiers = ['AM' => 'صباحي', 'PM' => 'مسائي'];
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;">
<i data-lucide="calendar-x" style="width:40px;height:40px;color:#D1D5DB;"></i>
<p style="color:#9CA3AF;font-size:14px;margin:10px 0 0;">لا توجد حجوزات لهذا العقد</p>
<i data-lucide="calendar-x" style="width:40px;height:40px;color:#D1D5DB;display:block;margin:0 auto 10px;"></i>
<p style="color:#9CA3AF;font-size:14px;margin:0;">لا توجد حجوزات لهذا العقد</p>
</div>
<?php endif; ?>
</div>
......
<?php
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>فاتورة إيجار جديدة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/rentals/contracts/<?= (int) $contract->id ?>" class="btn btn-outline">
<i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العودة للعقد
</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Summary Banner -->
<div class="card" style="margin-bottom:20px;padding:15px 20px;background:#F0FDF4;border:1px solid #BBF7D0;">
<div style="display:flex;align-items:center;gap:20px;flex-wrap:wrap;">
<code style="background:#fff;color:#0D7377;padding:4px 10px;border-radius:4px;font-weight:600;"><?= e($contract->contract_number) ?></code>
<div style="display:flex;gap:30px;font-size:14px;color:#374151;">
<span>إيجار شهري: <strong><?= money($monthly_base) ?></strong></span>
<?php if ($monthly_utils > 0): ?>
<span>مرافق: <strong style="color:#7C3AED;"><?= money($monthly_utils) ?></strong></span>
<?php endif; ?>
<span>VAT (<?= (float) ($contract->vat_percentage ?? 1) ?>%): <strong style="color:#D97706;"><?= money($monthly_vat) ?></strong></span>
<span>الإجمالي: <strong style="color:#059669;font-size:16px;"><?= money($monthly_total) ?></strong></span>
</div>
</div>
</div>
<form method="POST" action="/rentals/contracts/<?= (int) $contract->id ?>/invoices">
<?= \App\Core\CSRF::field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">بيانات الفاتورة</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div>
<label class="form-label">بداية الفترة <span style="color:#DC2626;">*</span></label>
<input type="date" name="period_start" value="<?= e(old('period_start')) ?>" class="form-input" required>
</div>
<div>
<label class="form-label">نهاية الفترة <span style="color:#DC2626;">*</span></label>
<input type="date" name="period_end" value="<?= e(old('period_end')) ?>" class="form-input" required>
</div>
<div>
<label class="form-label">تاريخ الاستحقاق <span style="color:#DC2626;">*</span></label>
<input type="date" name="due_date" value="<?= e(old('due_date')) ?>" class="form-input" required>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2" style="resize:vertical;"><?= e(old('notes')) ?></textarea>
</div>
</div>
</div>
<!-- Preview -->
<div class="card" style="margin-bottom:20px;padding:20px;background:#F9FAFB;">
<h4 style="margin:0 0 12px;color:#374151;">تفصيل الفاتورة</h4>
<table style="width:100%;font-size:14px;border-collapse:collapse;">
<tr style="border-bottom:1px solid #E5E7EB;">
<td style="padding:8px 0;color:#6B7280;">الإيجار الشهري</td>
<td style="padding:8px 0;text-align:left;font-weight:600;"><?= money($monthly_base) ?></td>
</tr>
<?php if ($monthly_utils > 0): ?>
<tr style="border-bottom:1px solid #E5E7EB;">
<td style="padding:8px 0;color:#6B7280;">مرافق (<?= (float) ($contract->utilities_percentage ?? 0) ?>%)</td>
<td style="padding:8px 0;text-align:left;font-weight:600;color:#7C3AED;"><?= money($monthly_utils) ?></td>
</tr>
<?php endif; ?>
<tr style="border-bottom:1px solid #E5E7EB;">
<td style="padding:8px 0;color:#6B7280;">القيمة المضافة (<?= (float) ($contract->vat_percentage ?? 1) ?>%)</td>
<td style="padding:8px 0;text-align:left;font-weight:600;color:#D97706;"><?= money($monthly_vat) ?></td>
</tr>
<?php if (($contract->late_fee_type ?? 'none') !== 'none'): ?>
<tr style="border-bottom:1px solid #E5E7EB;">
<td style="padding:8px 0;color:#6B7280;">غرامة التأخير</td>
<td style="padding:8px 0;text-align:left;color:#DC2626;font-size:12px;">تُحسب عند الدفع إن تأخر</td>
</tr>
<?php endif; ?>
<tr>
<td style="padding:10px 0;font-weight:700;font-size:15px;">الإجمالي</td>
<td style="padding:10px 0;text-align:left;font-weight:700;font-size:18px;color:#059669;"><?= money($monthly_total) ?></td>
</tr>
</table>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">إنشاء الفاتورة</button>
<a href="/rentals/contracts/<?= (int) $contract->id ?>" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Rentals\Models\RentalInvoice;
use App\Modules\Rentals\Models\RentalContract;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($invoice->invoice_number ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($contract): ?>
<a href="/rentals/contracts/<?= (int) $contract->id ?>" class="btn btn-outline">
<i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العودة للعقد
</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$iStatus = $invoice->status ?? 'unpaid';
$iColor = RentalInvoice::getStatusColor($iStatus);
?>
<!-- Header -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
<div>
<div style="margin-bottom:6px;">
<code style="font-size:14px;background:#F0F9FF;color:#0284C7;padding:4px 12px;border-radius:4px;font-weight:600;"><?= e($invoice->invoice_number ?? '') ?></code>
</div>
<div style="font-size:14px;color:#374151;">
<?php if ($entity): ?>
<strong>الجهة:</strong> <?= e($entity->name_ar) ?>
&nbsp;|&nbsp;
<?php endif; ?>
<?php if ($contract): ?>
<strong>العقد:</strong> <a href="/rentals/contracts/<?= (int) $contract->id ?>" style="color:#0D7377;"><?= e($contract->contract_number) ?></a>
<?php endif; ?>
</div>
</div>
<span class="badge" style="background:<?= $iColor ?>15;color:<?= $iColor ?>;font-size:13px;padding:5px 14px;border-radius:10px;font-weight:600;">
<?= e(RentalInvoice::getStatusLabel($iStatus)) ?>
</span>
</div>
</div>
<!-- Financial Breakdown -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;"><i data-lucide="receipt" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> تفصيل الفاتورة</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<table style="font-size:14px;border-collapse:collapse;width:100%;">
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#6B7280;">الفترة</td>
<td style="padding:10px 0;font-weight:600;"><?= e($invoice->period_start ?? '') ?><?= e($invoice->period_end ?? '') ?></td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#6B7280;">تاريخ الاستحقاق</td>
<td style="padding:10px 0;font-weight:600;"><?= e($invoice->due_date ?? '') ?></td>
</tr>
<?php if ($iStatus === 'paid'): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#6B7280;">تاريخ الدفع</td>
<td style="padding:10px 0;font-weight:600;color:#059669;"><?= e($invoice->paid_at ?? '') ?></td>
</tr>
<?php endif; ?>
<?php if (!empty($invoice->notes)): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">ملاحظات</td>
<td style="padding:10px 0;"><?= nl2br(e($invoice->notes)) ?></td>
</tr>
<?php endif; ?>
</table>
<div style="background:#F9FAFB;border-radius:8px;padding:20px;">
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #E5E7EB;">
<span style="color:#6B7280;">الإيجار الأساسي</span>
<span style="font-weight:600;"><?= money((float) ($invoice->base_amount ?? 0)) ?></span>
</div>
<?php if ((float) ($invoice->utilities_amount ?? 0) > 0): ?>
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #E5E7EB;">
<span style="color:#6B7280;">المرافق</span>
<span style="font-weight:600;color:#7C3AED;"><?= money((float) ($invoice->utilities_amount ?? 0)) ?></span>
</div>
<?php endif; ?>
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #E5E7EB;">
<span style="color:#6B7280;">القيمة المضافة</span>
<span style="font-weight:600;color:#D97706;"><?= money((float) ($invoice->vat_amount ?? 0)) ?></span>
</div>
<?php if ((float) ($invoice->late_fee_amount ?? 0) > 0): ?>
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #E5E7EB;">
<span style="color:#DC2626;">غرامة التأخير</span>
<span style="font-weight:600;color:#DC2626;"><?= money((float) ($invoice->late_fee_amount ?? 0)) ?></span>
</div>
<?php endif; ?>
<div style="display:flex;justify-content:space-between;padding:12px 0;margin-top:4px;border-top:2px solid #E5E7EB;">
<span style="font-weight:700;font-size:15px;">الإجمالي</span>
<span style="font-weight:700;font-size:20px;color:#059669;"><?= money((float) ($invoice->total_amount ?? 0)) ?></span>
</div>
</div>
</div>
</div>
</div>
<!-- Pay Action -->
<?php if ($iStatus === 'unpaid' && can('rental.manage_contract')): ?>
<div class="card" style="margin-bottom:20px;padding:20px;background:#F0FDF4;border:1px solid #BBF7D0;">
<h4 style="margin:0 0 15px;color:#059669;"><i data-lucide="banknote" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> تسجيل الدفع</h4>
<form method="POST" action="/rentals/invoices/<?= (int) $invoice->id ?>/pay" style="display:grid;grid-template-columns:1fr 1fr auto;gap:12px;align-items:end;">
<?= \App\Core\CSRF::field() ?>
<div>
<label class="form-label">رقم الدفعة <span style="color:#DC2626;">*</span></label>
<input type="number" name="payment_id" class="form-input" required placeholder="رقم الدفعة من النظام">
</div>
<div>
<label class="form-label">تاريخ الدفع</label>
<input type="datetime-local" name="paid_at" class="form-input" value="<?= date('Y-m-d\TH:i') ?>">
</div>
<button type="submit" class="btn btn-primary">تسجيل الدفع</button>
</form>
<?php if ($contract && ($contract->late_fee_type ?? 'none') !== 'none'): ?>
<div style="margin-top:10px;font-size:12px;color:#6B7280;">
<i data-lucide="info" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>
سيتم احتساب غرامة التأخير تلقائياً إن كان الدفع بعد تاريخ الاستحقاق
(<?= e(\App\Modules\Rentals\Models\RentalContract::getLateFeeTtypes()[$contract->late_fee_type ?? 'none'] ?? '') ?>
<?= (float) ($contract->late_fee_rate ?? 0) ?>% لكل فترة)
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($iStatus === 'overdue' && can('rental.manage_contract')): ?>
<div class="card" style="margin-bottom:20px;padding:20px;background:#FEF2F2;border:1px solid #FECACA;">
<h4 style="margin:0 0 15px;color:#DC2626;"><i data-lucide="alert-triangle" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> فاتورة متأخرة — تسجيل الدفع</h4>
<form method="POST" action="/rentals/invoices/<?= (int) $invoice->id ?>/pay" style="display:grid;grid-template-columns:1fr 1fr auto;gap:12px;align-items:end;">
<?= \App\Core\CSRF::field() ?>
<div>
<label class="form-label">رقم الدفعة <span style="color:#DC2626;">*</span></label>
<input type="number" name="payment_id" class="form-input" required placeholder="رقم الدفعة">
</div>
<div>
<label class="form-label">تاريخ الدفع</label>
<input type="datetime-local" name="paid_at" class="form-input" value="<?= date('Y-m-d\TH:i') ?>">
</div>
<button type="submit" class="btn btn-primary" style="background:#DC2626;">تسجيل الدفع + الغرامة</button>
</form>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE rental_contracts
ADD COLUMN vat_percentage decimal(5,2) NOT NULL DEFAULT 1.00 AFTER total_amount,
ADD COLUMN vat_amount decimal(15,2) NOT NULL DEFAULT 0.00 AFTER vat_percentage,
ADD COLUMN utilities_percentage decimal(5,2) NOT NULL DEFAULT 0.00 AFTER vat_amount,
ADD COLUMN utilities_amount decimal(15,2) NOT NULL DEFAULT 0.00 AFTER utilities_percentage,
ADD COLUMN grand_total decimal(15,2) NOT NULL DEFAULT 0.00 AFTER utilities_amount,
ADD COLUMN late_fee_type varchar(10) NOT NULL DEFAULT 'none' AFTER grand_total,
ADD COLUMN late_fee_rate decimal(5,2) NOT NULL DEFAULT 0.00 AFTER late_fee_type;
",
'down' => "
ALTER TABLE rental_contracts
DROP COLUMN vat_percentage,
DROP COLUMN vat_amount,
DROP COLUMN utilities_percentage,
DROP COLUMN utilities_amount,
DROP COLUMN grand_total,
DROP COLUMN late_fee_type,
DROP COLUMN late_fee_rate;
",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE rental_invoices (
id bigint unsigned NOT NULL AUTO_INCREMENT,
invoice_number varchar(50) NOT NULL,
contract_id bigint unsigned NOT NULL,
entity_id bigint unsigned NOT NULL,
period_start date NOT NULL,
period_end date NOT NULL,
due_date date NOT NULL,
base_amount decimal(15,2) NOT NULL DEFAULT 0.00,
utilities_amount decimal(15,2) NOT NULL DEFAULT 0.00,
vat_amount decimal(15,2) NOT NULL DEFAULT 0.00,
late_fee_amount decimal(15,2) NOT NULL DEFAULT 0.00,
total_amount decimal(15,2) NOT NULL DEFAULT 0.00,
status varchar(20) NOT NULL DEFAULT 'unpaid',
paid_at timestamp NULL DEFAULT NULL,
payment_id bigint unsigned NULL DEFAULT NULL,
notes text NULL,
journal_entry_id bigint unsigned NULL DEFAULT NULL,
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by bigint unsigned NULL,
updated_by bigint unsigned NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_rental_invoices_number (invoice_number),
KEY idx_rental_invoices_contract (contract_id),
KEY idx_rental_invoices_entity (entity_id),
KEY idx_rental_invoices_status (status),
KEY idx_rental_invoices_due_date (due_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
",
'down' => "DROP TABLE IF EXISTS rental_invoices;",
];
# Rentals Module — Architecture Map
> **Last updated:** 2026-06-10
> **Last updated:** 2026-07-18
> **Status:** Living document — incrementally updated as new information is discovered
---
......@@ -27,24 +27,28 @@ It does **NOT** directly manage:
```
app/Modules/Rentals/
├── bootstrap.php # Permission registration (5 permissions)
├── Routes.php # Web routes (13 routes)
├── bootstrap.php # Permission + MenuRegistry (sidebar under العمليات, order 290)
├── Routes.php # Web routes (17 routes)
├── Controllers/
│ └── RentalController.php # Single controller (entities + contracts + deposits)
│ └── RentalController.php # Single controller (entities + contracts + deposits + invoices)
├── Models/
│ ├── RentalEntity.php # Tenant entity model (soft delete)
│ ├── RentalContract.php # Contract model (soft delete)
│ └── RentalBooking.php # Individual booking slots (no soft delete)
│ ├── RentalBooking.php # Individual booking slots (no soft delete)
│ └── RentalInvoice.php # Monthly invoice model (no soft delete)
├── Services/
│ ├── RentalContractService.php # Contract creation, activation, booking generation
│ └── RentalDepositService.php # Deposit collect/refund lifecycle
│ ├── RentalDepositService.php # Deposit collect/refund lifecycle
│ └── RentalInvoiceService.php # Invoice generation, late fee calc, mark paid
└── Views/
├── index.php # Contract list with filters
├── entities.php # Entity list with filters
├── entity_form.php # Create/edit entity form
├── entity_show.php # Entity detail with contracts
├── contract_form.php # Create contract form
└── contract_show.php # Contract detail with bookings + deposit status
├── contract_form.php # Create contract form (VAT + utilities + late fee + live calc)
├── contract_show.php # Contract detail with invoices + bookings + deposit
├── invoice_form.php # Create invoice for a contract
└── invoice_show.php # Invoice detail + pay action
```
---
......@@ -94,8 +98,15 @@ app/Modules/Rentals/
| subtotal | decimal(15,2) | NO | | 0.00 | units * rate |
| discount_percentage | decimal(5,2) | NO | | 0.00 | Bulk discount % |
| discount_amount | decimal(15,2) | NO | | 0.00 | |
| total_amount | decimal(15,2) | NO | | 0.00 | subtotal - discount |
| deposit_percentage | decimal(5,2) | NO | | 10.00 | % of total for deposit |
| total_amount | decimal(15,2) | NO | | 0.00 | subtotal - discount (net rental) |
| vat_percentage | decimal(5,2) | NO | | 1.00 | Always 1% by law |
| vat_amount | decimal(15,2) | NO | | 0.00 | total_amount * vat_percentage / 100 |
| utilities_percentage | decimal(5,2) | NO | | 0.00 | % of monthly base for utilities |
| utilities_amount | decimal(15,2) | NO | | 0.00 | total_amount * utilities_percentage / 100 |
| grand_total | decimal(15,2) | NO | | 0.00 | total + utilities + vat (full contract) |
| late_fee_type | varchar(10) | NO | | none | none/daily/weekly/monthly |
| late_fee_rate | decimal(5,2) | NO | | 0.00 | % of invoice total per late period |
| deposit_percentage | decimal(5,2) | NO | | 10.00 | % of grand_total for deposit |
| deposit_amount | decimal(15,2) | NO | | 0.00 | |
| deposit_status | varchar(20) | NO | | pending | pending/collected/partially_refunded/refunded |
| deposit_payment_id | bigint unsigned | YES | MUL | | FK to external payment |
......@@ -113,6 +124,32 @@ app/Modules/Rentals/
| created_by | bigint unsigned | YES | | | |
| updated_by | bigint unsigned | YES | | | |
### 3.4 `rental_invoices` Table
| Column | Type | Nullable | Key | Default | Notes |
|--------|------|----------|-----|---------|-------|
| id | bigint unsigned | NO | PRI | auto_increment | |
| invoice_number | varchar(50) | NO | UNI | | Format: RI-YYYYMM-XXXX |
| contract_id | bigint unsigned | NO | MUL | | FK to rental_contracts |
| entity_id | bigint unsigned | NO | MUL | | FK to rental_entities |
| period_start | date | NO | | | Billing period start |
| period_end | date | NO | | | Billing period end |
| due_date | date | NO | MUL | | Payment due date |
| base_amount | decimal(15,2) | NO | | 0.00 | Monthly share of net rental |
| utilities_amount | decimal(15,2) | NO | | 0.00 | Utilities surcharge |
| vat_amount | decimal(15,2) | NO | | 0.00 | 1% of (base + utilities) |
| late_fee_amount | decimal(15,2) | NO | | 0.00 | Set at payment if overdue |
| total_amount | decimal(15,2) | NO | | 0.00 | base + utilities + vat + late_fee |
| status | varchar(20) | NO | MUL | unpaid | unpaid/paid/overdue/cancelled |
| paid_at | timestamp | YES | | | |
| payment_id | bigint unsigned | YES | | | FK to external payment |
| notes | text | YES | | | |
| journal_entry_id | bigint unsigned | YES | | | FK to journal_entries |
| created_at | timestamp | NO | | CURRENT_TIMESTAMP | |
| updated_at | timestamp | NO | | CURRENT_TIMESTAMP | On update |
| created_by | bigint unsigned | YES | | | |
| updated_by | bigint unsigned | YES | | | |
### 3.3 `rental_bookings` Table
| Column | Type | Nullable | Key | Default | Notes |
......@@ -147,6 +184,10 @@ app/Modules/Rentals/
| POST | /rentals/contracts/{id}/approve | RentalController@approveContract | rental.approve |
| POST | /rentals/contracts/{id}/deposit/collect | RentalController@collectDeposit | rental.manage_deposit |
| POST | /rentals/contracts/{id}/deposit/refund | RentalController@refundDeposit | rental.manage_deposit |
| GET | /rentals/contracts/{id}/invoices/create | RentalController@createInvoice | rental.manage_contract |
| POST | /rentals/contracts/{id}/invoices | RentalController@storeInvoice | rental.manage_contract |
| GET | /rentals/invoices/{id} | RentalController@showInvoice | rental.view |
| POST | /rentals/invoices/{id}/pay | RentalController@payInvoice | rental.manage_contract |
---
......@@ -200,6 +241,7 @@ partially_refunded → refunded (on refund payment processing)
| `rental.deposit_collected` | `{contract_id, payment_id}` | RentalDepositService::collectDeposit |
| `rental.deposit_refund_requested` | `{contract_id}` | RentalDepositService::requestRefund |
| `rental.deposit_refunded` | `{contract_id, payment_id}` | RentalDepositService::processRefund |
| `rental.invoice_paid` | `{invoice_id, contract_id, payment_id, base_amount, utilities_amount, vat_amount, late_fee_amount, total_amount, paid_at}` | RentalInvoiceService::markPaid |
---
......@@ -209,6 +251,7 @@ partially_refunded → refunded (on refund payment processing)
|-------|----------------|--------|
| `rental.deposit_collected` | Accounting | Auto-post journal entry for deposit liability |
| `rental.deposit_refunded` | Accounting | Auto-post journal entry for deposit refund |
| `rental.invoice_paid` | Accounting | Dr. Cash, Cr. RentalRevenue(410521) + ServiceRevenue(410515) + TaxPayable(230804) + FineRevenue(410512) |
---
......@@ -258,9 +301,10 @@ partially_refunded → refunded (on refund payment processing)
- The Reservations module queries `rental_bookings` directly via raw SQL
- If the Rentals table schema changes, the Reservations conflict detector will break silently (wrapped in try/catch)
### 10.4 No Menu Registration
- The Rentals module does NOT register a sidebar menu in `bootstrap.php`
- Only permissions are registered — navigation must be handled elsewhere or manually
### 10.4 Invoice Late Fee Applied at Payment Time
- Late fee is NOT stored on the invoice at generation — it is calculated in `RentalInvoiceService::calcLateFee()` when `markPaid()` is called
- If `paid_at``due_date`, late fee = 0
- If overdue: periods = ceil(days / period_length); fee = total × rate% × periods
---
......
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