Commit d3e3030e authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add messaging update: 78 notification templates, 14 event listeners, public document links

- Seed 78 notification templates (26 events × 3 channels: in_app, email, sms) in Arabic
- Create 14 new listeners for previously unwired events (invoice sent/cancelled, wallet deposit/frozen, cash session closed, enrollment completed, session completed, participant registered/status changed/suspended/frozen, evaluation shared, POS receipt)
- Wire all listeners in EventServiceProvider
- Add PublicDocumentController with signed URL routes for invoices, receipts, and POS receipts
- Add PublicLinkHelper for generating 30-day signed URLs
- Add 3 standalone RTL Arabic public views (invoice, receipt, pos-receipt)
- Update existing listeners (SendInvoiceNotification, SendOverdueReminder, SendPaymentConfirmation) to include public_link/receipt_link variables
- Align all listener variable names with template placeholders
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent e11ab21d
<?php
namespace App\Domain\Financial\Listeners;
use App\Domain\Financial\Events\CashSessionClosed;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendCashSessionClosedNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(CashSessionClosed $event): void
{
try {
$this->notificationService->sendToRole(
type: 'cash_session_closed',
role: 'academy_admin',
data: [
'cashier_name' => $event->actor->name,
'session_date' => $event->cashSession->opened_at?->format('Y-m-d') ?? now()->format('Y-m-d'),
'total_sales' => number_format(($event->cashSession->total_sales ?? 0) / 100, 2),
'transactions_count' => (string) ($event->cashSession->transactions_count ?? 0),
],
);
} catch (\Throwable $e) {
Log::error('SendCashSessionClosedNotification failed: ' . $e->getMessage(), [
'cash_session_id' => $event->cashSession->id,
]);
}
}
public function failed(CashSessionClosed $event, \Throwable $exception): void
{
Log::critical('SendCashSessionClosedNotification PERMANENTLY FAILED', [
'cash_session_id' => $event->cashSession->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Financial\Listeners;
use App\Domain\Financial\Events\InvoiceCancelled;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendInvoiceCancelledNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(InvoiceCancelled $event): void
{
try {
$this->notificationService->sendSimple(
type: 'invoice_cancelled',
recipientId: $event->invoice->billable_id,
recipientType: $event->invoice->billable_type,
data: [
'participant_name' => $event->invoice->billable?->person?->full_name ?? '',
'invoice_number' => $event->invoice->number,
'reason' => $event->invoice->cancellation_reason ?? '',
],
);
} catch (\Throwable $e) {
Log::error('SendInvoiceCancelledNotification failed: ' . $e->getMessage(), [
'invoice_id' => $event->invoice->id,
]);
}
}
public function failed(InvoiceCancelled $event, \Throwable $exception): void
{
Log::critical('SendInvoiceCancelledNotification PERMANENTLY FAILED', [
'invoice_id' => $event->invoice->id,
'error' => $exception->getMessage(),
]);
}
}
......@@ -4,6 +4,7 @@
use App\Domain\Financial\Events\InvoiceCreated;
use App\Domain\Notification\Services\NotificationService;
use App\Domain\Shared\Helpers\PublicLinkHelper;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
......@@ -21,9 +22,11 @@ public function handle(InvoiceCreated $event): void
recipientId: $event->invoice->billable_id,
recipientType: $event->invoice->billable_type,
data: [
'participant_name' => $event->invoice->billable?->person?->full_name ?? '',
'invoice_number' => $event->invoice->number,
'total_amount' => $event->invoice->total_amount,
'total_amount' => number_format($event->invoice->total_amount / 100, 2),
'due_date' => $event->invoice->due_date?->format('Y-m-d'),
'public_link' => PublicLinkHelper::invoiceLink($event->invoice),
],
);
} catch (\Throwable $e) {
......
<?php
namespace App\Domain\Financial\Listeners;
use App\Domain\Financial\Events\InvoiceSent;
use App\Domain\Notification\Services\NotificationService;
use App\Domain\Shared\Helpers\PublicLinkHelper;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendInvoiceSentNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(InvoiceSent $event): void
{
try {
$this->notificationService->sendSimple(
type: 'invoice_sent',
recipientId: $event->invoice->billable_id,
recipientType: $event->invoice->billable_type,
data: [
'participant_name' => $event->invoice->billable?->person?->full_name ?? '',
'invoice_number' => $event->invoice->number,
'total_amount' => number_format($event->invoice->total_amount / 100, 2),
'due_date' => $event->invoice->due_date?->format('Y-m-d'),
'public_link' => PublicLinkHelper::invoiceLink($event->invoice),
],
);
} catch (\Throwable $e) {
Log::error('SendInvoiceSentNotification failed: ' . $e->getMessage(), [
'invoice_id' => $event->invoice->id,
]);
}
}
public function failed(InvoiceSent $event, \Throwable $exception): void
{
Log::critical('SendInvoiceSentNotification PERMANENTLY FAILED', [
'invoice_id' => $event->invoice->id,
'error' => $exception->getMessage(),
]);
}
}
......@@ -4,6 +4,7 @@
use App\Domain\Financial\Events\InvoiceOverdue;
use App\Domain\Notification\Services\NotificationService;
use App\Domain\Shared\Helpers\PublicLinkHelper;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
......@@ -21,11 +22,13 @@ public function handle(InvoiceOverdue $event): void
recipientId: $event->invoice->billable_id,
recipientType: $event->invoice->billable_type,
data: [
'participant_name' => $event->invoice->billable?->person?->full_name ?? '',
'invoice_number' => $event->invoice->number,
'total_amount' => $event->invoice->total_amount,
'balance_due' => $event->invoice->due_amount,
'total_amount' => number_format($event->invoice->total_amount / 100, 2),
'balance_due' => number_format($event->invoice->due_amount / 100, 2),
'due_date' => $event->invoice->due_date?->format('Y-m-d'),
'days_overdue' => $event->invoice->due_date?->diffInDays(now()),
'days_overdue' => (string) ($event->invoice->due_date?->diffInDays(now()) ?? 0),
'public_link' => PublicLinkHelper::invoiceLink($event->invoice),
],
);
} catch (\Throwable $e) {
......
......@@ -4,6 +4,7 @@
use App\Domain\Financial\Events\PaymentReceived;
use App\Domain\Notification\Services\NotificationService;
use App\Domain\Shared\Helpers\PublicLinkHelper;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
......@@ -21,10 +22,11 @@ public function handle(PaymentReceived $event): void
recipientId: $event->payment->payer_id,
recipientType: $event->payment->payer_type,
data: [
'payment_reference' => $event->payment->reference,
'amount' => $event->payment->amount,
'method' => $event->payment->method,
'invoice_number' => $event->payment->invoice?->number,
'participant_name' => $event->payment->invoice?->billable?->person?->full_name ?? '',
'payment_amount' => number_format($event->payment->amount / 100, 2),
'payment_method' => $event->payment->method?->value ?? $event->payment->method ?? '',
'invoice_number' => $event->payment->invoice?->number ?? '',
'receipt_link' => PublicLinkHelper::receiptLink($event->payment),
],
);
} catch (\Throwable $e) {
......
<?php
namespace App\Domain\Financial\Listeners;
use App\Domain\Financial\Events\PaymentPlanDefaulted;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendPaymentPlanDefaultedNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(PaymentPlanDefaulted $event): void
{
try {
$invoice = $event->paymentPlan->invoice;
$this->notificationService->sendSimple(
type: 'payment_plan_defaulted',
recipientId: $invoice->billable_id,
recipientType: $invoice->billable_type,
data: [
'participant_name' => $invoice->billable?->person?->full_name ?? '',
'plan_id' => (string) $event->paymentPlan->id,
'overdue_amount' => number_format(($event->paymentPlan->installments()->where('status', 'overdue')->sum('amount') ?? 0) / 100, 2),
'installment_date' => $event->paymentPlan->installments()->where('status', 'overdue')->first()?->due_date?->format('Y-m-d') ?? '',
],
priority: 'high',
);
} catch (\Throwable $e) {
Log::error('SendPaymentPlanDefaultedNotification failed: ' . $e->getMessage(), [
'payment_plan_id' => $event->paymentPlan->id,
]);
}
}
public function failed(PaymentPlanDefaulted $event, \Throwable $exception): void
{
Log::critical('SendPaymentPlanDefaultedNotification PERMANENTLY FAILED', [
'payment_plan_id' => $event->paymentPlan->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Financial\Listeners;
use App\Domain\Financial\Events\WalletDeposited;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendWalletDepositNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(WalletDeposited $event): void
{
try {
$this->notificationService->sendSimple(
type: 'wallet_deposited',
recipientId: $event->wallet->participant_id,
recipientType: 'participant',
data: [
'participant_name' => $event->wallet->participant?->person?->full_name ?? '',
'amount' => number_format($event->amount / 100, 2),
'new_balance' => number_format($event->wallet->balance / 100, 2),
],
);
} catch (\Throwable $e) {
Log::error('SendWalletDepositNotification failed: ' . $e->getMessage(), [
'wallet_id' => $event->wallet->id,
]);
}
}
public function failed(WalletDeposited $event, \Throwable $exception): void
{
Log::critical('SendWalletDepositNotification PERMANENTLY FAILED', [
'wallet_id' => $event->wallet->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Financial\Listeners;
use App\Domain\Financial\Events\WalletFrozen;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendWalletFrozenNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(WalletFrozen $event): void
{
try {
$this->notificationService->sendSimple(
type: 'wallet_frozen',
recipientId: $event->wallet->participant_id,
recipientType: 'participant',
data: [
'participant_name' => $event->wallet->participant?->person?->full_name ?? '',
'reason' => $event->wallet->freeze_reason ?? 'قرار إداري',
],
priority: 'high',
);
} catch (\Throwable $e) {
Log::error('SendWalletFrozenNotification failed: ' . $e->getMessage(), [
'wallet_id' => $event->wallet->id,
]);
}
}
public function failed(WalletFrozen $event, \Throwable $exception): void
{
Log::critical('SendWalletFrozenNotification PERMANENTLY FAILED', [
'wallet_id' => $event->wallet->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\POS\Listeners;
use App\Domain\POS\Events\POSTransactionCompleted;
use App\Domain\Notification\Services\NotificationService;
use App\Domain\Shared\Helpers\PublicLinkHelper;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendPOSReceiptNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(POSTransactionCompleted $event): void
{
try {
$participant = $event->transaction->participant;
if (!$participant) {
return;
}
$this->notificationService->sendSimple(
type: 'pos_transaction_completed',
recipientId: $participant->id,
recipientType: 'participant',
data: [
'participant_name' => $participant->person?->full_name ?? '',
'transaction_number' => $event->transaction->receipt_number ?? $event->transaction->number ?? '',
'total_amount' => number_format(($event->transaction->total_amount ?? 0) / 100, 2),
'items_count' => (string) ($event->transaction->items_count ?? $event->transaction->items()->count()),
'receipt_link' => PublicLinkHelper::posReceiptLink($event->transaction),
],
);
} catch (\Throwable $e) {
Log::error('SendPOSReceiptNotification failed: ' . $e->getMessage(), [
'transaction_id' => $event->transaction->id,
]);
}
}
public function failed(POSTransactionCompleted $event, \Throwable $exception): void
{
Log::critical('SendPOSReceiptNotification PERMANENTLY FAILED', [
'transaction_id' => $event->transaction->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Participant\Listeners;
use App\Domain\Participant\Events\ParticipantFrozen;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendParticipantFrozenNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(ParticipantFrozen $event): void
{
try {
$guardian = $event->participant->primaryGuardian;
if (!$guardian) {
return;
}
$this->notificationService->sendSimple(
type: 'participant_frozen',
recipientId: $guardian->id,
recipientType: 'guardian',
data: [
'participant_name' => $event->participant->person?->full_name ?? '',
'reason' => $event->reason,
'freeze_date' => now()->format('Y-m-d'),
],
priority: 'high',
);
} catch (\Throwable $e) {
Log::error('SendParticipantFrozenNotification failed: ' . $e->getMessage(), [
'participant_id' => $event->participant->id,
]);
}
}
public function failed(ParticipantFrozen $event, \Throwable $exception): void
{
Log::critical('SendParticipantFrozenNotification PERMANENTLY FAILED', [
'participant_id' => $event->participant->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Participant\Listeners;
use App\Domain\Participant\Events\ParticipantSuspended;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendParticipantSuspendedNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(ParticipantSuspended $event): void
{
try {
$guardian = $event->participant->primaryGuardian;
if (!$guardian) {
return;
}
$this->notificationService->sendSimple(
type: 'participant_suspended',
recipientId: $guardian->id,
recipientType: 'guardian',
data: [
'participant_name' => $event->participant->person?->full_name ?? '',
'reason' => $event->reason,
'suspension_date' => now()->format('Y-m-d'),
],
priority: 'high',
);
} catch (\Throwable $e) {
Log::error('SendParticipantSuspendedNotification failed: ' . $e->getMessage(), [
'participant_id' => $event->participant->id,
]);
}
}
public function failed(ParticipantSuspended $event, \Throwable $exception): void
{
Log::critical('SendParticipantSuspendedNotification PERMANENTLY FAILED', [
'participant_id' => $event->participant->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Shared\Helpers;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment;
use App\Domain\POS\Models\POSTransaction;
use Illuminate\Support\Facades\URL;
class PublicLinkHelper
{
public static function invoiceLink(Invoice $invoice): string
{
return URL::signedRoute('public.invoice', ['uuid' => $invoice->uuid], now()->addDays(30));
}
public static function receiptLink(Payment $payment): string
{
return URL::signedRoute('public.receipt', ['uuid' => $payment->uuid], now()->addDays(30));
}
public static function posReceiptLink(POSTransaction $transaction): string
{
return URL::signedRoute('public.pos-receipt', ['uuid' => $transaction->uuid], now()->addDays(30));
}
}
<?php
namespace App\Domain\Training\Listeners;
use App\Domain\Training\Events\EnrollmentCompleted;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendEnrollmentCompletedNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(EnrollmentCompleted $event): void
{
try {
$participant = $event->enrollment->participant;
$this->notificationService->sendSimple(
type: 'enrollment_completed',
recipientId: $participant->id,
recipientType: 'participant',
data: [
'participant_name' => $participant->person?->full_name ?? '',
'program_name' => $event->enrollment->trainingGroup?->trainingProgram?->name_ar ?? '',
'completion_date' => now()->format('Y-m-d'),
],
);
} catch (\Throwable $e) {
Log::error('SendEnrollmentCompletedNotification failed: ' . $e->getMessage(), [
'enrollment_id' => $event->enrollment->id,
]);
}
}
public function failed(EnrollmentCompleted $event, \Throwable $exception): void
{
Log::critical('SendEnrollmentCompletedNotification PERMANENTLY FAILED', [
'enrollment_id' => $event->enrollment->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Training\Listeners;
use App\Domain\Training\Events\EvaluationShared;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendEvaluationSharedNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(EvaluationShared $event): void
{
try {
$participant = $event->evaluation->participant;
$this->notificationService->sendSimple(
type: 'evaluation_shared',
recipientId: $participant->id,
recipientType: 'participant',
data: [
'participant_name' => $participant->person?->full_name ?? '',
'evaluation_title' => $event->evaluation->title ?? $event->evaluation->name ?? '',
'trainer_name' => $event->actor->name,
'evaluation_date' => $event->evaluation->evaluation_date?->format('Y-m-d') ?? now()->format('Y-m-d'),
],
);
} catch (\Throwable $e) {
Log::error('SendEvaluationSharedNotification failed: ' . $e->getMessage(), [
'evaluation_id' => $event->evaluation->id,
]);
}
}
public function failed(EvaluationShared $event, \Throwable $exception): void
{
Log::critical('SendEvaluationSharedNotification PERMANENTLY FAILED', [
'evaluation_id' => $event->evaluation->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Training\Listeners;
use App\Domain\Training\Events\ParticipantRegistered;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendParticipantRegisteredNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(ParticipantRegistered $event): void
{
try {
$guardian = $event->participant->primaryGuardian;
if (!$guardian) {
return;
}
$this->notificationService->sendSimple(
type: 'participant_registered',
recipientId: $guardian->id,
recipientType: 'guardian',
data: [
'participant_name' => $event->participant->person?->full_name ?? '',
'registration_date' => $event->participant->registration_date?->format('Y-m-d') ?? now()->format('Y-m-d'),
'participant_number' => $event->participant->participant_number ?? '',
],
);
} catch (\Throwable $e) {
Log::error('SendParticipantRegisteredNotification failed: ' . $e->getMessage(), [
'participant_id' => $event->participant->id,
]);
}
}
public function failed(ParticipantRegistered $event, \Throwable $exception): void
{
Log::critical('SendParticipantRegisteredNotification PERMANENTLY FAILED', [
'participant_id' => $event->participant->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Training\Listeners;
use App\Domain\Training\Events\ParticipantStatusChanged;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendParticipantStatusChangedNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(ParticipantStatusChanged $event): void
{
try {
$guardian = $event->participant->primaryGuardian;
if (!$guardian) {
return;
}
$this->notificationService->sendSimple(
type: 'participant_status_changed',
recipientId: $guardian->id,
recipientType: 'guardian',
data: [
'participant_name' => $event->participant->person?->full_name ?? '',
'old_status' => $event->oldStatus,
'new_status' => $event->newStatus,
'reason' => $event->participant->status_reason ?? '',
],
);
} catch (\Throwable $e) {
Log::error('SendParticipantStatusChangedNotification failed: ' . $e->getMessage(), [
'participant_id' => $event->participant->id,
]);
}
}
public function failed(ParticipantStatusChanged $event, \Throwable $exception): void
{
Log::critical('SendParticipantStatusChangedNotification PERMANENTLY FAILED', [
'participant_id' => $event->participant->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Domain\Training\Listeners;
use App\Domain\Training\Events\SessionCompleted;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;
class SendSessionCompletedNotification implements ShouldQueue
{
public function __construct(
private NotificationService $notificationService,
) {}
public function handle(SessionCompleted $event): void
{
try {
$this->notificationService->sendToRole(
type: 'session_completed',
role: 'head_trainer',
data: [
'group_name' => $event->session->trainingGroup?->name_ar ?? '',
'session_date' => $event->session->session_date?->format('Y-m-d') ?? $event->session->date?->format('Y-m-d') ?? '',
'attendance_count' => (string) ($event->session->attendanceRecords()->whereIn('status', ['present', 'late'])->count()),
'total_count' => (string) ($event->session->attendanceRecords()->count()),
],
);
} catch (\Throwable $e) {
Log::error('SendSessionCompletedNotification failed: ' . $e->getMessage(), [
'session_id' => $event->session->id,
]);
}
}
public function failed(SessionCompleted $event, \Throwable $exception): void
{
Log::critical('SendSessionCompletedNotification PERMANENTLY FAILED', [
'session_id' => $event->session->id,
'error' => $exception->getMessage(),
]);
}
}
<?php
namespace App\Http\Controllers;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment;
use App\Domain\POS\Models\POSTransaction;
use Illuminate\Http\Request;
class PublicDocumentController extends Controller
{
public function invoice(Request $request, string $uuid)
{
if (!$request->hasValidSignature()) {
abort(403, 'الرابط منتهي الصلاحية أو غير صالح');
}
$invoice = Invoice::where('uuid', $uuid)
->with(['items', 'billable.person', 'payments', 'creator'])
->firstOrFail();
return view('public.invoice', compact('invoice'));
}
public function receipt(Request $request, string $uuid)
{
if (!$request->hasValidSignature()) {
abort(403, 'الرابط منتهي الصلاحية أو غير صالح');
}
$payment = Payment::where('uuid', $uuid)
->with(['invoice.billable.person', 'creator'])
->firstOrFail();
return view('public.receipt', compact('payment'));
}
public function posReceipt(Request $request, string $uuid)
{
if (!$request->hasValidSignature()) {
abort(403, 'الرابط منتهي الصلاحية أو غير صالح');
}
$transaction = POSTransaction::where('uuid', $uuid)
->with(['items', 'participant.person', 'processedBy'])
->firstOrFail();
return view('public.pos-receipt', compact('transaction'));
}
}
......@@ -17,8 +17,12 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Financial\Events\InvoiceOverdue::class => [
\App\Domain\Financial\Listeners\SendOverdueReminder::class,
],
\App\Domain\Financial\Events\InvoiceSent::class => [],
\App\Domain\Financial\Events\InvoiceCancelled::class => [],
\App\Domain\Financial\Events\InvoiceSent::class => [
\App\Domain\Financial\Listeners\SendInvoiceSentNotification::class,
],
\App\Domain\Financial\Events\InvoiceCancelled::class => [
\App\Domain\Financial\Listeners\SendInvoiceCancelledNotification::class,
],
\App\Domain\Financial\Events\InstallmentOverdue::class => [
\App\Domain\Financial\Listeners\NotifyInstallmentDue::class,
],
......@@ -27,10 +31,18 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Financial\Listeners\SendPaymentNotification::class,
\App\Domain\Financial\Listeners\UpdateCashSessionTotals::class,
],
\App\Domain\Financial\Events\PaymentPlanDefaulted::class => [],
\App\Domain\Financial\Events\WalletDeposited::class => [],
\App\Domain\Financial\Events\WalletFrozen::class => [],
\App\Domain\Financial\Events\CashSessionClosed::class => [],
\App\Domain\Financial\Events\PaymentPlanDefaulted::class => [
\App\Domain\Financial\Listeners\SendPaymentPlanDefaultedNotification::class,
],
\App\Domain\Financial\Events\WalletDeposited::class => [
\App\Domain\Financial\Listeners\SendWalletDepositNotification::class,
],
\App\Domain\Financial\Events\WalletFrozen::class => [
\App\Domain\Financial\Listeners\SendWalletFrozenNotification::class,
],
\App\Domain\Financial\Events\CashSessionClosed::class => [
\App\Domain\Financial\Listeners\SendCashSessionClosedNotification::class,
],
// Training Events
\App\Domain\Training\Events\EnrollmentCreated::class => [
......@@ -42,7 +54,9 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Training\Listeners\RemoveAttendanceRecords::class,
\App\Domain\Training\Listeners\ProcessWaitlist::class,
],
\App\Domain\Training\Events\EnrollmentCompleted::class => [],
\App\Domain\Training\Events\EnrollmentCompleted::class => [
\App\Domain\Training\Listeners\SendEnrollmentCompletedNotification::class,
],
\App\Domain\Training\Events\SessionCreated::class => [
\App\Domain\Training\Listeners\CreateAutoReservation::class,
],
......@@ -50,13 +64,21 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Training\Listeners\NotifySessionCancellation::class,
\App\Domain\Training\Listeners\CancelLinkedReservation::class,
],
\App\Domain\Training\Events\SessionCompleted::class => [],
\App\Domain\Training\Events\SessionCompleted::class => [
\App\Domain\Training\Listeners\SendSessionCompletedNotification::class,
],
\App\Domain\Training\Events\WaitlistSpotAvailable::class => [
\App\Domain\Training\Listeners\NotifyWaitlistSpot::class,
],
\App\Domain\Training\Events\ParticipantRegistered::class => [],
\App\Domain\Training\Events\ParticipantStatusChanged::class => [],
\App\Domain\Training\Events\EvaluationShared::class => [],
\App\Domain\Training\Events\ParticipantRegistered::class => [
\App\Domain\Training\Listeners\SendParticipantRegisteredNotification::class,
],
\App\Domain\Training\Events\ParticipantStatusChanged::class => [
\App\Domain\Training\Listeners\SendParticipantStatusChangedNotification::class,
],
\App\Domain\Training\Events\EvaluationShared::class => [
\App\Domain\Training\Listeners\SendEvaluationSharedNotification::class,
],
// Attendance Events
\App\Domain\Attendance\Events\AttendanceMarked::class => [],
......@@ -85,11 +107,17 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Scheduling\Events\AssignmentEnded::class => [],
// Participant Events
\App\Domain\Participant\Events\ParticipantSuspended::class => [],
\App\Domain\Participant\Events\ParticipantFrozen::class => [],
\App\Domain\Participant\Events\ParticipantSuspended::class => [
\App\Domain\Participant\Listeners\SendParticipantSuspendedNotification::class,
],
\App\Domain\Participant\Events\ParticipantFrozen::class => [
\App\Domain\Participant\Listeners\SendParticipantFrozenNotification::class,
],
// POS Events
\App\Domain\POS\Events\POSTransactionCompleted::class => [],
\App\Domain\POS\Events\POSTransactionCompleted::class => [
\App\Domain\POS\Listeners\SendPOSReceiptNotification::class,
],
];
public function shouldDiscoverEvents(): bool
......
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class NotificationTemplateSeeder extends Seeder
{
public function run(): void
{
$now = Carbon::now();
$templates = $this->getTemplates();
foreach ($templates as $template) {
DB::table('notification_templates')->updateOrInsert(
[
'event_type' => $template['event_type'],
'channel' => $template['channel'],
'locale' => 'ar',
'academy_id' => null,
],
[
'subject' => $template['subject'],
'body' => $template['body'],
'variables' => json_encode($template['variables']),
'is_active' => true,
'created_at' => $now,
'updated_at' => $now,
]
);
}
}
private function getTemplates(): array
{
return [
// 1. invoice_created
[
'event_type' => 'invoice_created',
'channel' => 'in_app',
'subject' => 'فاتورة جديدة',
'body' => 'تم إنشاء فاتورة رقم {{invoice_number}} بمبلغ {{total_amount}} ج.م مستحقة في {{due_date}}',
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'due_date', 'public_link'],
],
[
'event_type' => 'invoice_created',
'channel' => 'email',
'subject' => 'فاتورة جديدة رقم {{invoice_number}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nتم إنشاء فاتورة جديدة:\n- رقم الفاتورة: {{invoice_number}}\n- المبلغ الإجمالي: {{total_amount}} ج.م\n- تاريخ الاستحقاق: {{due_date}}\n\nلعرض الفاتورة: {{public_link}}\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'due_date', 'public_link'],
],
[
'event_type' => 'invoice_created',
'channel' => 'sms',
'subject' => null,
'body' => 'فاتورة جديدة {{invoice_number}} بمبلغ {{total_amount}} ج.م - الاستحقاق: {{due_date}}',
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'due_date', 'public_link'],
],
// 2. invoice_sent
[
'event_type' => 'invoice_sent',
'channel' => 'in_app',
'subject' => 'تم إرسال الفاتورة',
'body' => 'تم إرسال الفاتورة رقم {{invoice_number}} بمبلغ {{total_amount}} ج.م',
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'due_date', 'public_link'],
],
[
'event_type' => 'invoice_sent',
'channel' => 'email',
'subject' => 'فاتورة مستحقة - {{invoice_number}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود إعلامكم بأن الفاتورة رقم {{invoice_number}} بمبلغ {{total_amount}} ج.م مستحقة بتاريخ {{due_date}}.\n\nلعرض الفاتورة والدفع: {{public_link}}\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'due_date', 'public_link'],
],
[
'event_type' => 'invoice_sent',
'channel' => 'sms',
'subject' => null,
'body' => 'فاتورة {{invoice_number}} بمبلغ {{total_amount}} ج.م مستحقة {{due_date}}',
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'due_date', 'public_link'],
],
// 3. invoice_paid
[
'event_type' => 'invoice_paid',
'channel' => 'in_app',
'subject' => 'تم سداد الفاتورة',
'body' => 'تم سداد الفاتورة رقم {{invoice_number}} بالكامل بمبلغ {{total_amount}} ج.م',
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'paid_date', 'receipt_link'],
],
[
'event_type' => 'invoice_paid',
'channel' => 'email',
'subject' => 'تأكيد سداد الفاتورة {{invoice_number}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنؤكد استلام سداد الفاتورة رقم {{invoice_number}} بمبلغ {{total_amount}} ج.م بتاريخ {{paid_date}}.\n\nلعرض الإيصال: {{receipt_link}}\n\nشكراً لكم.",
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'paid_date', 'receipt_link'],
],
[
'event_type' => 'invoice_paid',
'channel' => 'sms',
'subject' => null,
'body' => 'تم سداد الفاتورة {{invoice_number}} بمبلغ {{total_amount}} ج.م. شكراً لكم',
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'paid_date', 'receipt_link'],
],
// 4. invoice_overdue
[
'event_type' => 'invoice_overdue',
'channel' => 'in_app',
'subject' => 'فاتورة متأخرة',
'body' => 'الفاتورة رقم {{invoice_number}} متأخرة {{days_overdue}} يوم بمبلغ {{total_amount}} ج.م',
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'due_date', 'days_overdue', 'public_link'],
],
[
'event_type' => 'invoice_overdue',
'channel' => 'email',
'subject' => 'تذكير: فاتورة متأخرة {{invoice_number}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود تذكيركم بأن الفاتورة رقم {{invoice_number}} بمبلغ {{total_amount}} ج.م كانت مستحقة بتاريخ {{due_date}} وهي متأخرة {{days_overdue}} يوم.\n\nنرجو السداد في أقرب وقت: {{public_link}}\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'due_date', 'days_overdue', 'public_link'],
],
[
'event_type' => 'invoice_overdue',
'channel' => 'sms',
'subject' => null,
'body' => 'تذكير: فاتورة {{invoice_number}} متأخرة {{days_overdue}} يوم - {{total_amount}} ج.م',
'variables' => ['participant_name', 'invoice_number', 'total_amount', 'due_date', 'days_overdue', 'public_link'],
],
// 5. invoice_cancelled
[
'event_type' => 'invoice_cancelled',
'channel' => 'in_app',
'subject' => 'إلغاء فاتورة',
'body' => 'تم إلغاء الفاتورة رقم {{invoice_number}}',
'variables' => ['participant_name', 'invoice_number', 'reason'],
],
[
'event_type' => 'invoice_cancelled',
'channel' => 'email',
'subject' => 'إلغاء الفاتورة {{invoice_number}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود إعلامكم بأنه تم إلغاء الفاتورة رقم {{invoice_number}}.\nالسبب: {{reason}}\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'invoice_number', 'reason'],
],
[
'event_type' => 'invoice_cancelled',
'channel' => 'sms',
'subject' => null,
'body' => 'تم إلغاء الفاتورة {{invoice_number}}',
'variables' => ['participant_name', 'invoice_number', 'reason'],
],
// 6. payment_received
[
'event_type' => 'payment_received',
'channel' => 'in_app',
'subject' => 'تأكيد دفعة',
'body' => 'تم استلام دفعة بمبلغ {{payment_amount}} ج.م عبر {{payment_method}} للفاتورة {{invoice_number}}',
'variables' => ['participant_name', 'payment_amount', 'payment_method', 'invoice_number', 'receipt_link'],
],
[
'event_type' => 'payment_received',
'channel' => 'email',
'subject' => 'تأكيد استلام دفعة - {{invoice_number}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nتم استلام دفعة:\n- المبلغ: {{payment_amount}} ج.م\n- طريقة الدفع: {{payment_method}}\n- الفاتورة: {{invoice_number}}\n\nإيصال الدفع: {{receipt_link}}\n\nشكراً لكم.",
'variables' => ['participant_name', 'payment_amount', 'payment_method', 'invoice_number', 'receipt_link'],
],
[
'event_type' => 'payment_received',
'channel' => 'sms',
'subject' => null,
'body' => 'تم استلام {{payment_amount}} ج.م للفاتورة {{invoice_number}}. شكراً',
'variables' => ['participant_name', 'payment_amount', 'payment_method', 'invoice_number', 'receipt_link'],
],
// 7. payment_plan_defaulted
[
'event_type' => 'payment_plan_defaulted',
'channel' => 'in_app',
'subject' => 'تأخر قسط',
'body' => 'تأخر سداد قسط بمبلغ {{overdue_amount}} ج.م المستحق في {{installment_date}}',
'variables' => ['participant_name', 'plan_id', 'overdue_amount', 'installment_date'],
],
[
'event_type' => 'payment_plan_defaulted',
'channel' => 'email',
'subject' => 'تنبيه: تأخر سداد قسط',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود إعلامكم بتأخر سداد القسط المستحق بتاريخ {{installment_date}} بمبلغ {{overdue_amount}} ج.م.\n\nنرجو التواصل مع الإدارة لتسوية الموقف المالي.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'plan_id', 'overdue_amount', 'installment_date'],
],
[
'event_type' => 'payment_plan_defaulted',
'channel' => 'sms',
'subject' => null,
'body' => 'تنبيه: قسط {{overdue_amount}} ج.م متأخر من {{installment_date}}. يرجى السداد',
'variables' => ['participant_name', 'plan_id', 'overdue_amount', 'installment_date'],
],
// 8. wallet_deposited
[
'event_type' => 'wallet_deposited',
'channel' => 'in_app',
'subject' => 'إيداع في المحفظة',
'body' => 'تم إيداع {{amount}} ج.م في محفظتك. الرصيد الحالي: {{new_balance}} ج.م',
'variables' => ['participant_name', 'amount', 'new_balance'],
],
[
'event_type' => 'wallet_deposited',
'channel' => 'email',
'subject' => 'تأكيد إيداع في المحفظة',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nتم إيداع مبلغ {{amount}} ج.م في محفظتكم.\nالرصيد الحالي: {{new_balance}} ج.م\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'amount', 'new_balance'],
],
[
'event_type' => 'wallet_deposited',
'channel' => 'sms',
'subject' => null,
'body' => 'تم إيداع {{amount}} ج.م بمحفظتك. الرصيد: {{new_balance}} ج.م',
'variables' => ['participant_name', 'amount', 'new_balance'],
],
// 9. wallet_frozen
[
'event_type' => 'wallet_frozen',
'channel' => 'in_app',
'subject' => 'تجميد المحفظة',
'body' => 'تم تجميد محفظتك. السبب: {{reason}}',
'variables' => ['participant_name', 'reason'],
],
[
'event_type' => 'wallet_frozen',
'channel' => 'email',
'subject' => 'تجميد المحفظة',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود إعلامكم بأنه تم تجميد محفظتكم.\nالسبب: {{reason}}\n\nللاستفسار يرجى التواصل مع الإدارة.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'reason'],
],
[
'event_type' => 'wallet_frozen',
'channel' => 'sms',
'subject' => null,
'body' => 'تم تجميد محفظتك. السبب: {{reason}}. تواصل مع الإدارة',
'variables' => ['participant_name', 'reason'],
],
// 10. cash_session_closed
[
'event_type' => 'cash_session_closed',
'channel' => 'in_app',
'subject' => 'إغلاق وردية',
'body' => 'تم إغلاق وردية {{cashier_name}} بتاريخ {{session_date}} - إجمالي: {{total_sales}} ج.م ({{transactions_count}} عملية)',
'variables' => ['cashier_name', 'session_date', 'total_sales', 'transactions_count'],
],
[
'event_type' => 'cash_session_closed',
'channel' => 'email',
'subject' => 'تقرير إغلاق وردية - {{session_date}}',
'body' => "تم إغلاق الوردية:\n- أمين الصندوق: {{cashier_name}}\n- التاريخ: {{session_date}}\n- إجمالي المبيعات: {{total_sales}} ج.م\n- عدد العمليات: {{transactions_count}}\n\nمع تحيات الإدارة",
'variables' => ['cashier_name', 'session_date', 'total_sales', 'transactions_count'],
],
[
'event_type' => 'cash_session_closed',
'channel' => 'sms',
'subject' => null,
'body' => 'إغلاق وردية {{session_date}}: {{total_sales}} ج.م ({{transactions_count}} عملية)',
'variables' => ['cashier_name', 'session_date', 'total_sales', 'transactions_count'],
],
// 11. enrollment_created
[
'event_type' => 'enrollment_created',
'channel' => 'in_app',
'subject' => 'تسجيل جديد',
'body' => 'تم تسجيل {{participant_name}} في برنامج {{program_name}} - مجموعة {{group_name}}',
'variables' => ['participant_name', 'program_name', 'group_name', 'start_date'],
],
[
'event_type' => 'enrollment_created',
'channel' => 'email',
'subject' => 'تأكيد التسجيل في {{program_name}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nيسعدنا تأكيد تسجيلكم في:\n- البرنامج: {{program_name}}\n- المجموعة: {{group_name}}\n- تاريخ البدء: {{start_date}}\n\nنتمنى لكم تجربة ممتعة.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'program_name', 'group_name', 'start_date'],
],
[
'event_type' => 'enrollment_created',
'channel' => 'sms',
'subject' => null,
'body' => 'تم تسجيلك في {{program_name}} - مجموعة {{group_name}}. البدء: {{start_date}}',
'variables' => ['participant_name', 'program_name', 'group_name', 'start_date'],
],
// 12. enrollment_cancelled
[
'event_type' => 'enrollment_cancelled',
'channel' => 'in_app',
'subject' => 'إلغاء تسجيل',
'body' => 'تم إلغاء تسجيل {{participant_name}} من {{program_name}} - {{group_name}}',
'variables' => ['participant_name', 'program_name', 'group_name', 'reason'],
],
[
'event_type' => 'enrollment_cancelled',
'channel' => 'email',
'subject' => 'إلغاء التسجيل من {{program_name}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود إعلامكم بإلغاء تسجيلكم من:\n- البرنامج: {{program_name}}\n- المجموعة: {{group_name}}\n- السبب: {{reason}}\n\nللاستفسار يرجى التواصل مع الإدارة.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'program_name', 'group_name', 'reason'],
],
[
'event_type' => 'enrollment_cancelled',
'channel' => 'sms',
'subject' => null,
'body' => 'تم إلغاء تسجيلك من {{program_name}}. السبب: {{reason}}',
'variables' => ['participant_name', 'program_name', 'group_name', 'reason'],
],
// 13. enrollment_completed
[
'event_type' => 'enrollment_completed',
'channel' => 'in_app',
'subject' => 'إتمام البرنامج',
'body' => 'مبروك! أتم {{participant_name}} برنامج {{program_name}} بنجاح',
'variables' => ['participant_name', 'program_name', 'completion_date'],
],
[
'event_type' => 'enrollment_completed',
'channel' => 'email',
'subject' => 'تهانينا - إتمام برنامج {{program_name}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nتهانينا! لقد أتممت برنامج {{program_name}} بنجاح بتاريخ {{completion_date}}.\n\nنتمنى لك التوفيق والنجاح الدائم.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'program_name', 'completion_date'],
],
[
'event_type' => 'enrollment_completed',
'channel' => 'sms',
'subject' => null,
'body' => 'مبروك! أتممت برنامج {{program_name}} بنجاح',
'variables' => ['participant_name', 'program_name', 'completion_date'],
],
// 14. session_cancelled
[
'event_type' => 'session_cancelled',
'channel' => 'in_app',
'subject' => 'إلغاء حصة',
'body' => 'تم إلغاء حصة {{group_name}} يوم {{session_date}} الساعة {{session_time}}',
'variables' => ['participant_name', 'group_name', 'session_date', 'session_time', 'reason'],
],
[
'event_type' => 'session_cancelled',
'channel' => 'email',
'subject' => 'إلغاء حصة {{group_name}} - {{session_date}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود إعلامكم بإلغاء الحصة التالية:\n- المجموعة: {{group_name}}\n- التاريخ: {{session_date}}\n- الوقت: {{session_time}}\n- السبب: {{reason}}\n\nسيتم الإعلان عن موعد بديل قريباً.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'group_name', 'session_date', 'session_time', 'reason'],
],
[
'event_type' => 'session_cancelled',
'channel' => 'sms',
'subject' => null,
'body' => 'إلغاء حصة {{group_name}} يوم {{session_date}} {{session_time}}. السبب: {{reason}}',
'variables' => ['participant_name', 'group_name', 'session_date', 'session_time', 'reason'],
],
// 15. session_completed
[
'event_type' => 'session_completed',
'channel' => 'in_app',
'subject' => 'انتهاء حصة',
'body' => 'انتهت حصة {{group_name}} - الحضور: {{attendance_count}}/{{total_count}}',
'variables' => ['group_name', 'session_date', 'attendance_count', 'total_count'],
],
[
'event_type' => 'session_completed',
'channel' => 'email',
'subject' => 'تقرير حصة {{group_name}} - {{session_date}}',
'body' => "تقرير الحصة:\n- المجموعة: {{group_name}}\n- التاريخ: {{session_date}}\n- الحضور: {{attendance_count}} من {{total_count}}\n\nمع تحيات الإدارة",
'variables' => ['group_name', 'session_date', 'attendance_count', 'total_count'],
],
[
'event_type' => 'session_completed',
'channel' => 'sms',
'subject' => null,
'body' => 'حصة {{group_name}} {{session_date}}: حضور {{attendance_count}}/{{total_count}}',
'variables' => ['group_name', 'session_date', 'attendance_count', 'total_count'],
],
// 16. waitlist_spot_available
[
'event_type' => 'waitlist_spot_available',
'channel' => 'in_app',
'subject' => 'مكان متاح',
'body' => 'يوجد مكان متاح في {{group_name}} - {{program_name}}',
'variables' => ['participant_name', 'program_name', 'group_name', 'spots_available'],
],
[
'event_type' => 'waitlist_spot_available',
'channel' => 'email',
'subject' => 'مكان متاح في {{program_name}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nيسعدنا إعلامكم بتوفر مكان في:\n- البرنامج: {{program_name}}\n- المجموعة: {{group_name}}\n- الأماكن المتاحة: {{spots_available}}\n\nسارعوا بالتسجيل قبل نفاد الأماكن.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'program_name', 'group_name', 'spots_available'],
],
[
'event_type' => 'waitlist_spot_available',
'channel' => 'sms',
'subject' => null,
'body' => 'مكان متاح في {{group_name}} - {{program_name}}! سارع بالتسجيل',
'variables' => ['participant_name', 'program_name', 'group_name', 'spots_available'],
],
// 17. participant_registered
[
'event_type' => 'participant_registered',
'channel' => 'in_app',
'subject' => 'تسجيل لاعب جديد',
'body' => 'تم تسجيل {{participant_name}} برقم {{participant_number}}',
'variables' => ['participant_name', 'registration_date', 'participant_number'],
],
[
'event_type' => 'participant_registered',
'channel' => 'email',
'subject' => 'مرحباً {{participant_name}} - تأكيد التسجيل',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nمرحباً بكم! تم تسجيلكم بنجاح:\n- رقم العضوية: {{participant_number}}\n- تاريخ التسجيل: {{registration_date}}\n\nنتمنى لكم تجربة رائعة معنا.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'registration_date', 'participant_number'],
],
[
'event_type' => 'participant_registered',
'channel' => 'sms',
'subject' => null,
'body' => 'مرحباً {{participant_name}}! تم تسجيلك برقم {{participant_number}}',
'variables' => ['participant_name', 'registration_date', 'participant_number'],
],
// 18. participant_status_changed
[
'event_type' => 'participant_status_changed',
'channel' => 'in_app',
'subject' => 'تغيير حالة',
'body' => 'تم تغيير حالة {{participant_name}} من {{old_status}} إلى {{new_status}}',
'variables' => ['participant_name', 'old_status', 'new_status', 'reason'],
],
[
'event_type' => 'participant_status_changed',
'channel' => 'email',
'subject' => 'تغيير حالة العضوية - {{participant_name}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود إعلامكم بتغيير حالة عضويتكم:\n- الحالة السابقة: {{old_status}}\n- الحالة الجديدة: {{new_status}}\n- السبب: {{reason}}\n\nللاستفسار يرجى التواصل مع الإدارة.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'old_status', 'new_status', 'reason'],
],
[
'event_type' => 'participant_status_changed',
'channel' => 'sms',
'subject' => null,
'body' => 'تم تغيير حالتك من {{old_status}} إلى {{new_status}}',
'variables' => ['participant_name', 'old_status', 'new_status', 'reason'],
],
// 19. participant_suspended
[
'event_type' => 'participant_suspended',
'channel' => 'in_app',
'subject' => 'إيقاف عضوية',
'body' => 'تم إيقاف عضوية {{participant_name}}. السبب: {{reason}}',
'variables' => ['participant_name', 'reason', 'suspension_date'],
],
[
'event_type' => 'participant_suspended',
'channel' => 'email',
'subject' => 'إيقاف العضوية - {{participant_name}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود إعلامكم بإيقاف عضويتكم اعتباراً من {{suspension_date}}.\nالسبب: {{reason}}\n\nللاستفسار أو التظلم يرجى التواصل مع الإدارة.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'reason', 'suspension_date'],
],
[
'event_type' => 'participant_suspended',
'channel' => 'sms',
'subject' => null,
'body' => 'تم إيقاف عضويتك من {{suspension_date}}. السبب: {{reason}}',
'variables' => ['participant_name', 'reason', 'suspension_date'],
],
// 20. participant_frozen
[
'event_type' => 'participant_frozen',
'channel' => 'in_app',
'subject' => 'تجميد عضوية',
'body' => 'تم تجميد عضوية {{participant_name}}. السبب: {{reason}}',
'variables' => ['participant_name', 'reason', 'freeze_date'],
],
[
'event_type' => 'participant_frozen',
'channel' => 'email',
'subject' => 'تجميد العضوية - {{participant_name}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنود إعلامكم بتجميد عضويتكم اعتباراً من {{freeze_date}}.\nالسبب: {{reason}}\n\nسيتم إيقاف الفواتير خلال فترة التجميد.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'reason', 'freeze_date'],
],
[
'event_type' => 'participant_frozen',
'channel' => 'sms',
'subject' => null,
'body' => 'تم تجميد عضويتك من {{freeze_date}}. السبب: {{reason}}',
'variables' => ['participant_name', 'reason', 'freeze_date'],
],
// 21. participant_absent
[
'event_type' => 'participant_absent',
'channel' => 'in_app',
'subject' => 'غياب',
'body' => 'غاب {{participant_name}} عن حصة {{group_name}} يوم {{session_date}} ({{consecutive_absences}} غياب متتالي)',
'variables' => ['participant_name', 'session_date', 'group_name', 'consecutive_absences'],
],
[
'event_type' => 'participant_absent',
'channel' => 'email',
'subject' => 'إشعار غياب - {{participant_name}}',
'body' => "عزيزي ولي الأمر,\n\nنود إعلامكم بغياب {{participant_name}} عن:\n- المجموعة: {{group_name}}\n- التاريخ: {{session_date}}\n- عدد الغيابات المتتالية: {{consecutive_absences}}\n\nنرجو المتابعة والتواصل مع الإدارة.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'session_date', 'group_name', 'consecutive_absences'],
],
[
'event_type' => 'participant_absent',
'channel' => 'sms',
'subject' => null,
'body' => 'غياب {{participant_name}} عن {{group_name}} يوم {{session_date}} ({{consecutive_absences}} متتالي)',
'variables' => ['participant_name', 'session_date', 'group_name', 'consecutive_absences'],
],
// 22. attendance_threshold_breached
[
'event_type' => 'attendance_threshold_breached',
'channel' => 'in_app',
'subject' => 'تحذير حضور',
'body' => 'نسبة حضور {{participant_name}} في {{group_name}} وصلت {{attendance_rate}}% (الحد الأدنى: {{threshold}}%)',
'variables' => ['participant_name', 'attendance_rate', 'threshold', 'group_name'],
],
[
'event_type' => 'attendance_threshold_breached',
'channel' => 'email',
'subject' => 'تحذير: انخفاض نسبة الحضور',
'body' => "عزيزي ولي الأمر,\n\nنود إعلامكم بأن نسبة حضور {{participant_name}} في مجموعة {{group_name}} وصلت إلى {{attendance_rate}}%.\nالحد الأدنى المطلوب: {{threshold}}%\n\nنرجو المتابعة لتجنب الإيقاف التلقائي.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'attendance_rate', 'threshold', 'group_name'],
],
[
'event_type' => 'attendance_threshold_breached',
'channel' => 'sms',
'subject' => null,
'body' => 'تحذير: حضور {{participant_name}} {{attendance_rate}}% أقل من الحد {{threshold}}%',
'variables' => ['participant_name', 'attendance_rate', 'threshold', 'group_name'],
],
// 23. evaluation_shared
[
'event_type' => 'evaluation_shared',
'channel' => 'in_app',
'subject' => 'تقييم جديد',
'body' => "تم مشاركة تقييم '{{evaluation_title}}' من المدرب {{trainer_name}}",
'variables' => ['participant_name', 'evaluation_title', 'trainer_name', 'evaluation_date'],
],
[
'event_type' => 'evaluation_shared',
'channel' => 'email',
'subject' => 'تقييم جديد - {{evaluation_title}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nتم مشاركة تقييم جديد:\n- العنوان: {{evaluation_title}}\n- المدرب: {{trainer_name}}\n- التاريخ: {{evaluation_date}}\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'evaluation_title', 'trainer_name', 'evaluation_date'],
],
[
'event_type' => 'evaluation_shared',
'channel' => 'sms',
'subject' => null,
'body' => 'تقييم جديد من {{trainer_name}}: {{evaluation_title}}',
'variables' => ['participant_name', 'evaluation_title', 'trainer_name', 'evaluation_date'],
],
// 24. product_low_stock
[
'event_type' => 'product_low_stock',
'channel' => 'in_app',
'subject' => 'تنبيه مخزون',
'body' => 'المنتج {{product_name}} وصل {{current_stock}} (الحد الأدنى: {{min_level}}) في {{warehouse_name}}',
'variables' => ['product_name', 'current_stock', 'min_level', 'warehouse_name'],
],
[
'event_type' => 'product_low_stock',
'channel' => 'email',
'subject' => 'تنبيه: انخفاض مخزون {{product_name}}',
'body' => "تنبيه مخزون:\n- المنتج: {{product_name}}\n- المخزن: {{warehouse_name}}\n- الكمية الحالية: {{current_stock}}\n- الحد الأدنى: {{min_level}}\n\nيرجى إعادة الطلب.\n\nمع تحيات الإدارة",
'variables' => ['product_name', 'current_stock', 'min_level', 'warehouse_name'],
],
[
'event_type' => 'product_low_stock',
'channel' => 'sms',
'subject' => null,
'body' => 'تنبيه: مخزون {{product_name}} وصل {{current_stock}} (الحد: {{min_level}})',
'variables' => ['product_name', 'current_stock', 'min_level', 'warehouse_name'],
],
// 25. pos_transaction_completed
[
'event_type' => 'pos_transaction_completed',
'channel' => 'in_app',
'subject' => 'عملية بيع',
'body' => 'تمت عملية بيع رقم {{transaction_number}} بمبلغ {{total_amount}} ج.م ({{items_count}} صنف)',
'variables' => ['participant_name', 'transaction_number', 'total_amount', 'items_count', 'receipt_link'],
],
[
'event_type' => 'pos_transaction_completed',
'channel' => 'email',
'subject' => 'إيصال عملية الشراء {{transaction_number}}',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nتفاصيل عملية الشراء:\n- رقم العملية: {{transaction_number}}\n- عدد الأصناف: {{items_count}}\n- الإجمالي: {{total_amount}} ج.م\n\nلعرض الإيصال: {{receipt_link}}\n\nشكراً لكم.",
'variables' => ['participant_name', 'transaction_number', 'total_amount', 'items_count', 'receipt_link'],
],
[
'event_type' => 'pos_transaction_completed',
'channel' => 'sms',
'subject' => null,
'body' => 'إيصال {{transaction_number}}: {{total_amount}} ج.م ({{items_count}} صنف)',
'variables' => ['participant_name', 'transaction_number', 'total_amount', 'items_count', 'receipt_link'],
],
// 26. installment_overdue
[
'event_type' => 'installment_overdue',
'channel' => 'in_app',
'subject' => 'قسط متأخر',
'body' => 'قسط بمبلغ {{installment_amount}} ج.م متأخر من {{due_date}} (متبقي: {{plan_remaining}} ج.م)',
'variables' => ['participant_name', 'installment_amount', 'due_date', 'plan_remaining'],
],
[
'event_type' => 'installment_overdue',
'channel' => 'email',
'subject' => 'تذكير: قسط متأخر',
'body' => "عزيزي/عزيزتي {{participant_name}},\n\nنذكركم بالقسط المتأخر:\n- المبلغ: {{installment_amount}} ج.م\n- تاريخ الاستحقاق: {{due_date}}\n- المتبقي من الخطة: {{plan_remaining}} ج.م\n\nنرجو السداد لتجنب الرسوم الإضافية.\n\nمع تحيات الإدارة",
'variables' => ['participant_name', 'installment_amount', 'due_date', 'plan_remaining'],
],
[
'event_type' => 'installment_overdue',
'channel' => 'sms',
'subject' => null,
'body' => 'تذكير: قسط {{installment_amount}} ج.م متأخر من {{due_date}}. يرجى السداد',
'variables' => ['participant_name', 'installment_amount', 'due_date', 'plan_remaining'],
],
];
}
}
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>فاتورة {{ $invoice->number }}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Cairo', sans-serif; }
@media print {
.no-print { display: none; }
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
</style>
</head>
<body class="bg-gray-50 min-h-screen py-8">
<div class="max-w-3xl mx-auto bg-white shadow-lg rounded-lg overflow-hidden">
{{-- Header --}}
<div class="bg-blue-700 text-white px-8 py-6">
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold">فاتورة</h1>
<p class="text-blue-100 mt-1">{{ $invoice->number }}</p>
</div>
<div class="text-end">
<p class="text-lg font-semibold">{{ $invoice->academy?->name_ar ?? $invoice->academy?->name ?? '' }}</p>
</div>
</div>
</div>
{{-- Invoice Details --}}
<div class="px-8 py-6 border-b border-gray-200">
<div class="grid grid-cols-2 gap-6">
<div>
<h3 class="text-sm text-gray-500 mb-1">العميل</h3>
<p class="font-semibold text-gray-800">
{{ $invoice->contact_name ?? $invoice->billable?->person?->name_ar ?? $invoice->billable?->person?->name ?? '-' }}
</p>
@if($invoice->contact_phone)
<p class="text-sm text-gray-600" dir="ltr">{{ $invoice->contact_phone }}</p>
@endif
</div>
<div class="text-start">
<div class="space-y-2">
<div>
<span class="text-sm text-gray-500">تاريخ الإصدار:</span>
<span class="font-medium text-gray-800 ms-2">{{ $invoice->issue_date?->format('Y-m-d') ?? '-' }}</span>
</div>
<div>
<span class="text-sm text-gray-500">تاريخ الاستحقاق:</span>
<span class="font-medium text-gray-800 ms-2">{{ $invoice->due_date?->format('Y-m-d') ?? '-' }}</span>
</div>
<div>
<span class="text-sm text-gray-500">الحالة:</span>
@php
$statusColors = [
'draft' => 'bg-gray-100 text-gray-700',
'pending' => 'bg-yellow-100 text-yellow-700',
'sent' => 'bg-blue-100 text-blue-700',
'paid' => 'bg-green-100 text-green-700',
'partially_paid' => 'bg-orange-100 text-orange-700',
'overdue' => 'bg-red-100 text-red-700',
'cancelled' => 'bg-gray-100 text-gray-500',
];
$statusLabels = [
'draft' => 'مسودة',
'pending' => 'معلقة',
'awaiting_approval' => 'بانتظار الموافقة',
'sent' => 'مرسلة',
'paid' => 'مدفوعة',
'partially_paid' => 'مدفوعة جزئياً',
'overpaid' => 'مدفوعة بزيادة',
'overdue' => 'متأخرة',
'cancelled' => 'ملغاة',
'refunded' => 'مستردة',
'partially_refunded' => 'مستردة جزئياً',
'written_off' => 'مشطوبة',
'disputed' => 'متنازع عليها',
];
$statusValue = $invoice->status?->value ?? $invoice->status ?? '';
$colorClass = $statusColors[$statusValue] ?? 'bg-gray-100 text-gray-700';
$label = $statusLabels[$statusValue] ?? $statusValue;
@endphp
<span class="inline-block px-3 py-1 rounded-full text-xs font-semibold {{ $colorClass }} ms-2">
{{ $label }}
</span>
</div>
</div>
</div>
</div>
</div>
{{-- Items Table --}}
<div class="px-8 py-6">
<table class="w-full">
<thead>
<tr class="border-b-2 border-gray-200">
<th class="text-start py-3 text-sm font-semibold text-gray-600">البند</th>
<th class="text-center py-3 text-sm font-semibold text-gray-600">الكمية</th>
<th class="text-start py-3 text-sm font-semibold text-gray-600">سعر الوحدة</th>
<th class="text-start py-3 text-sm font-semibold text-gray-600">الإجمالي</th>
</tr>
</thead>
<tbody>
@foreach($invoice->items as $item)
<tr class="border-b border-gray-100">
<td class="py-3 text-gray-800">{{ $item->description }}</td>
<td class="py-3 text-center text-gray-600">{{ $item->quantity }}</td>
<td class="py-3 text-gray-600" dir="ltr">{{ number_format($item->unit_price / 100, 2) }} ج.م</td>
<td class="py-3 font-medium text-gray-800" dir="ltr">{{ number_format($item->total_amount / 100, 2) }} ج.م</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Totals --}}
<div class="px-8 py-6 bg-gray-50 border-t border-gray-200">
<div class="flex justify-start">
<div class="w-72 space-y-2">
<div class="flex justify-between text-gray-600">
<span>المجموع الفرعي</span>
<span dir="ltr">{{ number_format($invoice->subtotal_amount / 100, 2) }} ج.م</span>
</div>
@if($invoice->discount_amount > 0)
<div class="flex justify-between text-green-600">
<span>الخصم</span>
<span dir="ltr">- {{ number_format($invoice->discount_amount / 100, 2) }} ج.م</span>
</div>
@endif
@if($invoice->tax_amount > 0)
<div class="flex justify-between text-gray-600">
<span>الضريبة</span>
<span dir="ltr">{{ number_format($invoice->tax_amount / 100, 2) }} ج.م</span>
</div>
@endif
<div class="flex justify-between font-bold text-lg text-gray-800 border-t border-gray-300 pt-2">
<span>الإجمالي</span>
<span dir="ltr">{{ number_format($invoice->total_amount / 100, 2) }} ج.م</span>
</div>
@if($invoice->paid_amount > 0)
<div class="flex justify-between text-blue-600">
<span>المدفوع</span>
<span dir="ltr">{{ number_format($invoice->paid_amount / 100, 2) }} ج.م</span>
</div>
@endif
@if($invoice->due_amount > 0)
<div class="flex justify-between text-red-600 font-semibold">
<span>المتبقي</span>
<span dir="ltr">{{ number_format($invoice->due_amount / 100, 2) }} ج.م</span>
</div>
@endif
</div>
</div>
</div>
{{-- Notes --}}
@if($invoice->notes)
<div class="px-8 py-4 border-t border-gray-200">
<p class="text-sm text-gray-500">ملاحظات:</p>
<p class="text-gray-700 mt-1">{{ $invoice->notes }}</p>
</div>
@endif
{{-- Footer --}}
<div class="px-8 py-4 bg-gray-100 text-center">
<p class="text-xs text-gray-500">تم إنشاء هذا المستند إلكترونياً</p>
@if($invoice->creator)
<p class="text-xs text-gray-400 mt-1">أنشأ بواسطة: {{ $invoice->creator->name }}</p>
@endif
</div>
</div>
{{-- Print Button --}}
<div class="max-w-3xl mx-auto mt-4 text-center no-print">
<button onclick="window.print()" class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition">
طباعة
</button>
</div>
</body>
</html>
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>إيصال مبيعات {{ $transaction->receipt_number }}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Cairo', sans-serif; }
@media print {
.no-print { display: none; }
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
</style>
</head>
<body class="bg-gray-50 min-h-screen py-8">
<div class="max-w-md mx-auto bg-white shadow-lg rounded-lg overflow-hidden">
{{-- Header --}}
<div class="bg-indigo-700 text-white px-6 py-5 text-center">
<h1 class="text-xl font-bold">إيصال مبيعات</h1>
<p class="text-indigo-100 mt-1 text-sm">{{ $transaction->receipt_number }}</p>
</div>
{{-- Transaction Info --}}
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex justify-between items-center text-sm">
<span class="text-gray-500">التاريخ</span>
<span class="font-medium text-gray-800">{{ $transaction->processed_at?->format('Y-m-d H:i') ?? $transaction->created_at?->format('Y-m-d H:i') }}</span>
</div>
@if($transaction->participant?->person)
<div class="flex justify-between items-center text-sm mt-2">
<span class="text-gray-500">العميل</span>
<span class="font-medium text-gray-800">{{ $transaction->participant->person->name_ar ?? $transaction->participant->person->name ?? '-' }}</span>
</div>
@endif
</div>
{{-- Items --}}
<div class="px-6 py-4">
<table class="w-full">
<thead>
<tr class="border-b border-gray-200">
<th class="text-start py-2 text-xs font-semibold text-gray-500">الصنف</th>
<th class="text-center py-2 text-xs font-semibold text-gray-500">الكمية</th>
<th class="text-start py-2 text-xs font-semibold text-gray-500">السعر</th>
<th class="text-start py-2 text-xs font-semibold text-gray-500">الإجمالي</th>
</tr>
</thead>
<tbody>
@foreach($transaction->items as $item)
<tr class="border-b border-gray-50">
<td class="py-2 text-sm text-gray-800">{{ $item->item_name_ar ?? '-' }}</td>
<td class="py-2 text-sm text-center text-gray-600">{{ $item->quantity }}</td>
<td class="py-2 text-sm text-gray-600" dir="ltr">{{ number_format($item->unit_price / 100, 2) }}</td>
<td class="py-2 text-sm font-medium text-gray-800" dir="ltr">{{ number_format($item->line_total / 100, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Totals --}}
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200 space-y-2">
<div class="flex justify-between text-sm text-gray-600">
<span>المجموع الفرعي</span>
<span dir="ltr">{{ number_format($transaction->subtotal / 100, 2) }} ج.م</span>
</div>
@if($transaction->discount_amount > 0)
<div class="flex justify-between text-sm text-green-600">
<span>الخصم</span>
<span dir="ltr">- {{ number_format($transaction->discount_amount / 100, 2) }} ج.م</span>
</div>
@endif
@if($transaction->tax_amount > 0)
<div class="flex justify-between text-sm text-gray-600">
<span>الضريبة</span>
<span dir="ltr">{{ number_format($transaction->tax_amount / 100, 2) }} ج.م</span>
</div>
@endif
<div class="flex justify-between font-bold text-lg text-gray-800 border-t border-gray-300 pt-2">
<span>الإجمالي</span>
<span dir="ltr">{{ number_format($transaction->total_amount / 100, 2) }} ج.م</span>
</div>
{{-- Payment Method --}}
@php
$posMethodLabels = [
'cash' => 'نقدي',
'card' => 'بطاقة',
'wallet' => 'محفظة',
'split' => 'مقسم',
];
$posMethodValue = $transaction->payment_method?->value ?? $transaction->payment_method ?? '';
@endphp
<div class="flex justify-between text-sm text-gray-600 pt-1">
<span>طريقة الدفع</span>
<span class="font-medium">{{ $posMethodLabels[$posMethodValue] ?? $posMethodValue }}</span>
</div>
</div>
{{-- Cashier --}}
@if($transaction->processedBy)
<div class="px-6 py-3 border-t border-gray-200">
<div class="flex justify-between items-center text-sm">
<span class="text-gray-500">الكاشير</span>
<span class="text-gray-700">{{ $transaction->processedBy->name }}</span>
</div>
</div>
@endif
{{-- Footer --}}
<div class="px-6 py-4 bg-gray-100 text-center">
<p class="text-xs text-gray-500">تم إنشاء هذا المستند إلكترونياً</p>
</div>
</div>
{{-- Print Button --}}
<div class="max-w-md mx-auto mt-4 text-center no-print">
<button onclick="window.print()" class="bg-indigo-600 text-white px-6 py-2 rounded-lg hover:bg-indigo-700 transition">
طباعة
</button>
</div>
</body>
</html>
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>إيصال دفع {{ $payment->reference ?? $payment->uuid }}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Cairo', sans-serif; }
@media print {
.no-print { display: none; }
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
</style>
</head>
<body class="bg-gray-50 min-h-screen py-8">
<div class="max-w-md mx-auto bg-white shadow-lg rounded-lg overflow-hidden">
{{-- Header --}}
<div class="bg-green-700 text-white px-6 py-5 text-center">
<h1 class="text-xl font-bold">إيصال دفع</h1>
<p class="text-green-100 mt-1 text-sm">{{ $payment->reference ?? $payment->uuid }}</p>
</div>
{{-- Payment Details --}}
<div class="px-6 py-6 space-y-4">
{{-- Amount --}}
<div class="text-center py-4 border-b border-gray-200">
<p class="text-sm text-gray-500 mb-1">المبلغ المدفوع</p>
<p class="text-3xl font-bold text-gray-800" dir="ltr">{{ number_format($payment->amount / 100, 2) }} ج.م</p>
</div>
{{-- Details Grid --}}
<div class="space-y-3 py-2">
<div class="flex justify-between items-center">
<span class="text-gray-500 text-sm">طريقة الدفع</span>
@php
$methodLabels = [
'cash' => 'نقدي',
'card' => 'بطاقة',
'bank_transfer' => 'تحويل بنكي',
'wallet' => 'محفظة',
'online' => 'إلكتروني',
'cheque' => 'شيك',
'other' => 'أخرى',
];
$methodValue = $payment->method?->value ?? $payment->method ?? '';
@endphp
<span class="font-medium text-gray-800">{{ $methodLabels[$methodValue] ?? $methodValue }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-500 text-sm">تاريخ الدفع</span>
<span class="font-medium text-gray-800">{{ $payment->payment_date?->format('Y-m-d') ?? $payment->created_at?->format('Y-m-d') }}</span>
</div>
@if($payment->invoice)
<div class="flex justify-between items-center">
<span class="text-gray-500 text-sm">رقم الفاتورة</span>
<span class="font-medium text-gray-800">{{ $payment->invoice->number }}</span>
</div>
@endif
<div class="flex justify-between items-center">
<span class="text-gray-500 text-sm">الدافع</span>
<span class="font-medium text-gray-800">
{{ $payment->invoice?->contact_name ?? $payment->invoice?->billable?->person?->name_ar ?? $payment->invoice?->billable?->person?->name ?? '-' }}
</span>
</div>
@if($payment->creator)
<div class="flex justify-between items-center">
<span class="text-gray-500 text-sm">استلم بواسطة</span>
<span class="font-medium text-gray-800">{{ $payment->creator->name }}</span>
</div>
@endif
</div>
{{-- Status --}}
@php
$statusLabels = [
'pending' => 'معلق',
'confirmed' => 'مؤكد',
'failed' => 'فاشل',
'cancelled' => 'ملغى',
'refunded' => 'مسترد',
];
$statusColors = [
'pending' => 'bg-yellow-100 text-yellow-700',
'confirmed' => 'bg-green-100 text-green-700',
'failed' => 'bg-red-100 text-red-700',
'cancelled' => 'bg-gray-100 text-gray-500',
'refunded' => 'bg-purple-100 text-purple-700',
];
$statusValue = $payment->status?->value ?? $payment->status ?? '';
@endphp
<div class="text-center pt-4 border-t border-gray-200">
<span class="inline-block px-4 py-1.5 rounded-full text-sm font-semibold {{ $statusColors[$statusValue] ?? 'bg-gray-100 text-gray-700' }}">
{{ $statusLabels[$statusValue] ?? $statusValue }}
</span>
</div>
</div>
{{-- Notes --}}
@if($payment->notes)
<div class="px-6 py-3 border-t border-gray-200">
<p class="text-sm text-gray-500">ملاحظات:</p>
<p class="text-gray-700 text-sm mt-1">{{ $payment->notes }}</p>
</div>
@endif
{{-- Footer --}}
<div class="px-6 py-4 bg-gray-100 text-center">
<p class="text-xs text-gray-500">تم إنشاء هذا المستند إلكترونياً</p>
</div>
</div>
{{-- Print Button --}}
<div class="max-w-md mx-auto mt-4 text-center no-print">
<button onclick="window.print()" class="bg-green-600 text-white px-6 py-2 rounded-lg hover:bg-green-700 transition">
طباعة
</button>
</div>
</body>
</html>
......@@ -66,6 +66,7 @@
use App\Livewire\Inventory\WarehouseList as InventoryWarehouseList;
use App\Livewire\Wallets\WalletList;
use App\Livewire\Wallets\WalletShow;
use App\Http\Controllers\PublicDocumentController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
......@@ -76,6 +77,13 @@
*/
Route::get('/health', \App\Http\Controllers\HealthController::class)->name('health');
// Public document routes (signed URLs, no auth required)
Route::prefix('public')->name('public.')->group(function () {
Route::get('/invoice/{uuid}', [PublicDocumentController::class, 'invoice'])->name('invoice');
Route::get('/receipt/{uuid}', [PublicDocumentController::class, 'receipt'])->name('receipt');
Route::get('/pos-receipt/{uuid}', [PublicDocumentController::class, 'posReceipt'])->name('pos-receipt');
});
/*
|--------------------------------------------------------------------------
| Guest Routes
......
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