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 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Financial\Events\InvoiceCreated; use App\Domain\Financial\Events\InvoiceCreated;
use App\Domain\Notification\Services\NotificationService; use App\Domain\Notification\Services\NotificationService;
use App\Domain\Shared\Helpers\PublicLinkHelper;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
...@@ -21,9 +22,11 @@ public function handle(InvoiceCreated $event): void ...@@ -21,9 +22,11 @@ public function handle(InvoiceCreated $event): void
recipientId: $event->invoice->billable_id, recipientId: $event->invoice->billable_id,
recipientType: $event->invoice->billable_type, recipientType: $event->invoice->billable_type,
data: [ data: [
'participant_name' => $event->invoice->billable?->person?->full_name ?? '',
'invoice_number' => $event->invoice->number, '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'), 'due_date' => $event->invoice->due_date?->format('Y-m-d'),
'public_link' => PublicLinkHelper::invoiceLink($event->invoice),
], ],
); );
} catch (\Throwable $e) { } 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 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Financial\Events\InvoiceOverdue; use App\Domain\Financial\Events\InvoiceOverdue;
use App\Domain\Notification\Services\NotificationService; use App\Domain\Notification\Services\NotificationService;
use App\Domain\Shared\Helpers\PublicLinkHelper;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
...@@ -21,11 +22,13 @@ public function handle(InvoiceOverdue $event): void ...@@ -21,11 +22,13 @@ public function handle(InvoiceOverdue $event): void
recipientId: $event->invoice->billable_id, recipientId: $event->invoice->billable_id,
recipientType: $event->invoice->billable_type, recipientType: $event->invoice->billable_type,
data: [ data: [
'participant_name' => $event->invoice->billable?->person?->full_name ?? '',
'invoice_number' => $event->invoice->number, 'invoice_number' => $event->invoice->number,
'total_amount' => $event->invoice->total_amount, 'total_amount' => number_format($event->invoice->total_amount / 100, 2),
'balance_due' => $event->invoice->due_amount, 'balance_due' => number_format($event->invoice->due_amount / 100, 2),
'due_date' => $event->invoice->due_date?->format('Y-m-d'), '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) { } catch (\Throwable $e) {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
use App\Domain\Financial\Events\PaymentReceived; use App\Domain\Financial\Events\PaymentReceived;
use App\Domain\Notification\Services\NotificationService; use App\Domain\Notification\Services\NotificationService;
use App\Domain\Shared\Helpers\PublicLinkHelper;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
...@@ -21,10 +22,11 @@ public function handle(PaymentReceived $event): void ...@@ -21,10 +22,11 @@ public function handle(PaymentReceived $event): void
recipientId: $event->payment->payer_id, recipientId: $event->payment->payer_id,
recipientType: $event->payment->payer_type, recipientType: $event->payment->payer_type,
data: [ data: [
'payment_reference' => $event->payment->reference, 'participant_name' => $event->payment->invoice?->billable?->person?->full_name ?? '',
'amount' => $event->payment->amount, 'payment_amount' => number_format($event->payment->amount / 100, 2),
'method' => $event->payment->method, 'payment_method' => $event->payment->method?->value ?? $event->payment->method ?? '',
'invoice_number' => $event->payment->invoice?->number, 'invoice_number' => $event->payment->invoice?->number ?? '',
'receipt_link' => PublicLinkHelper::receiptLink($event->payment),
], ],
); );
} catch (\Throwable $e) { } 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 ...@@ -17,8 +17,12 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Financial\Events\InvoiceOverdue::class => [ \App\Domain\Financial\Events\InvoiceOverdue::class => [
\App\Domain\Financial\Listeners\SendOverdueReminder::class, \App\Domain\Financial\Listeners\SendOverdueReminder::class,
], ],
\App\Domain\Financial\Events\InvoiceSent::class => [], \App\Domain\Financial\Events\InvoiceSent::class => [
\App\Domain\Financial\Events\InvoiceCancelled::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\Events\InstallmentOverdue::class => [
\App\Domain\Financial\Listeners\NotifyInstallmentDue::class, \App\Domain\Financial\Listeners\NotifyInstallmentDue::class,
], ],
...@@ -27,10 +31,18 @@ class EventServiceProvider extends ServiceProvider ...@@ -27,10 +31,18 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Financial\Listeners\SendPaymentNotification::class, \App\Domain\Financial\Listeners\SendPaymentNotification::class,
\App\Domain\Financial\Listeners\UpdateCashSessionTotals::class, \App\Domain\Financial\Listeners\UpdateCashSessionTotals::class,
], ],
\App\Domain\Financial\Events\PaymentPlanDefaulted::class => [], \App\Domain\Financial\Events\PaymentPlanDefaulted::class => [
\App\Domain\Financial\Events\WalletDeposited::class => [], \App\Domain\Financial\Listeners\SendPaymentPlanDefaultedNotification::class,
\App\Domain\Financial\Events\WalletFrozen::class => [], ],
\App\Domain\Financial\Events\CashSessionClosed::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 // Training Events
\App\Domain\Training\Events\EnrollmentCreated::class => [ \App\Domain\Training\Events\EnrollmentCreated::class => [
...@@ -42,7 +54,9 @@ class EventServiceProvider extends ServiceProvider ...@@ -42,7 +54,9 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Training\Listeners\RemoveAttendanceRecords::class, \App\Domain\Training\Listeners\RemoveAttendanceRecords::class,
\App\Domain\Training\Listeners\ProcessWaitlist::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\Events\SessionCreated::class => [
\App\Domain\Training\Listeners\CreateAutoReservation::class, \App\Domain\Training\Listeners\CreateAutoReservation::class,
], ],
...@@ -50,13 +64,21 @@ class EventServiceProvider extends ServiceProvider ...@@ -50,13 +64,21 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Training\Listeners\NotifySessionCancellation::class, \App\Domain\Training\Listeners\NotifySessionCancellation::class,
\App\Domain\Training\Listeners\CancelLinkedReservation::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\Events\WaitlistSpotAvailable::class => [
\App\Domain\Training\Listeners\NotifyWaitlistSpot::class, \App\Domain\Training\Listeners\NotifyWaitlistSpot::class,
], ],
\App\Domain\Training\Events\ParticipantRegistered::class => [], \App\Domain\Training\Events\ParticipantRegistered::class => [
\App\Domain\Training\Events\ParticipantStatusChanged::class => [], \App\Domain\Training\Listeners\SendParticipantRegisteredNotification::class,
\App\Domain\Training\Events\EvaluationShared::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 // Attendance Events
\App\Domain\Attendance\Events\AttendanceMarked::class => [], \App\Domain\Attendance\Events\AttendanceMarked::class => [],
...@@ -85,11 +107,17 @@ class EventServiceProvider extends ServiceProvider ...@@ -85,11 +107,17 @@ class EventServiceProvider extends ServiceProvider
\App\Domain\Scheduling\Events\AssignmentEnded::class => [], \App\Domain\Scheduling\Events\AssignmentEnded::class => [],
// Participant Events // Participant Events
\App\Domain\Participant\Events\ParticipantSuspended::class => [], \App\Domain\Participant\Events\ParticipantSuspended::class => [
\App\Domain\Participant\Events\ParticipantFrozen::class => [], \App\Domain\Participant\Listeners\SendParticipantSuspendedNotification::class,
],
\App\Domain\Participant\Events\ParticipantFrozen::class => [
\App\Domain\Participant\Listeners\SendParticipantFrozenNotification::class,
],
// POS Events // POS Events
\App\Domain\POS\Events\POSTransactionCompleted::class => [], \App\Domain\POS\Events\POSTransactionCompleted::class => [
\App\Domain\POS\Listeners\SendPOSReceiptNotification::class,
],
]; ];
public function shouldDiscoverEvents(): bool public function shouldDiscoverEvents(): bool
......
This diff is collapsed.
<!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 @@ ...@@ -66,6 +66,7 @@
use App\Livewire\Inventory\WarehouseList as InventoryWarehouseList; use App\Livewire\Inventory\WarehouseList as InventoryWarehouseList;
use App\Livewire\Wallets\WalletList; use App\Livewire\Wallets\WalletList;
use App\Livewire\Wallets\WalletShow; use App\Livewire\Wallets\WalletShow;
use App\Http\Controllers\PublicDocumentController;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
...@@ -76,6 +77,13 @@ ...@@ -76,6 +77,13 @@
*/ */
Route::get('/health', \App\Http\Controllers\HealthController::class)->name('health'); 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 | 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