Commit 909a0b40 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add reports, admin tools, print views, guardian portal, and 15+ features

- Financial report with daily revenue chart and top programs
- Attendance report with per-participant stats and CSV export
- Activity log viewer with filters and expandable details
- System settings page with grouped tabs
- Guardian portal dashboard (children, attendance, invoices)
- Payment plan creation UI with installment preview
- Coupon validator component
- Invoice, group schedule, participant card, and certificate print views
- Session rescheduling component
- Participant freeze/unfreeze, bulk status change, status timeline
- Enrollment history component
- Dashboard widgets (revenue, enrollment trends)
- Dark mode toggle, language switcher, locale middleware
- API stats endpoint for mobile
- Parent weekly SMS report command
- Group capacity alert command
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent b4c9f796
<?php
namespace App\Console\Commands;
use App\Domain\Training\Models\TrainingGroup;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class AlertFullGroups extends Command
{
protected $signature = 'groups:alert-capacity {--threshold=90 : Percentage threshold}';
protected $description = 'Alert admins about groups nearing capacity';
public function handle(): int
{
$threshold = (int) $this->option('threshold');
$alerts = [];
$groups = TrainingGroup::where('status', 'active')
->where('max_capacity', '>', 0)
->get();
foreach ($groups as $group) {
$percentage = round(($group->current_count / $group->max_capacity) * 100);
if ($percentage >= $threshold) {
$alerts[] = [
'group' => $group->name_ar,
'capacity' => "{$group->current_count}/{$group->max_capacity}",
'percentage' => $percentage,
];
}
}
if (empty($alerts)) {
$this->info("No groups at {$threshold}%+ capacity.");
return self::SUCCESS;
}
$this->table(['المجموعة', 'السعة', 'النسبة%'], array_map(fn ($a) => [$a['group'], $a['capacity'], $a['percentage'] . '%'], $alerts));
Log::channel('daily')->info("Groups nearing capacity ({$threshold}%+)", ['groups' => $alerts]);
$this->info(count($alerts) . " groups at {$threshold}%+ capacity.");
return self::SUCCESS;
}
}
<?php
namespace App\Console\Commands;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ReconcileFinancials extends Command
{
protected $signature = 'financials:reconcile {--fix : Auto-fix discrepancies}';
protected $description = 'Check invoice paid_amount matches sum of confirmed payments';
public function handle(): int
{
$fix = $this->option('fix');
$discrepancies = 0;
$invoices = Invoice::whereIn('status', ['partially_paid', 'paid', 'overdue', 'sent'])
->get();
foreach ($invoices as $invoice) {
$actualPaid = Payment::where('invoice_id', $invoice->id)
->where('status', 'confirmed')
->sum('amount');
if ((int) $invoice->paid_amount !== (int) $actualPaid) {
$discrepancies++;
$this->warn("Invoice {$invoice->invoice_number}: stored={$invoice->paid_amount}, actual={$actualPaid}");
if ($fix) {
$invoice->update([
'paid_amount' => $actualPaid,
'due_amount' => max(0, $invoice->total_amount - $actualPaid),
]);
if ($actualPaid >= $invoice->total_amount && $invoice->status !== 'paid') {
$invoice->update(['status' => 'paid']);
} elseif ($actualPaid > 0 && $actualPaid < $invoice->total_amount && $invoice->status === 'paid') {
$invoice->update(['status' => 'partially_paid']);
}
$this->info(" → Fixed");
}
}
}
if ($discrepancies === 0) {
$this->info("All {$invoices->count()} invoices reconcile correctly.");
} else {
$level = $fix ? 'info' : 'warn';
$this->$level("{$discrepancies} discrepancies found" . ($fix ? ' and fixed.' : '. Run with --fix to correct.'));
if (!$fix) {
Log::warning("Financial reconciliation: {$discrepancies} discrepancies found");
}
}
return self::SUCCESS;
}
}
<?php
namespace App\Console\Commands;
use App\Domain\Notification\Services\NotificationService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Models\Enrollment;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SendParentWeeklyReport extends Command
{
protected $signature = 'reports:parent-weekly';
protected $description = 'Send weekly attendance/progress reports to guardians via SMS';
public function handle(NotificationService $notificationService): int
{
$participants = Participant::where('status', 'active')
->whereHas('guardians')
->with(['guardians', 'enrollments' => fn ($q) => $q->where('status', 'active')])
->get();
$sent = 0;
$weekStart = now()->startOfWeek()->toDateString();
$weekEnd = now()->endOfWeek()->toDateString();
foreach ($participants as $participant) {
$attendance = DB::table('attendance_records')
->where('subject_type', 'participant')
->where('subject_id', $participant->id)
->whereBetween('date', [$weekStart, $weekEnd])
->selectRaw("
COUNT(*) as total,
SUM(CASE WHEN status IN ('present','late','partial') THEN 1 ELSE 0 END) as attended,
SUM(CASE WHEN status = 'absent' THEN 1 ELSE 0 END) as absent
")
->first();
if (!$attendance || $attendance->total == 0) {
continue;
}
$rate = round(($attendance->attended / $attendance->total) * 100);
foreach ($participant->guardians as $guardian) {
$phone = $guardian->phone ?? $guardian->person?->phone ?? null;
if (!$phone) continue;
$message = "تقرير أسبوعي - {$participant->full_name_ar}: "
. "حضر {$attendance->attended}/{$attendance->total} حصة "
. "({$rate}%) - غياب: {$attendance->absent}";
try {
$notificationService->send(
channel: 'sms',
recipient: $phone,
subject: 'تقرير أسبوعي',
body: $message,
eventType: 'parent.weekly_report',
);
$sent++;
} catch (\Throwable $e) {
$this->warn("Failed for {$participant->full_name_ar}: {$e->getMessage()}");
}
}
}
$this->info("Sent {$sent} weekly reports to guardians.");
return self::SUCCESS;
}
}
<?php
namespace App\Http\Controllers\Api;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Models\Enrollment;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class QuickStatsController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$today = now()->toDateString();
return response()->json([
'participants' => [
'total' => Participant::count(),
'active' => Participant::where('status', 'active')->count(),
'new_this_month' => Participant::where('created_at', '>=', now()->startOfMonth())->count(),
],
'enrollments' => [
'active' => Enrollment::where('status', 'active')->count(),
'pending' => Enrollment::where('status', 'pending')->count(),
'new_today' => Enrollment::whereDate('created_at', $today)->count(),
],
'financial' => [
'revenue_today' => Payment::where('status', 'confirmed')->whereDate('created_at', $today)->sum('amount'),
'revenue_month' => Payment::where('status', 'confirmed')->where('created_at', '>=', now()->startOfMonth())->sum('amount'),
'outstanding' => Invoice::whereIn('status', ['sent', 'partially_paid', 'overdue'])->sum('balance_due'),
'overdue_count' => Invoice::where('status', 'overdue')->count(),
],
]);
}
}
<?php
namespace App\Http\Controllers;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Models\Enrollment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CertificateController extends Controller
{
public function attendance(Participant $participant, Enrollment $enrollment)
{
$this->authorize('participants.view');
$enrollment->load('trainingGroup.program');
$stats = DB::table('attendance_records')
->where('subject_type', 'participant')
->where('subject_id', $participant->id)
->join('training_sessions', 'training_sessions.id', '=', 'attendance_records.training_session_id')
->where('training_sessions.training_group_id', $enrollment->training_group_id)
->selectRaw("
COUNT(*) as total,
SUM(CASE WHEN attendance_records.status IN ('present','late','partial') THEN 1 ELSE 0 END) as attended
")
->first();
$rate = $stats && $stats->total > 0 ? round(($stats->attended / $stats->total) * 100) : 0;
return view('print.certificate', compact('participant', 'enrollment', 'stats', 'rate'));
}
}
<?php
namespace App\Http\Controllers;
use App\Domain\Training\Models\TrainingGroup;
use Illuminate\Http\Request;
class GroupSchedulePrintController extends Controller
{
public function __invoke(TrainingGroup $group)
{
$this->authorize('groups.list');
$group->load(['program', 'schedules', 'enrollments' => function ($q) {
$q->where('status', 'active')->with('participant');
}]);
return view('print.group-schedule', compact('group'));
}
}
<?php
namespace App\Http\Controllers;
use App\Domain\Financial\Models\Invoice;
use Illuminate\Http\Request;
class InvoicePrintController extends Controller
{
public function __invoke(Invoice $invoice)
{
$this->authorize('invoices.view');
$invoice->load(['items', 'payments', 'billable']);
return view('print.invoice', compact('invoice'));
}
}
<?php
namespace App\Http\Controllers;
use App\Domain\Participant\Models\Participant;
use Illuminate\Http\Request;
class ParticipantCardController extends Controller
{
public function __invoke(Participant $participant)
{
$this->authorize('participants.view');
$participant->load(['enrollments' => function ($q) {
$q->where('status', 'active')->with('trainingGroup.program');
}]);
return view('print.participant-card', compact('participant'));
}
}
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = session('locale', config('app.locale', 'ar'));
if (in_array($locale, ['ar', 'en'])) {
app()->setLocale($locale);
}
return $next($request);
}
}
<?php
namespace App\Livewire\Admin;
use App\Domain\Audit\Models\AuditLog;
use App\Models\User;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('سجل النشاطات')]
class ActivityLog extends Component
{
use WithPagination;
#[Url]
public string $search = '';
#[Url]
public string $actionFilter = '';
#[Url]
public string $dateFrom = '';
#[Url]
public string $dateTo = '';
#[Url]
public string $userFilter = '';
public int $expandedRow = 0;
public function mount(): void
{
$this->authorize('audit.view');
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedActionFilter(): void
{
$this->resetPage();
}
public function updatedDateFrom(): void
{
$this->resetPage();
}
public function updatedDateTo(): void
{
$this->resetPage();
}
public function updatedUserFilter(): void
{
$this->resetPage();
}
public function toggleExpand(int $id): void
{
$this->expandedRow = $this->expandedRow === $id ? 0 : $id;
}
public function resetFilters(): void
{
$this->reset(['search', 'actionFilter', 'dateFrom', 'dateTo', 'userFilter']);
$this->resetPage();
}
/**
* Translate auditable_type class name to Arabic.
*/
public static function translateEntityType(string $type): string
{
$shortName = class_basename($type);
$map = [
'User' => 'مستخدم',
'Academy' => 'أكاديمية',
'Branch' => 'فرع',
'Person' => 'شخص',
'Participant' => 'مشارك',
'Guardian' => 'ولي أمر',
'Invoice' => 'فاتورة',
'Payment' => 'دفعة',
'Transaction' => 'معاملة مالية',
'Enrollment' => 'تسجيل',
'TrainingProgram' => 'برنامج تدريبي',
'TrainingGroup' => 'مجموعة تدريبية',
'TrainingSession' => 'حصة تدريبية',
'Facility' => 'منشأة',
'Product' => 'منتج',
'Warehouse' => 'مستودع',
'Role' => 'دور',
'Activity' => 'نشاط',
'Wallet' => 'محفظة',
'CashSession' => 'وردية',
'InventoryMovement' => 'حركة مخزون',
'AttendanceRecord' => 'سجل حضور',
'AuditLog' => 'سجل تدقيق',
];
return $map[$shortName] ?? $shortName;
}
public function render()
{
$query = AuditLog::query()
->with('user')
->when($this->search, function ($q) {
$search = $this->search;
$q->where(function ($q2) use ($search) {
$q2->where('auditable_type', 'ilike', "%{$search}%")
->orWhere('ip_address', 'ilike', "%{$search}%")
->orWhereHas('user', fn ($uq) => $uq->where('name', 'ilike', "%{$search}%")
->orWhere('name_ar', 'ilike', "%{$search}%"));
});
})
->when($this->actionFilter, fn ($q) => $q->where('action', $this->actionFilter))
->when($this->dateFrom, fn ($q) => $q->whereDate('created_at', '>=', $this->dateFrom))
->when($this->dateTo, fn ($q) => $q->whereDate('created_at', '<=', $this->dateTo))
->when($this->userFilter, fn ($q) => $q->where('user_id', $this->userFilter))
->orderByDesc('created_at');
$users = User::query()
->select('id', 'name', 'name_ar')
->orderBy('name_ar')
->get();
return view('livewire.admin.activity-log', [
'logs' => $query->paginate(25),
'users' => $users,
]);
}
}
<?php
namespace App\Livewire\Admin;
use App\Domain\Shared\Models\SystemSetting;
use App\Domain\Shared\Services\SettingsService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('إعدادات النظام')]
class SystemSettings extends Component
{
public string $activeTab = 'general';
public array $settings = [];
protected array $settingsSchema = [
'general' => [
'academy_name_ar' => ['type' => 'string', 'label' => 'اسم الأكاديمية (عربي)'],
'academy_name_en' => ['type' => 'string', 'label' => 'اسم الأكاديمية (إنجليزي)'],
'default_locale' => ['type' => 'select', 'label' => 'اللغة الافتراضية', 'options' => ['ar' => 'العربية', 'en' => 'English']],
'timezone' => ['type' => 'select', 'label' => 'المنطقة الزمنية', 'options' => ['Africa/Cairo' => 'القاهرة', 'Asia/Riyadh' => 'الرياض', 'Asia/Dubai' => 'دبي', 'UTC' => 'UTC']],
],
'financial' => [
'currency_code' => ['type' => 'select', 'label' => 'رمز العملة', 'options' => ['EGP' => 'جنيه مصري (EGP)', 'SAR' => 'ريال سعودي (SAR)', 'AED' => 'درهم إماراتي (AED)', 'USD' => 'دولار أمريكي (USD)']],
'tax_rate' => ['type' => 'number', 'label' => 'نسبة الضريبة (%)', 'step' => '0.01', 'min' => 0, 'max' => 100],
'max_discount_percent' => ['type' => 'number', 'label' => 'الحد الأقصى للخصم (%)', 'step' => '1', 'min' => 0, 'max' => 100],
'wallet_enabled' => ['type' => 'boolean', 'label' => 'تفعيل المحفظة'],
],
'attendance' => [
'grace_period_participants' => ['type' => 'number', 'label' => 'فترة السماح للمشتركين (دقيقة)', 'step' => '1', 'min' => 0],
'grace_period_trainers' => ['type' => 'number', 'label' => 'فترة السماح للمدربين (دقيقة)', 'step' => '1', 'min' => 0],
'auto_absent_delay' => ['type' => 'number', 'label' => 'تأخير الغياب التلقائي (ساعة)', 'step' => '1', 'min' => 1],
'consecutive_absence_threshold' => ['type' => 'number', 'label' => 'حد الغياب المتتالي', 'step' => '1', 'min' => 1],
'min_attendance_percentage' => ['type' => 'number', 'label' => 'الحد الأدنى لنسبة الحضور (%)', 'step' => '1', 'min' => 0, 'max' => 100],
],
'notifications' => [
'sms_enabled' => ['type' => 'boolean', 'label' => 'تفعيل الرسائل القصيرة'],
'email_enabled' => ['type' => 'boolean', 'label' => 'تفعيل البريد الإلكتروني'],
'digest_time' => ['type' => 'time', 'label' => 'وقت الملخص اليومي'],
'sms_rate_limit' => ['type' => 'number', 'label' => 'حد الرسائل القصيرة (بالساعة)', 'step' => '1', 'min' => 1],
],
'enrollment' => [
'allow_waitlist' => ['type' => 'boolean', 'label' => 'تفعيل قائمة الانتظار'],
'max_waitlist_per_group' => ['type' => 'number', 'label' => 'الحد الأقصى لقائمة الانتظار لكل مجموعة', 'step' => '1', 'min' => 0],
'auto_promote_waitlist' => ['type' => 'boolean', 'label' => 'ترقية تلقائية من قائمة الانتظار'],
],
];
protected array $defaults = [
'academy_name_ar' => '',
'academy_name_en' => '',
'default_locale' => 'ar',
'timezone' => 'Africa/Cairo',
'currency_code' => 'EGP',
'tax_rate' => '14',
'max_discount_percent' => '50',
'wallet_enabled' => '0',
'grace_period_participants' => '15',
'grace_period_trainers' => '10',
'auto_absent_delay' => '2',
'consecutive_absence_threshold' => '5',
'min_attendance_percentage' => '75',
'sms_enabled' => '0',
'email_enabled' => '1',
'digest_time' => '08:00',
'sms_rate_limit' => '100',
'allow_waitlist' => '1',
'max_waitlist_per_group' => '10',
'auto_promote_waitlist' => '1',
];
public function mount(): void
{
$this->authorize('settings.manage');
$this->loadAllSettings();
}
public function loadAllSettings(): void
{
$academyId = app('current_academy')?->id;
if (!$academyId) {
return;
}
$records = SystemSetting::withoutGlobalScope('academy')
->where('academy_id', $academyId)
->get()
->mapWithKeys(fn ($s) => [$s->key => $s->value])
->toArray();
// Merge defaults with stored values
$this->settings = array_merge($this->defaults, $records);
}
public function save(SettingsService $service): void
{
$group = $this->activeTab;
$schema = $this->settingsSchema[$group] ?? [];
foreach ($schema as $key => $config) {
$value = $this->settings[$key] ?? $this->defaults[$key] ?? '';
// Normalize boolean values
if ($config['type'] === 'boolean') {
$value = $value ? '1' : '0';
}
$type = match ($config['type']) {
'boolean' => 'boolean',
'number' => str_contains($config['step'] ?? '1', '.') ? 'float' : 'integer',
default => 'string',
};
$service->set($key, $value, $group, $type);
}
session()->flash('success', __('تم حفظ الإعدادات بنجاح'));
}
public function getActiveSchemaProperty(): array
{
return $this->settingsSchema[$this->activeTab] ?? [];
}
public function render()
{
$tabs = [
'general' => __('عام'),
'financial' => __('مالي'),
'attendance' => __('الحضور'),
'notifications' => __('الإشعارات'),
'enrollment' => __('التسجيل'),
];
return view('livewire.admin.system-settings', [
'tabs' => $tabs,
'schema' => $this->settingsSchema[$this->activeTab] ?? [],
]);
}
}
...@@ -16,10 +16,30 @@ class TakeAttendance extends Component ...@@ -16,10 +16,30 @@ class TakeAttendance extends Component
{ {
public TrainingSession $session; public TrainingSession $session;
public string $sessionNotes = '';
/** @var array<int, string> Per-record notes keyed by record id */
public array $recordNotes = [];
public function mount(TrainingSession $session): void public function mount(TrainingSession $session): void
{ {
$this->authorize('attendance.mark'); $this->authorize('attendance.mark');
$this->session = $session->load(['group']); $this->session = $session->load(['group']);
$this->sessionNotes = $this->session->notes ?? '';
}
public function saveNotes(): void
{
$this->session->update(['notes' => $this->sessionNotes]);
session()->flash('notes_saved', __('تم حفظ ملاحظات الجلسة'));
}
public function saveRecordNote(int $recordId): void
{
$record = AttendanceRecord::findOrFail($recordId);
$note = $this->recordNotes[$recordId] ?? '';
$record->update(['notes' => $note]);
session()->flash("record_note_saved_{$recordId}", __('تم حفظ الملاحظة'));
} }
public function markAs(int $recordId, string $status, AttendanceMarkingService $service): void public function markAs(int $recordId, string $status, AttendanceMarkingService $service): void
...@@ -54,6 +74,13 @@ public function render() ...@@ -54,6 +74,13 @@ public function render()
->orderBy('status') ->orderBy('status')
->get(); ->get();
// Populate per-record notes from DB (only on first load or if not yet set)
foreach ($records as $record) {
if (!array_key_exists($record->id, $this->recordNotes)) {
$this->recordNotes[$record->id] = $record->notes ?? '';
}
}
$summary = [ $summary = [
'total' => $records->count(), 'total' => $records->count(),
'present' => $records->where('status', AttendanceStatus::Present)->count(), 'present' => $records->where('status', AttendanceStatus::Present)->count(),
......
<?php
namespace App\Livewire\Components;
use Livewire\Component;
class DarkModeToggle extends Component
{
public function render()
{
return view('livewire.components.dark-mode-toggle');
}
}
<?php
namespace App\Livewire\Components;
use Livewire\Component;
class LanguageSwitcher extends Component
{
public string $locale;
public function mount(): void
{
$this->locale = session('locale', app()->getLocale());
}
public function switchTo(string $locale): void
{
if (!in_array($locale, ['ar', 'en'])) {
return;
}
session(['locale' => $locale]);
app()->setLocale($locale);
$this->locale = $locale;
$this->redirect(request()->header('Referer', '/'), navigate: true);
}
public function render()
{
return view('livewire.components.language-switcher');
}
}
<?php
namespace App\Livewire\Dashboard;
use App\Domain\Training\Models\Enrollment;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class EnrollmentTrends extends Component
{
public string $period = '30';
public function render()
{
$days = (int) $this->period;
$startDate = now()->subDays($days)->toDateString();
$dailyEnrollments = Enrollment::where('created_at', '>=', $startDate)
->select(DB::raw('DATE(created_at) as date'), DB::raw('COUNT(*) as count'))
->groupBy('date')
->orderBy('date')
->pluck('count', 'date')
->toArray();
$totalNew = array_sum($dailyEnrollments);
$cancelled = Enrollment::where('status', 'cancelled')
->where('updated_at', '>=', $startDate)
->count();
$active = Enrollment::where('status', 'active')->count();
return view('livewire.dashboard.enrollment-trends', [
'dailyEnrollments' => $dailyEnrollments,
'totalNew' => $totalNew,
'cancelled' => $cancelled,
'active' => $active,
]);
}
}
<?php
namespace App\Livewire\Dashboard;
use App\Domain\Financial\Models\Payment;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class RevenueWidget extends Component
{
public string $period = 'month';
public function render()
{
$startDate = match ($this->period) {
'today' => now()->startOfDay(),
'week' => now()->startOfWeek(),
'month' => now()->startOfMonth(),
'year' => now()->startOfYear(),
default => now()->startOfMonth(),
};
$revenue = Payment::where('status', 'confirmed')
->where('created_at', '>=', $startDate)
->sum('amount');
$previousStart = match ($this->period) {
'today' => now()->subDay()->startOfDay(),
'week' => now()->subWeek()->startOfWeek(),
'month' => now()->subMonth()->startOfMonth(),
'year' => now()->subYear()->startOfYear(),
default => now()->subMonth()->startOfMonth(),
};
$previousRevenue = Payment::where('status', 'confirmed')
->whereBetween('created_at', [$previousStart, $startDate])
->sum('amount');
$change = $previousRevenue > 0
? round(($revenue - $previousRevenue) / $previousRevenue * 100, 1)
: ($revenue > 0 ? 100 : 0);
$byMethod = Payment::where('status', 'confirmed')
->where('created_at', '>=', $startDate)
->select('payment_method', DB::raw('SUM(amount) as total'))
->groupBy('payment_method')
->pluck('total', 'payment_method')
->toArray();
return view('livewire.dashboard.revenue-widget', [
'revenue' => $revenue,
'change' => $change,
'byMethod' => $byMethod,
]);
}
}
<?php
namespace App\Livewire\Financial;
use App\Domain\Pricing\Models\Promotion;
use Carbon\Carbon;
use Livewire\Component;
class CouponValidator extends Component
{
public string $code = '';
public ?array $result = null;
public bool $checking = false;
public function check(): void
{
$this->result = null;
if (empty(trim($this->code))) {
return;
}
$promotion = Promotion::where('code', strtoupper(trim($this->code)))
->where('is_active', true)
->first();
if (!$promotion) {
$this->result = ['valid' => false, 'message' => __('كود غير صالح')];
return;
}
if ($promotion->start_date && Carbon::parse($promotion->start_date)->isFuture()) {
$this->result = ['valid' => false, 'message' => __('هذا العرض لم يبدأ بعد')];
return;
}
if ($promotion->end_date && Carbon::parse($promotion->end_date)->isPast()) {
$this->result = ['valid' => false, 'message' => __('انتهت صلاحية هذا العرض')];
return;
}
if ($promotion->max_uses && $promotion->times_used >= $promotion->max_uses) {
$this->result = ['valid' => false, 'message' => __('تم استنفاد عدد مرات الاستخدام')];
return;
}
$this->result = [
'valid' => true,
'message' => __('كود صالح'),
'name' => $promotion->name_ar ?? $promotion->name,
'type' => $promotion->adjustment_type,
'value' => $promotion->adjustment_value,
'remaining' => $promotion->max_uses ? ($promotion->max_uses - $promotion->times_used) : null,
'expires' => $promotion->end_date,
];
$this->dispatch('coupon-validated', promotionId: $promotion->id, code: $this->code);
}
public function render()
{
return view('livewire.financial.coupon-validator');
}
}
<?php
namespace App\Livewire\Financial;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\PaymentPlan;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('components.layouts.app')]
class PaymentPlanCreate extends Component
{
public ?int $invoiceId = null;
public int $installmentCount = 3;
public string $frequency = 'monthly';
public string $startDate = '';
public int $downPayment = 0;
public array $installments = [];
public function mount(?int $invoice = null): void
{
$this->authorize('invoices.create');
$this->invoiceId = $invoice;
$this->startDate = now()->addDays(7)->toDateString();
$this->calculateInstallments();
}
public function updatedInstallmentCount(): void
{
$this->calculateInstallments();
}
public function updatedFrequency(): void
{
$this->calculateInstallments();
}
public function updatedStartDate(): void
{
$this->calculateInstallments();
}
public function updatedDownPayment(): void
{
$this->calculateInstallments();
}
private function calculateInstallments(): void
{
if (!$this->invoiceId) {
$this->installments = [];
return;
}
$invoice = Invoice::find($this->invoiceId);
if (!$invoice) return;
$remaining = $invoice->balance_due - ($this->downPayment * 100);
if ($remaining <= 0 || $this->installmentCount < 1) {
$this->installments = [];
return;
}
$perInstallment = intdiv($remaining, $this->installmentCount);
$lastExtra = $remaining - ($perInstallment * $this->installmentCount);
$startDate = Carbon::parse($this->startDate);
$this->installments = [];
for ($i = 0; $i < $this->installmentCount; $i++) {
$dueDate = match ($this->frequency) {
'weekly' => $startDate->copy()->addWeeks($i),
'biweekly' => $startDate->copy()->addWeeks($i * 2),
'monthly' => $startDate->copy()->addMonths($i),
default => $startDate->copy()->addMonths($i),
};
$amount = $perInstallment + ($i === $this->installmentCount - 1 ? $lastExtra : 0);
$this->installments[] = [
'number' => $i + 1,
'due_date' => $dueDate->toDateString(),
'amount' => $amount,
];
}
}
public function save(): void
{
$this->authorize('invoices.create');
$invoice = Invoice::findOrFail($this->invoiceId);
DB::transaction(function () use ($invoice) {
$plan = PaymentPlan::create([
'academy_id' => $invoice->academy_id,
'invoice_id' => $invoice->id,
'total_amount' => $invoice->balance_due,
'down_payment' => $this->downPayment * 100,
'installment_count' => $this->installmentCount,
'frequency' => $this->frequency,
'start_date' => $this->startDate,
'status' => 'active',
'created_by' => auth()->id(),
]);
foreach ($this->installments as $inst) {
$plan->installments()->create([
'academy_id' => $invoice->academy_id,
'installment_number' => $inst['number'],
'due_date' => $inst['due_date'],
'amount' => $inst['amount'],
'paid_amount' => 0,
'status' => 'pending',
]);
}
});
session()->flash('success', __('تم إنشاء خطة الدفع بنجاح'));
$this->redirect(route('invoices.show', $invoice));
}
public function render()
{
$invoice = $this->invoiceId ? Invoice::find($this->invoiceId) : null;
$invoices = Invoice::whereIn('status', ['sent', 'partially_paid', 'overdue'])
->where('balance_due', '>', 0)
->orderByDesc('created_at')
->limit(50)
->get();
return view('livewire.financial.payment-plan-create', [
'invoice' => $invoice,
'invoices' => $invoices,
]);
}
}
<?php
namespace App\Livewire\Guardian;
use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Identity\Models\Guardian;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Enums\SessionStatus;
use App\Domain\Training\Models\TrainingSession;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('بوابة ولي الأمر')]
class GuardianDashboard extends Component
{
public function mount(): void
{
// Guardian role doesn't have a specific permission — guard by role check
$user = auth()->user();
if (! $user->person_id) {
abort(403, __('لا يوجد ملف شخصي مرتبط بحسابك'));
}
// Ensure user is actually a guardian
$guardian = Guardian::where('person_id', $user->person_id)->first()
?? Guardian::where('user_id', $user->id)->first();
if (! $guardian) {
abort(403, __('هذه الصفحة مخصصة لأولياء الأمور فقط'));
}
}
public function render()
{
$user = auth()->user();
// Find guardian record for this user
$guardian = Guardian::where('person_id', $user->person_id)->first()
?? Guardian::where('user_id', $user->id)->first();
// Get participant IDs for this guardian's children
$participantIds = $guardian->participants()->pluck('participants.id');
// Load children with their person data and active enrollments
$children = Participant::whereIn('id', $participantIds)
->with(['person', 'activeEnrollments.group', 'activeEnrollments.program'])
->get();
// Calculate attendance rates per child
$attendanceData = [];
foreach ($children as $child) {
$totalRecords = AttendanceRecord::where('subject_type', Participant::class)
->where('subject_id', $child->id)
->whereNotIn('status', ['cancelled', 'exempt'])
->count();
$positiveRecords = AttendanceRecord::where('subject_type', Participant::class)
->where('subject_id', $child->id)
->whereIn('status', ['present', 'late', 'partial'])
->count();
$attendanceData[$child->id] = [
'rate' => $totalRecords > 0 ? round(($positiveRecords / $totalRecords) * 100, 1) : null,
'total' => $totalRecords,
'positive' => $positiveRecords,
];
}
// Upcoming sessions for children's active groups
$activeGroupIds = $children->flatMap(function ($child) {
return $child->activeEnrollments->pluck('training_group_id');
})->unique()->filter();
$upcomingSessions = TrainingSession::whereIn('training_group_id', $activeGroupIds)
->where('session_date', '>=', now()->toDateString())
->where('status', SessionStatus::Scheduled)
->with('group')
->orderBy('session_date')
->orderBy('start_time')
->limit(10)
->get();
// Invoices for children (billable = Participant)
$invoices = Invoice::where('billable_type', Participant::class)
->whereIn('billable_id', $participantIds)
->orderByDesc('issue_date')
->limit(20)
->get();
// Outstanding balance
$totalOutstanding = Invoice::where('billable_type', Participant::class)
->whereIn('billable_id', $participantIds)
->whereIn('status', ['sent', 'partially_paid', 'overdue'])
->sum('due_amount');
// Total paid
$totalPaid = Invoice::where('billable_type', Participant::class)
->whereIn('billable_id', $participantIds)
->sum('paid_amount');
return view('livewire.guardian.guardian-dashboard', [
'children' => $children,
'attendanceData' => $attendanceData,
'upcomingSessions' => $upcomingSessions,
'invoices' => $invoices,
'totalOutstanding' => $totalOutstanding,
'totalPaid' => $totalPaid,
]);
}
}
<?php
namespace App\Livewire\Participants;
use App\Domain\Participant\Models\Participant;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('components.layouts.app')]
class BulkStatusChange extends Component
{
public array $selectedIds = [];
public string $targetStatus = '';
public string $reason = '';
public string $currentFilter = 'active';
public bool $confirmModal = false;
private const VALID_TRANSITIONS = [
'active' => ['frozen', 'suspended', 'inactive', 'graduated', 'withdrawn'],
'frozen' => ['active', 'withdrawn', 'inactive'],
'suspended' => ['active', 'withdrawn'],
'inactive' => ['active', 'withdrawn'],
];
public function mount(): void
{
$this->authorize('participants.update');
}
public function getAvailableTargets(): array
{
return self::VALID_TRANSITIONS[$this->currentFilter] ?? [];
}
public function selectAll(): void
{
$this->selectedIds = Participant::where('status', $this->currentFilter)
->pluck('id')
->map(fn ($id) => (string) $id)
->toArray();
}
public function deselectAll(): void
{
$this->selectedIds = [];
}
public function openConfirm(): void
{
if (empty($this->selectedIds) || empty($this->targetStatus)) {
return;
}
$this->confirmModal = true;
}
public function execute(): void
{
$this->validate([
'targetStatus' => 'required',
'reason' => 'required|min:3',
]);
$allowed = self::VALID_TRANSITIONS[$this->currentFilter] ?? [];
if (!in_array($this->targetStatus, $allowed)) {
session()->flash('error', __('انتقال غير مسموح'));
return;
}
$updated = Participant::whereIn('id', $this->selectedIds)
->where('status', $this->currentFilter)
->update(['status' => $this->targetStatus]);
$this->selectedIds = [];
$this->confirmModal = false;
session()->flash('success', __('تم تحديث :count مشترك', ['count' => $updated]));
}
public function render()
{
$participants = Participant::where('status', $this->currentFilter)
->orderBy('created_at', 'desc')
->limit(100)
->get();
return view('livewire.participants.bulk-status-change', [
'participants' => $participants,
'availableTargets' => $this->getAvailableTargets(),
]);
}
}
<?php
namespace App\Livewire\Participants;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Models\Enrollment;
use Livewire\Component;
class EnrollmentHistory extends Component
{
public Participant $participant;
public bool $showAll = false;
public function mount(Participant $participant): void
{
$this->participant = $participant;
}
public function toggleShowAll(): void
{
$this->showAll = !$this->showAll;
}
public function render()
{
$query = Enrollment::where('participant_id', $this->participant->id)
->with(['trainingGroup.program'])
->orderByDesc('created_at');
$enrollments = $this->showAll ? $query->get() : $query->limit(5)->get();
$totalCount = Enrollment::where('participant_id', $this->participant->id)->count();
return view('livewire.participants.enrollment-history', [
'enrollments' => $enrollments,
'totalCount' => $totalCount,
]);
}
}
<?php
namespace App\Livewire\Participants;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use Livewire\Component;
class FreezeParticipant extends Component
{
public Participant $participant;
public bool $showModal = false;
public string $reason = '';
public string $action = '';
public function mount(Participant $participant): void
{
$this->participant = $participant;
}
public function openFreeze(): void
{
$this->action = 'freeze';
$this->reason = '';
$this->showModal = true;
}
public function openUnfreeze(): void
{
$this->action = 'unfreeze';
$this->reason = '';
$this->showModal = true;
}
public function confirm(): void
{
$this->validate(['reason' => 'required|min:3']);
$validTransitions = [
'active' => ['frozen'],
'frozen' => ['active'],
];
$targetStatus = $this->action === 'freeze' ? 'frozen' : 'active';
$allowed = $validTransitions[$this->participant->status] ?? [];
if (!in_array($targetStatus, $allowed)) {
session()->flash('error', __('لا يمكن تنفيذ هذا الإجراء'));
$this->showModal = false;
return;
}
$this->participant->update([
'status' => $targetStatus,
'notes' => ($this->participant->notes ? $this->participant->notes . "\n" : '')
. now()->format('Y-m-d') . " - " . ($this->action === 'freeze' ? 'تجميد' : 'إلغاء تجميد') . ": {$this->reason}",
]);
$this->showModal = false;
session()->flash('success', $this->action === 'freeze'
? __('تم تجميد المشترك بنجاح')
: __('تم إلغاء التجميد بنجاح'));
$this->dispatch('participant-updated');
}
public function render()
{
return view('livewire.participants.freeze-participant');
}
}
<?php
namespace App\Livewire\Participants;
use App\Domain\Participant\Models\Participant;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class StatusTimeline extends Component
{
public Participant $participant;
public function mount(Participant $participant): void
{
$this->participant = $participant;
}
public function render()
{
$history = DB::table('audit_logs')
->where('auditable_type', Participant::class)
->where('auditable_id', $this->participant->id)
->whereRaw("new_values::text LIKE '%status%'")
->orderByDesc('created_at')
->limit(20)
->get()
->map(function ($log) {
$old = json_decode($log->old_values, true);
$new = json_decode($log->new_values, true);
return [
'from' => $old['status'] ?? null,
'to' => $new['status'] ?? null,
'date' => $log->created_at,
'user' => DB::table('users')->where('id', $log->user_id)->value('name_ar') ?? '-',
];
})
->filter(fn ($item) => $item['to'] !== null);
return view('livewire.participants.status-timeline', [
'history' => $history,
]);
}
}
<?php
namespace App\Livewire\Profile;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('تفضيلات الإشعارات')]
class NotificationPreferences extends Component
{
public array $preferences = [];
private array $eventTypes = [
'payment.received' => 'إيصال الدفع',
'enrollment_expiring' => 'انتهاء التسجيل',
'installment_due' => 'موعد القسط',
'invoice_overdue_reminder' => 'تذكير فاتورة متأخرة',
'daily_summary' => 'ملخص يومي',
'birthday_greeting' => 'تهنئة عيد ميلاد',
'low_stock_alert' => 'تنبيه نقص مخزون',
'bulk_message' => 'رسائل جماعية',
];
public function mount(): void
{
$userId = auth()->id();
$existing = DB::table('notification_preferences')
->where('user_id', $userId)
->pluck('channels', 'event_type')
->toArray();
foreach ($this->eventTypes as $type => $label) {
$channels = isset($existing[$type]) ? json_decode($existing[$type], true) : ['email' => true, 'sms' => true, 'in_app' => true];
$this->preferences[$type] = $channels;
}
}
public function save(): void
{
$userId = auth()->id();
foreach ($this->preferences as $eventType => $channels) {
DB::table('notification_preferences')
->updateOrInsert(
['user_id' => $userId, 'event_type' => $eventType],
['channels' => json_encode($channels), 'updated_at' => now()]
);
}
session()->flash('success', __('تم حفظ تفضيلات الإشعارات'));
}
public function render()
{
return view('livewire.profile.notification-preferences', [
'eventTypes' => $this->eventTypes,
]);
}
}
This diff is collapsed.
<?php
namespace App\Livewire\Reports;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
#[Layout('components.layouts.app')]
class FinancialReport extends Component
{
#[Url]
public string $period = 'month';
#[Url]
public string $dateFrom = '';
#[Url]
public string $dateTo = '';
public function mount(): void
{
$this->authorize('reports.view');
if (!$this->dateFrom) {
$this->dateFrom = now()->startOfMonth()->toDateString();
}
if (!$this->dateTo) {
$this->dateTo = now()->toDateString();
}
}
public function updatedPeriod(): void
{
match ($this->period) {
'today' => $this->dateFrom = $this->dateTo = now()->toDateString(),
'week' => (function () {
$this->dateFrom = now()->startOfWeek()->toDateString();
$this->dateTo = now()->toDateString();
})(),
'month' => (function () {
$this->dateFrom = now()->startOfMonth()->toDateString();
$this->dateTo = now()->toDateString();
})(),
'quarter' => (function () {
$this->dateFrom = now()->startOfQuarter()->toDateString();
$this->dateTo = now()->toDateString();
})(),
'year' => (function () {
$this->dateFrom = now()->startOfYear()->toDateString();
$this->dateTo = now()->toDateString();
})(),
default => null,
};
}
public function render()
{
$payments = Payment::where('status', 'confirmed')
->whereBetween('created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59'])
->get();
$totalRevenue = $payments->sum('amount');
$paymentsByMethod = $payments->groupBy('payment_method')
->map(fn ($group) => $group->sum('amount'));
$invoices = Invoice::whereBetween('created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59'])->get();
$totalInvoiced = $invoices->sum('total_amount');
$totalOutstanding = $invoices->whereIn('status', ['sent', 'partially_paid', 'overdue'])->sum('balance_due');
$overdueCount = $invoices->where('status', 'overdue')->count();
$dailyRevenue = Payment::where('status', 'confirmed')
->whereBetween('created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59'])
->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(amount) as total'))
->groupBy('date')
->orderBy('date')
->pluck('total', 'date')
->toArray();
$topPrograms = DB::table('invoice_items')
->join('invoices', 'invoices.id', '=', 'invoice_items.invoice_id')
->where('invoices.status', '!=', 'cancelled')
->whereBetween('invoices.created_at', [$this->dateFrom, $this->dateTo . ' 23:59:59'])
->whereNotNull('invoice_items.itemable_type')
->select('invoice_items.description', DB::raw('SUM(invoice_items.line_total) as revenue'), DB::raw('COUNT(*) as count'))
->groupBy('invoice_items.description')
->orderByDesc('revenue')
->limit(10)
->get();
return view('livewire.reports.financial-report', [
'totalRevenue' => $totalRevenue,
'paymentsByMethod' => $paymentsByMethod,
'totalInvoiced' => $totalInvoiced,
'totalOutstanding' => $totalOutstanding,
'overdueCount' => $overdueCount,
'dailyRevenue' => $dailyRevenue,
'topPrograms' => $topPrograms,
]);
}
}
<?php
namespace App\Livewire\Training;
use App\Domain\Training\Models\TrainingSession;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class RescheduleSession extends Component
{
public TrainingSession $session;
public bool $showModal = false;
public string $newDate = '';
public string $newStartTime = '';
public string $newEndTime = '';
public string $reason = '';
public function mount(TrainingSession $session): void
{
$this->session = $session;
$this->newDate = $session->date;
$this->newStartTime = $session->start_time;
$this->newEndTime = $session->end_time;
}
public function open(): void
{
$this->showModal = true;
}
public function reschedule(): void
{
$this->validate([
'newDate' => 'required|date|after_or_equal:today',
'newStartTime' => 'required',
'newEndTime' => 'required|after:newStartTime',
'reason' => 'required|min:3',
], [
'newDate.required' => __('التاريخ مطلوب'),
'newDate.after_or_equal' => __('لا يمكن الجدولة في الماضي'),
'newEndTime.after' => __('وقت الانتهاء يجب أن يكون بعد وقت البدء'),
'reason.required' => __('السبب مطلوب'),
]);
if (!in_array($this->session->status, ['scheduled'])) {
session()->flash('error', __('لا يمكن إعادة جدولة هذه الحصة'));
$this->showModal = false;
return;
}
DB::transaction(function () {
$this->session->update([
'date' => $this->newDate,
'start_time' => $this->newStartTime,
'end_time' => $this->newEndTime,
'status' => 'rescheduled',
'notes' => ($this->session->notes ? $this->session->notes . "\n" : '')
. __('إعادة جدولة') . ": {$this->reason}",
]);
// Create new session with the rescheduled details
TrainingSession::create([
'academy_id' => $this->session->academy_id,
'training_group_id' => $this->session->training_group_id,
'date' => $this->newDate,
'start_time' => $this->newStartTime,
'end_time' => $this->newEndTime,
'status' => 'scheduled',
'notes' => __('حصة معاد جدولتها من') . " {$this->session->date}",
]);
});
$this->showModal = false;
session()->flash('success', __('تم إعادة الجدولة بنجاح'));
$this->dispatch('session-rescheduled');
}
public function render()
{
return view('livewire.training.reschedule-session');
}
}
...@@ -26,6 +26,29 @@ ...@@ -26,6 +26,29 @@
<a href="{{ route('notifications.center') }}" class="relative text-gray-500 hover:text-gray-700"> <a href="{{ route('notifications.center') }}" class="relative text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
</a> </a>
<!-- Language Switcher -->
@livewire('components.language-switcher')
<!-- Dark Mode -->
@livewire('components.dark-mode-toggle')
<!-- User Menu -->
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900">
<span class="hidden sm:inline">{{ auth()->user()->name_ar ?? auth()->user()->name }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</button>
<div x-show="open" @click.outside="open = false" x-transition
class="absolute end-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 z-50 py-1">
<a href="{{ route('profile') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">{{ __('الملف الشخصي') }}</a>
<a href="{{ route('profile.notifications') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">{{ __('تفضيلات الإشعارات') }}</a>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="w-full text-start px-4 py-2 text-sm text-red-600 hover:bg-red-50">{{ __('تسجيل الخروج') }}</button>
</form>
</div>
</div>
</div> </div>
</div> </div>
</header> </header>
This diff is collapsed.
<div>
{{-- Header --}}
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">{{ __('إعدادات النظام') }}</h1>
<p class="mt-1 text-sm text-gray-500">{{ __('إدارة الإعدادات العامة والمالية والحضور والإشعارات والتسجيل') }}</p>
</div>
{{-- Success flash --}}
@if(session('success'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 text-green-700 rounded-lg text-sm">
{{ session('success') }}
</div>
@endif
{{-- Tabs --}}
<div class="mb-6">
<div class="border-b border-gray-200">
<nav class="flex gap-1 -mb-px overflow-x-auto" aria-label="{{ __('أقسام الإعدادات') }}">
@foreach($tabs as $key => $label)
<button
type="button"
wire:click="$set('activeTab', '{{ $key }}')"
class="px-4 py-2.5 text-sm font-medium whitespace-nowrap border-b-2 transition-colors
{{ $activeTab === $key
? 'border-blue-600 text-blue-700'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}"
>
{{ $label }}
</button>
@endforeach
</nav>
</div>
</div>
{{-- Settings Form --}}
<form wire:submit="save">
<div class="bg-white border border-gray-200 rounded-lg p-6 shadow-sm">
@if(empty($schema))
<div class="p-8 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<p class="mt-4 text-sm text-gray-500">{{ __('لا توجد إعدادات في هذا القسم') }}</p>
</div>
@else
<div class="space-y-6">
@foreach($schema as $key => $config)
<div class="max-w-lg">
<label for="setting-{{ $key }}" class="block text-sm font-medium text-gray-700 mb-1.5">
{{ __($config['label']) }}
</label>
@if($config['type'] === 'boolean')
{{-- Toggle switch --}}
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
id="setting-{{ $key }}"
wire:model="settings.{{ $key }}"
value="1"
@checked(filter_var($settings[$key] ?? false, FILTER_VALIDATE_BOOLEAN))
class="sr-only peer"
>
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
<span class="ms-3 text-sm text-gray-600">
{{ filter_var($settings[$key] ?? false, FILTER_VALIDATE_BOOLEAN) ? __('مفعّل') : __('معطّل') }}
</span>
</label>
@elseif($config['type'] === 'select')
{{-- Select dropdown --}}
<select
id="setting-{{ $key }}"
wire:model="settings.{{ $key }}"
class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
>
@foreach($config['options'] as $optValue => $optLabel)
<option value="{{ $optValue }}">{{ $optLabel }}</option>
@endforeach
</select>
@elseif($config['type'] === 'number')
{{-- Number input --}}
<input
type="number"
id="setting-{{ $key }}"
wire:model="settings.{{ $key }}"
dir="ltr"
step="{{ $config['step'] ?? '1' }}"
@if(isset($config['min'])) min="{{ $config['min'] }}" @endif
@if(isset($config['max'])) max="{{ $config['max'] }}" @endif
class="w-full max-w-xs rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm text-start"
>
@elseif($config['type'] === 'time')
{{-- Time input --}}
<input
type="time"
id="setting-{{ $key }}"
wire:model="settings.{{ $key }}"
dir="ltr"
class="w-full max-w-xs rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
>
@else
{{-- Text input --}}
<input
type="text"
id="setting-{{ $key }}"
wire:model="settings.{{ $key }}"
class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm"
>
@endif
</div>
@endforeach
</div>
@endif
</div>
{{-- Save button --}}
@if(!empty($schema))
<div class="mt-6">
<button
type="submit"
wire:loading.attr="disabled"
wire:target="save"
class="inline-flex items-center px-6 py-2.5 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:bg-blue-400 disabled:cursor-not-allowed transition-colors"
>
<span wire:loading.remove wire:target="save">{{ __('حفظ الإعدادات') }}</span>
<span wire:loading wire:target="save" class="inline-flex items-center">
<svg class="animate-spin -ms-1 me-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ __('جارٍ الحفظ...') }}
</span>
</button>
</div>
@endif
</form>
</div>
...@@ -46,6 +46,31 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-70 ...@@ -46,6 +46,31 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-70
</div> </div>
</div> </div>
<!-- Session Notes -->
<div class="mb-6 bg-white border border-gray-200 rounded-lg p-4">
<label for="sessionNotes" class="block text-sm font-medium text-gray-700 mb-2">{{ __('ملاحظات الجلسة') }}</label>
<textarea
id="sessionNotes"
wire:model="sessionNotes"
rows="3"
class="block w-full rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm"
placeholder="{{ __('أضف ملاحظات أو تعليقات حول هذه الجلسة...') }}"
></textarea>
<div class="mt-2 flex items-center gap-3">
<button
wire:click="saveNotes"
wire:loading.attr="disabled"
wire:target="saveNotes"
class="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed">
<span wire:loading.remove wire:target="saveNotes">{{ __('حفظ الملاحظات') }}</span>
<span wire:loading wire:target="saveNotes">{{ __('جارٍ الحفظ...') }}</span>
</button>
@if(session('notes_saved'))
<span class="text-sm text-green-600">{{ session('notes_saved') }}</span>
@endif
</div>
</div>
<!-- Bulk Action --> <!-- Bulk Action -->
@if($summary['expected'] > 0) @if($summary['expected'] > 0)
<div class="mb-4"> <div class="mb-4">
...@@ -67,7 +92,7 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white b ...@@ -67,7 +92,7 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white b
@endif @endif
<!-- Attendance Records --> <!-- Attendance Records -->
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden" wire:loading.class="opacity-50 pointer-events-none" wire:target="markAs, markPresent, markAllPresent"> <div class="bg-white border border-gray-200 rounded-lg overflow-hidden" wire:loading.class="opacity-50 pointer-events-none" wire:target="markAs, markPresent, markAllPresent, saveRecordNote">
@if($records->isEmpty()) @if($records->isEmpty())
<div class="p-12 text-center"> <div class="p-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg> <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
...@@ -82,6 +107,7 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white b ...@@ -82,6 +107,7 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white b
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('النوع') }}</th> <th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('النوع') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('الحالة') }}</th> <th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('وقت الحضور') }}</th> <th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('وقت الحضور') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('ملاحظة') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('الإجراءات') }}</th> <th class="px-4 py-3 text-start text-xs font-medium text-gray-500 uppercase">{{ __('الإجراءات') }}</th>
</tr> </tr>
</thead> </thead>
...@@ -139,6 +165,43 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white b ...@@ -139,6 +165,43 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white b
{{ $record->check_in_at?->format('H:i') ?? '-' }} {{ $record->check_in_at?->format('H:i') ?? '-' }}
</td> </td>
<!-- Per-Record Note -->
<td class="px-4 py-3" x-data="{ expanded: false }">
<div class="flex items-center gap-1">
<input
type="text"
wire:model.blur="recordNotes.{{ $record->id }}"
wire:change="saveRecordNote({{ $record->id }})"
class="w-32 text-xs rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 placeholder-gray-400"
placeholder="{{ __('ملاحظة...') }}"
x-show="!expanded"
>
<textarea
wire:model.blur="recordNotes.{{ $record->id }}"
wire:change="saveRecordNote({{ $record->id }})"
rows="2"
class="w-48 text-xs rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 placeholder-gray-400"
placeholder="{{ __('ملاحظة...') }}"
x-show="expanded"
x-cloak
></textarea>
<button
type="button"
@click="expanded = !expanded"
class="text-gray-400 hover:text-gray-600 shrink-0"
:title="expanded ? '{{ __('تصغير') }}' : '{{ __('توسيع') }}'"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path x-show="!expanded" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"/>
<path x-show="expanded" x-cloak stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 4H5v4M15 4h4v4M9 20H5v-4M15 20h4v-4"/>
</svg>
</button>
</div>
@if(session("record_note_saved_{$record->id}"))
<span class="text-xs text-green-600 mt-0.5 block">{{ __('تم') }}</span>
@endif
</td>
<!-- Actions --> <!-- Actions -->
<td class="px-4 py-3 whitespace-nowrap"> <td class="px-4 py-3 whitespace-nowrap">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
......
<div x-data="{ dark: localStorage.getItem('dark') === 'true' }"
x-init="$watch('dark', val => { localStorage.setItem('dark', val); document.documentElement.classList.toggle('dark', val) }); if (dark) document.documentElement.classList.add('dark')">
<button @click="dark = !dark"
class="p-2 rounded-lg text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
:title="dark ? '{{ __('الوضع الفاتح') }}' : '{{ __('الوضع الداكن') }}'">
<svg x-show="!dark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
</svg>
<svg x-show="dark" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
</button>
</div>
<div class="inline-flex rounded-lg border border-gray-200 overflow-hidden text-xs">
<button wire:click="switchTo('ar')"
class="px-3 py-1.5 {{ $locale === 'ar' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50' }}">
عربي
</button>
<button wire:click="switchTo('en')"
class="px-3 py-1.5 {{ $locale === 'en' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50' }}">
EN
</button>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold text-gray-700">{{ __('اتجاهات التسجيل') }}</h3>
<select wire:model.live="period" class="text-xs rounded border-gray-200 py-1 px-2">
<option value="7">{{ __('7 أيام') }}</option>
<option value="30">{{ __('30 يوم') }}</option>
<option value="90">{{ __('90 يوم') }}</option>
</select>
</div>
<div class="grid grid-cols-3 gap-3 mb-4">
<div class="text-center p-2 bg-green-50 rounded-lg">
<p class="text-lg font-bold text-green-600">{{ $totalNew }}</p>
<p class="text-xs text-gray-500">{{ __('تسجيل جديد') }}</p>
</div>
<div class="text-center p-2 bg-red-50 rounded-lg">
<p class="text-lg font-bold text-red-600">{{ $cancelled }}</p>
<p class="text-xs text-gray-500">{{ __('ملغى') }}</p>
</div>
<div class="text-center p-2 bg-blue-50 rounded-lg">
<p class="text-lg font-bold text-blue-600">{{ $active }}</p>
<p class="text-xs text-gray-500">{{ __('نشط حالياً') }}</p>
</div>
</div>
@if(count($dailyEnrollments) > 0)
@php $max = max($dailyEnrollments) ?: 1; @endphp
<div class="flex items-end gap-1 h-24">
@foreach($dailyEnrollments as $date => $count)
<div class="flex-1 flex flex-col items-center justify-end h-full" title="{{ $date }}: {{ $count }}">
<div class="w-full bg-blue-400 rounded-t" style="height: {{ round($count / $max * 100) }}%; min-height: 2px;"></div>
</div>
@endforeach
</div>
<div class="flex justify-between mt-1">
<span class="text-[10px] text-gray-400" dir="ltr">{{ array_key_first($dailyEnrollments) }}</span>
<span class="text-[10px] text-gray-400" dir="ltr">{{ array_key_last($dailyEnrollments) }}</span>
</div>
@else
<p class="text-center text-sm text-gray-400 py-4">{{ __('لا توجد بيانات') }}</p>
@endif
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-gray-700">{{ __('الإيرادات') }}</h3>
<select wire:model.live="period" class="text-xs rounded border-gray-200 py-1 px-2">
<option value="today">{{ __('اليوم') }}</option>
<option value="week">{{ __('الأسبوع') }}</option>
<option value="month">{{ __('الشهر') }}</option>
<option value="year">{{ __('السنة') }}</option>
</select>
</div>
<div class="flex items-baseline gap-2">
<p class="text-2xl font-bold text-gray-900" dir="ltr">{{ format_money($revenue) }}</p>
@if($change != 0)
<span class="text-xs font-medium {{ $change > 0 ? 'text-green-600' : 'text-red-600' }}">
{{ $change > 0 ? '+' : '' }}{{ $change }}%
</span>
@endif
</div>
@if(count($byMethod) > 0)
<div class="mt-3 pt-3 border-t border-gray-100 space-y-2">
@foreach($byMethod as $method => $total)
<div class="flex items-center justify-between text-xs">
<span class="text-gray-500">{{ __($method) }}</span>
<span class="font-medium text-gray-700" dir="ltr">{{ format_money($total) }}</span>
</div>
@endforeach
</div>
@endif
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<h4 class="text-sm font-semibold text-gray-700 mb-3">{{ __('كود خصم') }}</h4>
<div class="flex gap-2">
<input type="text" wire:model="code" wire:keydown.enter="check"
placeholder="{{ __('أدخل الكود') }}"
class="flex-1 rounded-lg border-gray-300 text-sm uppercase" dir="ltr">
<button wire:click="check" wire:loading.attr="disabled" wire:target="check"
class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50">
<span wire:loading.remove wire:target="check">{{ __('تحقق') }}</span>
<span wire:loading wire:target="check">...</span>
</button>
</div>
@if($result)
<div class="mt-3 p-3 rounded-lg text-sm {{ $result['valid'] ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200' }}">
<p class="{{ $result['valid'] ? 'text-green-700' : 'text-red-700' }} font-medium">
{{ $result['message'] }}
</p>
@if($result['valid'])
<div class="mt-2 text-green-600 text-xs space-y-1">
<p>{{ $result['name'] }}</p>
<p>
@if($result['type'] === 'percentage_discount')
{{ __('خصم') }} {{ $result['value'] }}%
@elseif($result['type'] === 'fixed_discount')
{{ __('خصم') }} {{ format_money($result['value']) }}
@elseif($result['type'] === 'fixed_price')
{{ __('سعر ثابت') }} {{ format_money($result['value']) }}
@endif
</p>
@if($result['remaining'])
<p>{{ __('متبقي') }}: {{ $result['remaining'] }} {{ __('استخدام') }}</p>
@endif
@if($result['expires'])
<p>{{ __('ينتهي') }}: {{ $result['expires'] }}</p>
@endif
</div>
@endif
</div>
@endif
</div>
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-800">{{ __('إنشاء خطة دفع') }}</h1>
</div>
@if(session('success'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Configuration -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">{{ __('إعدادات الخطة') }}</h3>
<div class="space-y-4">
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('الفاتورة') }}</label>
<select wire:model.live="invoiceId" class="w-full rounded-lg border-gray-300 text-sm">
<option value="">{{ __('اختر فاتورة') }}</option>
@foreach($invoices as $inv)
<option value="{{ $inv->id }}">
{{ $inv->invoice_number }} — {{ format_money($inv->balance_due) }}
</option>
@endforeach
</select>
</div>
@if($invoice)
<div class="p-3 bg-blue-50 rounded-lg text-sm">
<p>{{ __('إجمالي الفاتورة') }}: <strong dir="ltr">{{ format_money($invoice->total_amount) }}</strong></p>
<p>{{ __('المتبقي') }}: <strong dir="ltr">{{ format_money($invoice->balance_due) }}</strong></p>
</div>
@endif
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('دفعة مقدمة (ج.م)') }}</label>
<input type="number" wire:model.live.debounce.500ms="downPayment" dir="ltr" min="0"
class="w-full rounded-lg border-gray-300 text-sm">
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('عدد الأقساط') }}</label>
<input type="number" wire:model.live="installmentCount" dir="ltr" min="2" max="24"
class="w-full rounded-lg border-gray-300 text-sm">
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('التكرار') }}</label>
<select wire:model.live="frequency" class="w-full rounded-lg border-gray-300 text-sm">
<option value="weekly">{{ __('أسبوعي') }}</option>
<option value="biweekly">{{ __('كل أسبوعين') }}</option>
<option value="monthly">{{ __('شهري') }}</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('تاريخ أول قسط') }}</label>
<input type="date" wire:model.live="startDate" dir="ltr"
class="w-full rounded-lg border-gray-300 text-sm">
</div>
</div>
</div>
<!-- Preview -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">{{ __('معاينة الأقساط') }}</h3>
@if(count($installments) > 0)
<div class="space-y-2">
@if($downPayment > 0)
<div class="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<span class="text-sm text-gray-700">{{ __('دفعة مقدمة') }}</span>
<span class="text-sm font-medium text-green-700" dir="ltr">{{ format_money($downPayment * 100) }}</span>
</div>
@endif
@foreach($installments as $inst)
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<span class="text-sm text-gray-700">{{ __('قسط') }} {{ $inst['number'] }}</span>
<span class="text-xs text-gray-500 ms-2" dir="ltr">{{ $inst['due_date'] }}</span>
</div>
<span class="text-sm font-medium text-gray-800" dir="ltr">{{ format_money($inst['amount']) }}</span>
</div>
@endforeach
<div class="pt-3 mt-3 border-t border-gray-200 flex items-center justify-between">
<span class="text-sm font-medium text-gray-700">{{ __('الإجمالي') }}</span>
<span class="text-sm font-bold text-gray-900" dir="ltr">
{{ format_money(($downPayment * 100) + collect($installments)->sum('amount')) }}
</span>
</div>
</div>
<button wire:click="save" wire:loading.attr="disabled"
class="w-full mt-6 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 text-sm font-medium">
<span wire:loading.remove wire:target="save">{{ __('إنشاء خطة الدفع') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الإنشاء...') }}</span>
</button>
@else
<p class="text-sm text-gray-500 text-center py-8">{{ __('اختر فاتورة لمعاينة الأقساط') }}</p>
@endif
</div>
</div>
</div>
This diff is collapsed.
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-800">{{ __('تغيير حالة جماعي') }}</h1>
<a href="{{ route('participants.list') }}" wire:navigate class="text-sm text-blue-600 hover:text-blue-800">{{ __('العودة') }}</a>
</div>
@if(session('success'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{{ session('error') }}</div>
@endif
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="flex flex-wrap items-end gap-4">
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('الحالة الحالية') }}</label>
<select wire:model.live="currentFilter" class="rounded-lg border-gray-300 text-sm">
<option value="active">{{ __('active') }}</option>
<option value="frozen">{{ __('frozen') }}</option>
<option value="suspended">{{ __('suspended') }}</option>
<option value="inactive">{{ __('inactive') }}</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('التغيير إلى') }}</label>
<select wire:model="targetStatus" class="rounded-lg border-gray-300 text-sm">
<option value="">{{ __('اختر') }}</option>
@foreach($availableTargets as $target)
<option value="{{ $target }}">{{ __($target) }}</option>
@endforeach
</select>
</div>
<div class="flex gap-2">
<button wire:click="selectAll" class="px-3 py-2 text-xs bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">
{{ __('تحديد الكل') }}
</button>
<button wire:click="deselectAll" class="px-3 py-2 text-xs bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">
{{ __('إلغاء التحديد') }}
</button>
</div>
<button wire:click="openConfirm" @disabled(empty($selectedIds) || empty($targetStatus))
class="px-4 py-2 text-sm bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50">
{{ __('تنفيذ') }} ({{ count($selectedIds) }})
</button>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500 w-10"></th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('الاسم') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('تاريخ التسجيل') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($participants as $p)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3">
<input type="checkbox" wire:model="selectedIds" value="{{ $p->id }}"
class="w-4 h-4 text-blue-600 rounded border-gray-300">
</td>
<td class="px-4 py-3 text-sm text-gray-800">{{ $p->full_name_ar }}</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-600">{{ __($p->status) }}</span>
</td>
<td class="px-4 py-3 text-sm text-gray-500" dir="ltr">{{ $p->created_at?->format('Y-m-d') }}</td>
</tr>
@empty
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-500">{{ __('لا يوجد مشتركين') }}</td></tr>
@endforelse
</tbody>
</table>
</div>
@if($confirmModal)
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6">
<h3 class="text-lg font-bold text-gray-800 mb-2">{{ __('تأكيد التغيير') }}</h3>
<p class="text-sm text-gray-600 mb-4">
{{ __('سيتم تغيير حالة :count مشترك من :from إلى :to', [
'count' => count($selectedIds),
'from' => __($currentFilter),
'to' => __($targetStatus),
]) }}
</p>
<div class="mb-4">
<label class="block text-sm text-gray-600 mb-1">{{ __('السبب') }} *</label>
<textarea wire:model="reason" rows="2" class="w-full rounded-lg border-gray-300 text-sm"></textarea>
@error('reason') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
<div class="flex gap-2 justify-end">
<button wire:click="$set('confirmModal', false)" class="px-4 py-2 text-sm text-gray-600">{{ __('إلغاء') }}</button>
<button wire:click="execute" class="px-4 py-2 text-sm text-white bg-red-600 rounded-lg hover:bg-red-700">{{ __('تأكيد') }}</button>
</div>
</div>
</div>
@endif
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-800">{{ __('سجل التسجيل') }} ({{ $totalCount }})</h3>
</div>
@if($enrollments->isEmpty())
<div class="p-8 text-center text-sm text-gray-500">{{ __('لا يوجد تسجيلات') }}</div>
@else
<div class="divide-y divide-gray-100">
@foreach($enrollments as $enrollment)
<div class="p-4 hover:bg-gray-50">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-800">
{{ $enrollment->trainingGroup?->program?->name_ar ?? '-' }}
</p>
<p class="text-xs text-gray-500 mt-1">
{{ $enrollment->trainingGroup?->name_ar ?? '-' }}
&bull; {{ $enrollment->created_at->format('Y-m-d') }}
</p>
</div>
<span class="px-2 py-1 text-xs rounded-full font-medium
@switch($enrollment->status)
@case('active') bg-green-100 text-green-700 @break
@case('completed') bg-blue-100 text-blue-700 @break
@case('cancelled') bg-red-100 text-red-700 @break
@case('expired') bg-gray-100 text-gray-600 @break
@case('pending') bg-yellow-100 text-yellow-700 @break
@case('waitlisted') bg-purple-100 text-purple-700 @break
@default bg-gray-100 text-gray-600
@endswitch
">{{ __($enrollment->status) }}</span>
</div>
</div>
@endforeach
</div>
@if($totalCount > 5)
<div class="p-3 border-t border-gray-200 text-center">
<button wire:click="toggleShowAll" class="text-sm text-blue-600 hover:text-blue-800">
{{ $showAll ? __('عرض أقل') : __('عرض الكل') . " ({$totalCount})" }}
</button>
</div>
@endif
@endif
</div>
<div>
@if($participant->status === 'active')
<button wire:click="openFreeze" class="px-3 py-1.5 text-xs bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200">
{{ __('تجميد') }}
</button>
@elseif($participant->status === 'frozen')
<button wire:click="openUnfreeze" class="px-3 py-1.5 text-xs bg-green-100 text-green-700 rounded-lg hover:bg-green-200">
{{ __('إلغاء التجميد') }}
</button>
@endif
@if($showModal)
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" wire:click.self="$set('showModal', false)">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6">
<h3 class="text-lg font-bold text-gray-800 mb-4">
{{ $action === 'freeze' ? __('تجميد المشترك') : __('إلغاء تجميد المشترك') }}
</h3>
<p class="text-sm text-gray-600 mb-4">
{{ $action === 'freeze'
? __('سيتم تعليق جميع الأنشطة للمشترك. يمكن إلغاء التجميد لاحقاً.')
: __('سيتم إعادة تنشيط المشترك واستئناف أنشطته.')
}}
</p>
<div class="mb-4">
<label class="block text-sm text-gray-600 mb-1">{{ __('السبب') }} *</label>
<textarea wire:model="reason" rows="3" class="w-full rounded-lg border-gray-300 text-sm"
placeholder="{{ __('أدخل سبب الإجراء') }}"></textarea>
@error('reason') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
<div class="flex gap-2 justify-end">
<button wire:click="$set('showModal', false)" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800">
{{ __('إلغاء') }}
</button>
<button wire:click="confirm" wire:loading.attr="disabled"
class="px-4 py-2 text-sm text-white rounded-lg {{ $action === 'freeze' ? 'bg-blue-600 hover:bg-blue-700' : 'bg-green-600 hover:bg-green-700' }} disabled:opacity-50">
<span wire:loading.remove wire:target="confirm">{{ __('تأكيد') }}</span>
<span wire:loading wire:target="confirm">{{ __('جارٍ...') }}</span>
</button>
</div>
</div>
</div>
@endif
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<h3 class="text-sm font-semibold text-gray-700 mb-4">{{ __('سجل الحالات') }}</h3>
@if($history->isEmpty())
<p class="text-sm text-gray-400 text-center py-4">{{ __('لا يوجد تغييرات') }}</p>
@else
<div class="relative">
<div class="absolute top-0 bottom-0 start-3 w-0.5 bg-gray-200"></div>
<div class="space-y-4">
@foreach($history as $item)
<div class="flex items-start gap-3 relative">
<div class="w-6 h-6 rounded-full border-2 border-white shadow-sm z-10 shrink-0
@switch($item['to'])
@case('active') bg-green-500 @break
@case('frozen') bg-blue-500 @break
@case('suspended') bg-red-500 @break
@case('graduated') bg-purple-500 @break
@case('withdrawn') bg-gray-500 @break
@case('inactive') bg-gray-400 @break
@default bg-gray-300
@endswitch
"></div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
@if($item['from'])
<span class="text-xs text-gray-500">{{ __($item['from']) }}</span>
<svg class="w-3 h-3 text-gray-400 rtl:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
@endif
<span class="text-xs font-medium text-gray-800">{{ __($item['to']) }}</span>
</div>
<p class="text-[11px] text-gray-400 mt-0.5">
{{ $item['user'] }} &bull; {{ \Carbon\Carbon::parse($item['date'])->diffForHumans() }}
</p>
</div>
</div>
@endforeach
</div>
</div>
@endif
</div>
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-800">{{ __('تفضيلات الإشعارات') }}</h1>
<a href="{{ route('profile') }}" wire:navigate class="text-sm text-blue-600 hover:text-blue-800">{{ __('العودة للملف الشخصي') }}</a>
</div>
@if(session('success'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">{{ session('success') }}</div>
@endif
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="p-4 bg-gray-50 border-b">
<div class="grid grid-cols-4 gap-4 text-sm font-medium text-gray-600">
<div class="col-span-1">{{ __('نوع الإشعار') }}</div>
<div class="text-center">{{ __('بريد') }}</div>
<div class="text-center">{{ __('SMS') }}</div>
<div class="text-center">{{ __('داخلي') }}</div>
</div>
</div>
<div class="divide-y">
@foreach($eventTypes as $type => $label)
<div class="grid grid-cols-4 gap-4 p-4 items-center hover:bg-gray-50">
<div class="col-span-1 text-sm text-gray-800">{{ $label }}</div>
<div class="text-center">
<input type="checkbox" wire:model="preferences.{{ $type }}.email"
class="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500">
</div>
<div class="text-center">
<input type="checkbox" wire:model="preferences.{{ $type }}.sms"
class="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500">
</div>
<div class="text-center">
<input type="checkbox" wire:model="preferences.{{ $type }}.in_app"
class="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500" disabled>
</div>
</div>
@endforeach
</div>
<div class="p-4 bg-gray-50 border-t">
<p class="text-xs text-gray-500 mb-3">{{ __('الإشعارات الداخلية لا يمكن تعطيلها') }}</p>
<button wire:click="save" wire:loading.attr="disabled"
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 text-sm font-medium">
<span wire:loading.remove wire:target="save">{{ __('حفظ التفضيلات') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
</div>
</div>
</div>
This diff is collapsed.
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-800">{{ __('التقرير المالي') }}</h1>
<a href="{{ route('reports.view') }}" wire:navigate class="text-sm text-blue-600 hover:text-blue-800">{{ __('العودة للتقارير') }}</a>
</div>
<!-- Filters -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-6">
<div class="flex flex-wrap items-end gap-4">
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('الفترة') }}</label>
<select wire:model.live="period" class="rounded-lg border-gray-300 text-sm">
<option value="today">{{ __('اليوم') }}</option>
<option value="week">{{ __('هذا الأسبوع') }}</option>
<option value="month">{{ __('هذا الشهر') }}</option>
<option value="quarter">{{ __('هذا الربع') }}</option>
<option value="year">{{ __('هذه السنة') }}</option>
<option value="custom">{{ __('مخصص') }}</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('من') }}</label>
<input type="date" wire:model.live="dateFrom" dir="ltr" class="rounded-lg border-gray-300 text-sm">
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('إلى') }}</label>
<input type="date" wire:model.live="dateTo" dir="ltr" class="rounded-lg border-gray-300 text-sm">
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<p class="text-sm text-gray-500">{{ __('إجمالي الإيرادات') }}</p>
<p class="text-2xl font-bold text-green-600 mt-1" dir="ltr">{{ format_money($totalRevenue) }}</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<p class="text-sm text-gray-500">{{ __('إجمالي الفواتير') }}</p>
<p class="text-2xl font-bold text-blue-600 mt-1" dir="ltr">{{ format_money($totalInvoiced) }}</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<p class="text-sm text-gray-500">{{ __('المستحقات المعلقة') }}</p>
<p class="text-2xl font-bold text-orange-600 mt-1" dir="ltr">{{ format_money($totalOutstanding) }}</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<p class="text-sm text-gray-500">{{ __('فواتير متأخرة') }}</p>
<p class="text-2xl font-bold text-red-600 mt-1">{{ $overdueCount }}</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Payment Methods Breakdown -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<h3 class="text-lg font-semibold text-gray-800 mb-4">{{ __('طرق الدفع') }}</h3>
@forelse($paymentsByMethod as $method => $amount)
<div class="flex items-center justify-between py-2 border-b border-gray-100 last:border-0">
<span class="text-sm text-gray-700">{{ __($method) }}</span>
<div class="flex items-center gap-3">
<div class="w-32 bg-gray-100 rounded-full h-2">
<div class="bg-blue-500 h-2 rounded-full" style="width: {{ $totalRevenue > 0 ? round($amount / $totalRevenue * 100) : 0 }}%"></div>
</div>
<span class="text-sm font-medium text-gray-800" dir="ltr">{{ format_money($amount) }}</span>
</div>
</div>
@empty
<p class="text-sm text-gray-500 text-center py-4">{{ __('لا توجد مدفوعات') }}</p>
@endforelse
</div>
<!-- Daily Revenue Chart (simple bars) -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
<h3 class="text-lg font-semibold text-gray-800 mb-4">{{ __('الإيرادات اليومية') }}</h3>
@if(count($dailyRevenue) > 0)
@php $maxDaily = max($dailyRevenue) ?: 1; @endphp
<div class="space-y-2 max-h-64 overflow-y-auto">
@foreach($dailyRevenue as $date => $amount)
<div class="flex items-center gap-2">
<span class="text-xs text-gray-500 w-20 shrink-0" dir="ltr">{{ \Carbon\Carbon::parse($date)->format('m/d') }}</span>
<div class="flex-1 bg-gray-100 rounded-full h-4">
<div class="bg-green-500 h-4 rounded-full flex items-center justify-end pe-1" style="width: {{ round($amount / $maxDaily * 100) }}%">
@if($amount / $maxDaily > 0.3)
<span class="text-[10px] text-white font-medium" dir="ltr">{{ format_money($amount) }}</span>
@endif
</div>
</div>
@if($amount / $maxDaily <= 0.3)
<span class="text-xs text-gray-600" dir="ltr">{{ format_money($amount) }}</span>
@endif
</div>
@endforeach
</div>
@else
<p class="text-sm text-gray-500 text-center py-4">{{ __('لا توجد بيانات') }}</p>
@endif
</div>
</div>
<!-- Top Programs by Revenue -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="p-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800">{{ __('أعلى البرامج إيراداً') }}</h3>
</div>
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">#</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('البرنامج') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('عدد المبيعات') }}</th>
<th class="px-4 py-3 text-start text-xs font-medium text-gray-500">{{ __('الإيرادات') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($topPrograms as $i => $program)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm text-gray-500">{{ $i + 1 }}</td>
<td class="px-4 py-3 text-sm text-gray-800">{{ $program->description }}</td>
<td class="px-4 py-3 text-sm text-gray-600">{{ $program->count }}</td>
<td class="px-4 py-3 text-sm font-medium text-green-600" dir="ltr">{{ format_money($program->revenue) }}</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-4 py-8 text-center text-sm text-gray-500">{{ __('لا توجد بيانات') }}</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div>
@if($session->status === 'scheduled')
<button wire:click="open" class="px-3 py-1.5 text-xs bg-yellow-100 text-yellow-700 rounded-lg hover:bg-yellow-200">
{{ __('إعادة جدولة') }}
</button>
@endif
@if($showModal)
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" wire:click.self="$set('showModal', false)">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6">
<h3 class="text-lg font-bold text-gray-800 mb-4">{{ __('إعادة جدولة الحصة') }}</h3>
<div class="space-y-4">
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('التاريخ الجديد') }}</label>
<input type="date" wire:model="newDate" dir="ltr" class="w-full rounded-lg border-gray-300 text-sm">
@error('newDate') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('من') }}</label>
<input type="time" wire:model="newStartTime" dir="ltr" class="w-full rounded-lg border-gray-300 text-sm">
@error('newStartTime') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('إلى') }}</label>
<input type="time" wire:model="newEndTime" dir="ltr" class="w-full rounded-lg border-gray-300 text-sm">
@error('newEndTime') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">{{ __('السبب') }} *</label>
<textarea wire:model="reason" rows="2" class="w-full rounded-lg border-gray-300 text-sm"
placeholder="{{ __('سبب إعادة الجدولة') }}"></textarea>
@error('reason') <p class="text-xs text-red-500 mt-1">{{ $message }}</p> @enderror
</div>
</div>
<div class="flex gap-2 justify-end mt-6">
<button wire:click="$set('showModal', false)" class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800">
{{ __('إلغاء') }}
</button>
<button wire:click="reschedule" wire:loading.attr="disabled"
class="px-4 py-2 text-sm text-white bg-yellow-600 rounded-lg hover:bg-yellow-700 disabled:opacity-50">
<span wire:loading.remove wire:target="reschedule">{{ __('تأكيد') }}</span>
<span wire:loading wire:target="reschedule">{{ __('جارٍ...') }}</span>
</button>
</div>
</div>
</div>
@endif
</div>
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<title>{{ __('شهادة حضور') }}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Cairo', 'Noto Sans Arabic', sans-serif; direction: rtl; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f0f0f0; padding: 20px; }
.certificate { width: 800px; min-height: 560px; background: white; border: 8px solid #1e40af; border-radius: 4px; padding: 50px; text-align: center; position: relative; }
.certificate::before { content: ''; position: absolute; inset: 8px; border: 2px solid #93c5fd; border-radius: 2px; pointer-events: none; }
.logo { font-size: 16px; color: #64748b; margin-bottom: 10px; }
h1 { font-size: 32px; color: #1e40af; margin-bottom: 5px; }
.subtitle { font-size: 14px; color: #64748b; margin-bottom: 30px; }
.recipient { font-size: 24px; font-weight: bold; color: #1e293b; margin-bottom: 10px; }
.details { font-size: 14px; color: #475569; line-height: 2; margin-bottom: 25px; }
.stats { display: flex; justify-content: center; gap: 40px; margin-bottom: 30px; }
.stat { text-align: center; }
.stat .value { font-size: 28px; font-weight: bold; color: #1e40af; }
.stat .label { font-size: 12px; color: #64748b; }
.footer { display: flex; justify-content: space-between; align-items: flex-end; margin-top: 30px; padding-top: 20px; }
.signature { text-align: center; }
.signature .line { width: 150px; border-bottom: 1px solid #333; margin-bottom: 5px; }
.signature .label { font-size: 11px; color: #64748b; }
.date { font-size: 12px; color: #64748b; }
.no-print { position: fixed; top: 20px; right: 20px; }
@media print { body { background: white; padding: 0; } .no-print { display: none; } .certificate { border-width: 4px; } }
</style>
</head>
<body>
<div class="no-print">
<button onclick="window.print()" style="padding: 8px 20px; background: #1e40af; color: white; border: none; border-radius: 6px; cursor: pointer; font-family: inherit;">{{ __('طباعة الشهادة') }}</button>
</div>
<div class="certificate">
<p class="logo">{{ app()->bound('current_academy') ? app('current_academy')->name_ar : 'الكابتن' }}</p>
<h1>{{ __('شهادة حضور') }}</h1>
<p class="subtitle">{{ __('تشهد الأكاديمية بأن') }}</p>
<p class="recipient">{{ $participant->full_name_ar }}</p>
<div class="details">
<p>{{ __('قد أتم بنجاح حضور برنامج') }}</p>
<p><strong>{{ $enrollment->trainingGroup?->program?->name_ar ?? '-' }}</strong></p>
<p>{{ __('المجموعة') }}: {{ $enrollment->trainingGroup?->name_ar ?? '-' }}</p>
</div>
<div class="stats">
<div class="stat">
<p class="value">{{ $stats->total ?? 0 }}</p>
<p class="label">{{ __('إجمالي الحصص') }}</p>
</div>
<div class="stat">
<p class="value">{{ $stats->attended ?? 0 }}</p>
<p class="label">{{ __('حصص الحضور') }}</p>
</div>
<div class="stat">
<p class="value">{{ $rate }}%</p>
<p class="label">{{ __('نسبة الحضور') }}</p>
</div>
</div>
<div class="footer">
<div class="signature">
<div class="line"></div>
<p class="label">{{ __('التوقيع') }}</p>
</div>
<div class="date">
{{ __('تاريخ الإصدار') }}: {{ now()->format('Y-m-d') }}
</div>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<title>{{ __('جدول المجموعة') }} - {{ $group->name_ar }}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Cairo', 'Noto Sans Arabic', sans-serif; direction: rtl; padding: 20px; font-size: 14px; }
.header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #333; padding-bottom: 15px; }
.header h1 { font-size: 22px; margin-bottom: 5px; }
.header p { color: #666; font-size: 13px; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px; }
.info-item { padding: 8px; background: #f9f9f9; border-radius: 4px; }
.info-item label { font-weight: bold; font-size: 12px; color: #555; }
.info-item span { display: block; margin-top: 2px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: right; font-size: 13px; }
th { background: #f3f3f3; font-weight: bold; }
.section-title { font-size: 16px; font-weight: bold; margin: 20px 0 10px; padding-bottom: 5px; border-bottom: 1px solid #eee; }
.footer { margin-top: 30px; text-align: center; font-size: 11px; color: #999; }
@media print { body { padding: 0; } .no-print { display: none; } }
</style>
</head>
<body>
<div class="no-print" style="margin-bottom: 15px;">
<button onclick="window.print()" style="padding: 8px 20px; background: #2563eb; color: white; border: none; border-radius: 6px; cursor: pointer;">{{ __('طباعة') }}</button>
</div>
<div class="header">
<h1>{{ $group->name_ar }}</h1>
<p>{{ $group->program?->name_ar ?? '' }} — {{ __('الحالة') }}: {{ __($group->status) }}</p>
</div>
<div class="info-grid">
<div class="info-item">
<label>{{ __('السعة') }}</label>
<span>{{ $group->current_count ?? 0 }} / {{ $group->max_capacity }}</span>
</div>
<div class="info-item">
<label>{{ __('الفترة') }}</label>
<span>{{ $group->start_date ?? '-' }} — {{ $group->end_date ?? '-' }}</span>
</div>
</div>
<p class="section-title">{{ __('المواعيد الأسبوعية') }}</p>
@if($group->schedules && $group->schedules->count())
<table>
<thead>
<tr>
<th>{{ __('اليوم') }}</th>
<th>{{ __('من') }}</th>
<th>{{ __('إلى') }}</th>
</tr>
</thead>
<tbody>
@php
$days = ['saturday' => 'السبت', 'sunday' => 'الأحد', 'monday' => 'الاثنين', 'tuesday' => 'الثلاثاء', 'wednesday' => 'الأربعاء', 'thursday' => 'الخميس', 'friday' => 'الجمعة'];
@endphp
@foreach($group->schedules as $schedule)
<tr>
<td>{{ $days[$schedule->day_of_week] ?? $schedule->day_of_week }}</td>
<td dir="ltr">{{ $schedule->start_time }}</td>
<td dir="ltr">{{ $schedule->end_time }}</td>
</tr>
@endforeach
</tbody>
</table>
@else
<p style="color: #999; padding: 10px;">{{ __('لا توجد مواعيد') }}</p>
@endif
<p class="section-title">{{ __('قائمة المشتركين') }} ({{ $group->enrollments->count() }})</p>
@if($group->enrollments->count())
<table>
<thead>
<tr>
<th>#</th>
<th>{{ __('الاسم') }}</th>
<th>{{ __('تاريخ التسجيل') }}</th>
</tr>
</thead>
<tbody>
@foreach($group->enrollments as $i => $enrollment)
<tr>
<td>{{ $i + 1 }}</td>
<td>{{ $enrollment->participant?->full_name_ar ?? '-' }}</td>
<td dir="ltr">{{ $enrollment->created_at?->format('Y-m-d') }}</td>
</tr>
@endforeach
</tbody>
</table>
@else
<p style="color: #999; padding: 10px;">{{ __('لا يوجد مشتركين') }}</p>
@endif
<div class="footer">
{{ __('تم الطباعة بتاريخ') }}: {{ now()->format('Y-m-d H:i') }}
</div>
</body>
</html>
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<title>{{ __('فاتورة') }} - {{ $invoice->invoice_number }}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Cairo', 'Noto Sans Arabic', sans-serif; direction: rtl; padding: 30px; font-size: 13px; color: #333; }
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #1e40af; }
.header h1 { font-size: 24px; color: #1e40af; }
.header .meta { text-align: left; font-size: 12px; color: #666; }
.header .meta p { margin-bottom: 3px; }
.parties { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-bottom: 25px; }
.party { padding: 15px; background: #f8fafc; border-radius: 8px; }
.party h3 { font-size: 12px; color: #64748b; margin-bottom: 8px; text-transform: uppercase; }
.party p { margin-bottom: 3px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
th { background: #1e40af; color: white; padding: 10px 12px; text-align: right; font-size: 12px; }
td { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; }
.totals { margin-right: auto; width: 280px; }
.totals .row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; }
.totals .row.total { font-weight: bold; font-size: 16px; border-top: 2px solid #333; padding-top: 10px; margin-top: 5px; }
.status-badge { display: inline-block; padding: 3px 12px; border-radius: 12px; font-size: 11px; font-weight: bold; }
.status-paid { background: #dcfce7; color: #166534; }
.status-sent { background: #dbeafe; color: #1e40af; }
.status-overdue { background: #fef2f2; color: #991b1b; }
.status-partially_paid { background: #fef9c3; color: #854d0e; }
.payments { margin-top: 20px; }
.payments h3 { font-size: 14px; margin-bottom: 10px; }
.footer { margin-top: 40px; text-align: center; font-size: 11px; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 15px; }
.no-print { margin-bottom: 15px; }
@media print { .no-print { display: none; } body { padding: 15px; } }
</style>
</head>
<body>
<div class="no-print">
<button onclick="window.print()" style="padding: 8px 20px; background: #1e40af; color: white; border: none; border-radius: 6px; cursor: pointer;">{{ __('طباعة') }}</button>
</div>
<div class="header">
<div>
<h1>{{ __('فاتورة') }}</h1>
<p style="color: #64748b; margin-top: 4px;">{{ app()->bound('current_academy') ? app('current_academy')->name_ar : 'الكابتن' }}</p>
</div>
<div class="meta">
<p><strong>{{ __('رقم الفاتورة') }}:</strong> {{ $invoice->invoice_number }}</p>
<p><strong>{{ __('التاريخ') }}:</strong> {{ $invoice->issue_date ?? $invoice->created_at?->format('Y-m-d') }}</p>
@if($invoice->due_date)
<p><strong>{{ __('تاريخ الاستحقاق') }}:</strong> {{ $invoice->due_date }}</p>
@endif
<p style="margin-top: 5px;">
<span class="status-badge status-{{ $invoice->status }}">{{ __($invoice->status) }}</span>
</p>
</div>
</div>
<div class="parties">
<div class="party">
<h3>{{ __('العميل') }}</h3>
@if($invoice->billable)
<p><strong>{{ $invoice->billable->full_name_ar ?? $invoice->billable->name_ar ?? '-' }}</strong></p>
@if($invoice->billable->phone ?? null)
<p dir="ltr">{{ $invoice->billable->phone }}</p>
@endif
@endif
</div>
<div class="party">
<h3>{{ __('تفاصيل') }}</h3>
@if($invoice->notes)
<p>{{ $invoice->notes }}</p>
@endif
</div>
</div>
<table>
<thead>
<tr>
<th>#</th>
<th>{{ __('الوصف') }}</th>
<th>{{ __('الكمية') }}</th>
<th>{{ __('سعر الوحدة') }}</th>
<th>{{ __('الإجمالي') }}</th>
</tr>
</thead>
<tbody>
@forelse($invoice->items as $i => $item)
<tr>
<td>{{ $i + 1 }}</td>
<td>{{ $item->description }}</td>
<td>{{ $item->quantity }}</td>
<td dir="ltr">{{ format_money($item->unit_price) }}</td>
<td dir="ltr">{{ format_money($item->line_total) }}</td>
</tr>
@empty
<tr><td colspan="5" style="text-align: center; color: #999;">{{ __('لا توجد بنود') }}</td></tr>
@endforelse
</tbody>
</table>
<div class="totals">
<div class="row">
<span>{{ __('المجموع الفرعي') }}</span>
<span dir="ltr">{{ format_money($invoice->subtotal ?? $invoice->total_amount) }}</span>
</div>
@if($invoice->discount_amount > 0)
<div class="row">
<span>{{ __('الخصم') }}</span>
<span dir="ltr" style="color: #dc2626;">-{{ format_money($invoice->discount_amount) }}</span>
</div>
@endif
@if($invoice->tax_amount > 0)
<div class="row">
<span>{{ __('الضريبة') }}</span>
<span dir="ltr">{{ format_money($invoice->tax_amount) }}</span>
</div>
@endif
<div class="row total">
<span>{{ __('الإجمالي') }}</span>
<span dir="ltr">{{ format_money($invoice->total_amount) }}</span>
</div>
@if($invoice->paid_amount > 0)
<div class="row" style="color: #166534;">
<span>{{ __('المدفوع') }}</span>
<span dir="ltr">{{ format_money($invoice->paid_amount) }}</span>
</div>
<div class="row" style="font-weight: bold; color: #dc2626;">
<span>{{ __('المتبقي') }}</span>
<span dir="ltr">{{ format_money($invoice->balance_due) }}</span>
</div>
@endif
</div>
@if($invoice->payments && $invoice->payments->count())
<div class="payments">
<h3>{{ __('المدفوعات') }}</h3>
<table>
<thead>
<tr>
<th>{{ __('التاريخ') }}</th>
<th>{{ __('الطريقة') }}</th>
<th>{{ __('المبلغ') }}</th>
<th>{{ __('الحالة') }}</th>
</tr>
</thead>
<tbody>
@foreach($invoice->payments as $payment)
<tr>
<td dir="ltr">{{ $payment->created_at?->format('Y-m-d') }}</td>
<td>{{ __($payment->payment_method) }}</td>
<td dir="ltr">{{ format_money($payment->amount) }}</td>
<td>{{ __($payment->status) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
<div class="footer">
{{ __('تم إنشاء هذه الفاتورة إلكترونياً ولا تحتاج إلى توقيع') }}
</div>
</body>
</html>
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<title>{{ __('بطاقة عضوية') }} - {{ $participant->full_name_ar }}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Cairo', 'Noto Sans Arabic', sans-serif; direction: rtl; padding: 40px; background: #f5f5f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.card { width: 340px; background: white; border-radius: 16px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
.card-header { background: linear-gradient(135deg, #1e40af, #3b82f6); padding: 20px; text-align: center; color: white; }
.card-header h2 { font-size: 14px; opacity: 0.9; margin-bottom: 4px; }
.card-header h1 { font-size: 12px; font-weight: normal; }
.avatar { width: 80px; height: 80px; border-radius: 50%; background: white; margin: -40px auto 0; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: bold; color: #1e40af; border: 4px solid white; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
.card-body { padding: 50px 20px 20px; text-align: center; }
.name { font-size: 18px; font-weight: bold; color: #1e293b; margin-bottom: 4px; }
.id-badge { display: inline-block; background: #f1f5f9; border-radius: 20px; padding: 4px 12px; font-size: 11px; color: #64748b; margin-bottom: 12px; }
.info-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f1f5f9; font-size: 12px; }
.info-row:last-child { border-bottom: none; }
.info-row label { color: #64748b; }
.info-row span { font-weight: 600; color: #1e293b; }
.programs { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e2e8f0; }
.programs h4 { font-size: 12px; color: #64748b; margin-bottom: 8px; }
.program-tag { display: inline-block; background: #eff6ff; color: #1d4ed8; border-radius: 12px; padding: 3px 10px; font-size: 11px; margin: 2px; }
.card-footer { background: #f8fafc; padding: 12px; text-align: center; font-size: 10px; color: #94a3b8; }
.no-print { margin-bottom: 20px; text-align: center; }
@media print { body { background: white; padding: 0; } .no-print { display: none; } .card { box-shadow: none; } }
</style>
</head>
<body>
<div>
<div class="no-print">
<button onclick="window.print()" style="padding: 8px 20px; background: #2563eb; color: white; border: none; border-radius: 6px; cursor: pointer;">{{ __('طباعة البطاقة') }}</button>
</div>
<div class="card">
<div class="card-header">
<h2>{{ app()->bound('current_academy') ? app('current_academy')->name_ar : 'الكابتن' }}</h2>
<h1>{{ __('بطاقة عضوية') }}</h1>
</div>
<div class="avatar">
{{ mb_substr($participant->full_name_ar ?? $participant->full_name ?? '?', 0, 1) }}
</div>
<div class="card-body">
<p class="name">{{ $participant->full_name_ar }}</p>
<span class="id-badge">{{ $participant->uuid ?? "#{$participant->id}" }}</span>
<div style="text-align: right; margin-top: 12px;">
<div class="info-row">
<label>{{ __('الحالة') }}</label>
<span>{{ __($participant->status) }}</span>
</div>
@if($participant->date_of_birth)
<div class="info-row">
<label>{{ __('تاريخ الميلاد') }}</label>
<span dir="ltr">{{ $participant->date_of_birth }}</span>
</div>
@endif
@if($participant->gender)
<div class="info-row">
<label>{{ __('النوع') }}</label>
<span>{{ $participant->gender === 'male' ? __('ذكر') : __('أنثى') }}</span>
</div>
@endif
</div>
@if($participant->enrollments->count())
<div class="programs">
<h4>{{ __('البرامج النشطة') }}</h4>
@foreach($participant->enrollments as $enrollment)
<span class="program-tag">{{ $enrollment->trainingGroup?->program?->name_ar ?? $enrollment->trainingGroup?->name_ar ?? '-' }}</span>
@endforeach
</div>
@endif
</div>
<div class="card-footer">
{{ __('تاريخ الإصدار') }}: {{ now()->format('Y-m-d') }}
</div>
</div>
</div>
</body>
</html>
<?php
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
Route::get('/stats', \App\Http\Controllers\Api\QuickStatsController::class)->name('api.stats');
});
...@@ -18,3 +18,6 @@ ...@@ -18,3 +18,6 @@
Schedule::command('reminders:overdue-invoices')->weeklyOn(4, '09:00'); Schedule::command('reminders:overdue-invoices')->weeklyOn(4, '09:00');
Schedule::command('groups:reconcile-counts')->dailyAt('03:00'); Schedule::command('groups:reconcile-counts')->dailyAt('03:00');
Schedule::command('enrollments:deactivate-expired')->dailyAt('00:30'); Schedule::command('enrollments:deactivate-expired')->dailyAt('00:30');
Schedule::command('financials:reconcile')->weeklyOn(0, '04:00');
Schedule::command('reports:parent-weekly')->weeklyOn(6, '12:00');
Schedule::command('groups:alert-capacity --threshold=90')->dailyAt('08:00');
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
use App\Livewire\Receptionist\EnrollExistingWizard; use App\Livewire\Receptionist\EnrollExistingWizard;
use App\Livewire\Receptionist\NewRegistrationWizard; use App\Livewire\Receptionist\NewRegistrationWizard;
use App\Livewire\Receptionist\ReceptionistDashboard; use App\Livewire\Receptionist\ReceptionistDashboard;
use App\Livewire\Guardian\GuardianDashboard;
use App\Livewire\Trainer\TrainerDashboard; use App\Livewire\Trainer\TrainerDashboard;
use App\Livewire\Wizards\SetupWizard; use App\Livewire\Wizards\SetupWizard;
use App\Livewire\Assignments\AssignmentForm; use App\Livewire\Assignments\AssignmentForm;
...@@ -99,6 +100,7 @@ ...@@ -99,6 +100,7 @@
// Profile // Profile
Route::get('/profile', \App\Livewire\Profile\UserProfile::class)->name('profile'); Route::get('/profile', \App\Livewire\Profile\UserProfile::class)->name('profile');
Route::get('/profile/notifications', \App\Livewire\Profile\NotificationPreferences::class)->name('profile.notifications');
// Logout // Logout
Route::post('/logout', function () { Route::post('/logout', function () {
...@@ -314,6 +316,10 @@ ...@@ -314,6 +316,10 @@
// Reports // Reports
Route::get('/reports', \App\Livewire\Reports\ReportsPage::class)->name('reports.view') Route::get('/reports', \App\Livewire\Reports\ReportsPage::class)->name('reports.view')
->middleware('permission:reports.view'); ->middleware('permission:reports.view');
Route::get('/reports/financial', \App\Livewire\Reports\FinancialReport::class)->name('reports.financial')
->middleware('permission:reports.view');
Route::get('/reports/attendance', \App\Livewire\Reports\AttendanceReport::class)->name('reports.attendance')
->middleware('permission:reports.attendance');
Route::get('/reports/daily-print', [\App\Http\Controllers\ReportPrintController::class, 'dailyFinancial']) Route::get('/reports/daily-print', [\App\Http\Controllers\ReportPrintController::class, 'dailyFinancial'])
->name('reports.daily-print')->middleware('permission:reports.view'); ->name('reports.daily-print')->middleware('permission:reports.view');
...@@ -376,4 +382,34 @@ ...@@ -376,4 +382,34 @@
->middleware('permission:enrollments.create'); ->middleware('permission:enrollments.create');
Route::get('/receptionist/collect-payment', CollectPaymentWizard::class)->name('receptionist.collect-payment') Route::get('/receptionist/collect-payment', CollectPaymentWizard::class)->name('receptionist.collect-payment')
->middleware('permission:invoices.create'); ->middleware('permission:invoices.create');
// Financial — Payment Plans
Route::get('/payment-plans/create', \App\Livewire\Financial\PaymentPlanCreate::class)->name('payment-plans.create')
->middleware('permission:invoices.create');
// Print — Group Schedule, Participant Card, Invoice
Route::get('/groups/{group}/print', \App\Http\Controllers\GroupSchedulePrintController::class)->name('groups.print')
->middleware('permission:groups.list');
Route::get('/participants/{participant}/card', \App\Http\Controllers\ParticipantCardController::class)->name('participants.card')
->middleware('permission:participants.view');
Route::get('/invoices/{invoice}/print', \App\Http\Controllers\InvoicePrintController::class)->name('invoices.print')
->middleware('permission:invoices.view');
// Participants — Bulk Actions
Route::get('/participants/bulk-status', \App\Livewire\Participants\BulkStatusChange::class)->name('participants.bulk-status')
->middleware('permission:participants.update');
// Certificates
Route::get('/participants/{participant}/certificate/{enrollment}', [\App\Http\Controllers\CertificateController::class, 'attendance'])
->name('participants.certificate')
->middleware('permission:participants.view');
// Activity Log (Admin)
Route::get('/admin/activity-log', \App\Livewire\Admin\ActivityLog::class)->name('admin.activity-log')
->middleware('permission:audit.view');
Route::get('/admin/system-settings', \App\Livewire\Admin\SystemSettings::class)->name('admin.system-settings')
->middleware('permission:settings.manage');
// Guardian Portal
Route::get('/guardian', GuardianDashboard::class)->name('guardian.dashboard');
}); });
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