Commit 71a6c06f authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add financial overview dashboard, enforce setup wizard, fix permission system

Financial Overview:
- New FinancialOverview Livewire component with P&L, break-even gauge,
  facility costs breakdown, revenue sources, collection rate
- Migration adds monthly_rental_cost to facilities table
- Monthly cost field added to facility form
- Sidebar entry under المالية section

Setup Wizard Enforcement:
- Activities are now mandatory (min 1)
- Programs are now mandatory with price > 0
- Facilities are now mandatory with monthly cost field
- Auto-creates BasePrice records for programs during setup
- Facility form includes operating hours and monthly cost

Permission System Fix:
- PermissionSeeder now called from DatabaseSeeder (has correct permission
  names matching route middleware: pos.sell, attendance.mark, etc.)
- Branch manager gets all relevant module prefixes (inventory, pricing,
  wallets, activities, audit, notifications)
- Receptionist gets pos.sell, pos.list, cash_sessions.manage
- Accountant gets reports.view, wallets.view, pricing.list
- PermissionService falls back to roles() pivot when primaryRole is null
- SetCurrentAcademy middleware eager-loads roles.permissions as fallback
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 2fc16d13
...@@ -39,6 +39,7 @@ class Facility extends Model ...@@ -39,6 +39,7 @@ class Facility extends Model
'operating_start', 'operating_start',
'operating_end', 'operating_end',
'rental_cost_per_hour', 'rental_cost_per_hour',
'monthly_rental_cost',
'address', 'address',
'latitude', 'latitude',
'longitude', 'longitude',
...@@ -62,6 +63,7 @@ protected function casts(): array ...@@ -62,6 +63,7 @@ protected function casts(): array
'has_lighting' => 'boolean', 'has_lighting' => 'boolean',
'has_ac' => 'boolean', 'has_ac' => 'boolean',
'rental_cost_per_hour' => 'integer', 'rental_cost_per_hour' => 'integer',
'monthly_rental_cost' => 'integer',
'amenities' => 'array', 'amenities' => 'array',
'metadata' => 'array', 'metadata' => 'array',
'sort_order' => 'integer', 'sort_order' => 'integer',
......
...@@ -20,7 +20,14 @@ public function can(User $user, string $permission, ?Model $record = null): bool ...@@ -20,7 +20,14 @@ public function can(User $user, string $permission, ?Model $record = null): bool
// Get user's role (primary role via role_id for performance) // Get user's role (primary role via role_id for performance)
$role = $user->primaryRole; $role = $user->primaryRole;
if (!$role) { if (!$role) {
return false; // Fallback: check many-to-many roles
$role = $user->roles->first();
if (!$role) {
return false;
}
if (!$role->relationLoaded('permissions')) {
$role->load('permissions');
}
} }
// Check if role has this permission // Check if role has this permission
...@@ -56,7 +63,13 @@ public function applyScope(Builder $query, User $user, string $permission): Buil ...@@ -56,7 +63,13 @@ public function applyScope(Builder $query, User $user, string $permission): Buil
$role = $user->primaryRole; $role = $user->primaryRole;
if (!$role) { if (!$role) {
return $query->whereRaw('1 = 0'); // no results $role = $user->roles->first();
if (!$role) {
return $query->whereRaw('1 = 0');
}
if (!$role->relationLoaded('permissions')) {
$role->load('permissions');
}
} }
$rolePermission = $role->permissions->firstWhere('name', $permission); $rolePermission = $role->permissions->firstWhere('name', $permission);
......
...@@ -21,6 +21,8 @@ public function handle(Request $request, Closure $next): Response ...@@ -21,6 +21,8 @@ public function handle(Request $request, Closure $next): Response
// Eager-load role with permissions for permission checks // Eager-load role with permissions for permission checks
if ($user->role_id && !$user->relationLoaded('primaryRole')) { if ($user->role_id && !$user->relationLoaded('primaryRole')) {
$user->load('primaryRole.permissions'); $user->load('primaryRole.permissions');
} elseif (!$user->role_id && !$user->relationLoaded('roles')) {
$user->load('roles.permissions');
} }
} }
......
...@@ -37,6 +37,7 @@ class FacilityForm extends Component ...@@ -37,6 +37,7 @@ class FacilityForm extends Component
public string $operating_start = ''; public string $operating_start = '';
public string $operating_end = ''; public string $operating_end = '';
public ?string $rental_cost_display = null; public ?string $rental_cost_display = null;
public ?string $monthly_rental_cost_display = null;
public string $address = ''; public string $address = '';
public ?string $latitude = null; public ?string $latitude = null;
public ?string $longitude = null; public ?string $longitude = null;
...@@ -68,6 +69,7 @@ public function mount(?Facility $facility = null): void ...@@ -68,6 +69,7 @@ public function mount(?Facility $facility = null): void
$this->operating_start = $facility->operating_start ?? ''; $this->operating_start = $facility->operating_start ?? '';
$this->operating_end = $facility->operating_end ?? ''; $this->operating_end = $facility->operating_end ?? '';
$this->rental_cost_display = $facility->rental_cost_per_hour ? (string) ($facility->rental_cost_per_hour / 100) : null; $this->rental_cost_display = $facility->rental_cost_per_hour ? (string) ($facility->rental_cost_per_hour / 100) : null;
$this->monthly_rental_cost_display = $facility->monthly_rental_cost ? (string) ($facility->monthly_rental_cost / 100) : null;
$this->address = $facility->address ?? ''; $this->address = $facility->address ?? '';
$this->latitude = $facility->latitude; $this->latitude = $facility->latitude;
$this->longitude = $facility->longitude; $this->longitude = $facility->longitude;
...@@ -97,6 +99,7 @@ public function rules(): array ...@@ -97,6 +99,7 @@ public function rules(): array
'operating_start' => 'nullable|date_format:H:i', 'operating_start' => 'nullable|date_format:H:i',
'operating_end' => 'nullable|date_format:H:i|after:operating_start', 'operating_end' => 'nullable|date_format:H:i|after:operating_start',
'rental_cost_display' => 'nullable|numeric|min:0', 'rental_cost_display' => 'nullable|numeric|min:0',
'monthly_rental_cost_display' => 'nullable|numeric|min:0',
'address' => 'nullable|string|max:255', 'address' => 'nullable|string|max:255',
'latitude' => 'nullable|numeric|between:-90,90', 'latitude' => 'nullable|numeric|between:-90,90',
'longitude' => 'nullable|numeric|between:-180,180', 'longitude' => 'nullable|numeric|between:-180,180',
...@@ -123,8 +126,10 @@ public function messages(): array ...@@ -123,8 +126,10 @@ public function messages(): array
'operating_start.date_format' => 'وقت البداية يجب أن يكون بصيغة ساعة:دقيقة', 'operating_start.date_format' => 'وقت البداية يجب أن يكون بصيغة ساعة:دقيقة',
'operating_end.date_format' => 'وقت النهاية يجب أن يكون بصيغة ساعة:دقيقة', 'operating_end.date_format' => 'وقت النهاية يجب أن يكون بصيغة ساعة:دقيقة',
'operating_end.after' => 'وقت النهاية يجب أن يكون بعد وقت البداية', 'operating_end.after' => 'وقت النهاية يجب أن يكون بعد وقت البداية',
'rental_cost_display.numeric' => 'تكلفة الإيجار يجب أن تكون رقم', 'rental_cost_display.numeric' => 'تكلفة الإيجار بالساعة يجب أن تكون رقم',
'rental_cost_display.min' => 'تكلفة الإيجار لا يمكن أن تكون سالبة', 'rental_cost_display.min' => 'تكلفة الإيجار بالساعة لا يمكن أن تكون سالبة',
'monthly_rental_cost_display.numeric' => 'تكلفة الإيجار الشهري يجب أن تكون رقم',
'monthly_rental_cost_display.min' => 'تكلفة الإيجار الشهري لا يمكن أن تكون سالبة',
'branch_id.required' => 'الفرع مطلوب', 'branch_id.required' => 'الفرع مطلوب',
'branch_id.exists' => 'الفرع غير موجود', 'branch_id.exists' => 'الفرع غير موجود',
'sort_order.integer' => 'الترتيب يجب أن يكون رقم صحيح', 'sort_order.integer' => 'الترتيب يجب أن يكون رقم صحيح',
...@@ -158,6 +163,7 @@ public function save(FacilityService $service): void ...@@ -158,6 +163,7 @@ public function save(FacilityService $service): void
'operating_start' => $this->operating_start ?: null, 'operating_start' => $this->operating_start ?: null,
'operating_end' => $this->operating_end ?: null, 'operating_end' => $this->operating_end ?: null,
'rental_cost_per_hour' => $this->rental_cost_display ? (int) round((float) $this->rental_cost_display * 100) : null, 'rental_cost_per_hour' => $this->rental_cost_display ? (int) round((float) $this->rental_cost_display * 100) : null,
'monthly_rental_cost' => $this->monthly_rental_cost_display ? (int) round((float) $this->monthly_rental_cost_display * 100) : null,
'address' => $this->address ?: null, 'address' => $this->address ?: null,
'latitude' => $this->latitude, 'latitude' => $this->latitude,
'longitude' => $this->longitude, 'longitude' => $this->longitude,
......
This diff is collapsed.
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
use App\Domain\Training\Models\Activity; use App\Domain\Training\Models\Activity;
use App\Domain\Training\Models\TrainingProgram; use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Facility\Models\Facility; use App\Domain\Facility\Models\Facility;
use App\Domain\Pricing\Models\BasePrice;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
...@@ -178,6 +179,9 @@ public function addFacility(): void ...@@ -178,6 +179,9 @@ public function addFacility(): void
'name_ar' => '', 'name_ar' => '',
'type' => 'field', 'type' => 'field',
'capacity' => '', 'capacity' => '',
'monthly_cost' => '',
'operating_start' => '08:00',
'operating_end' => '22:00',
'address' => '', 'address' => '',
]; ];
} }
...@@ -260,9 +264,10 @@ protected function validateBranches(): void ...@@ -260,9 +264,10 @@ protected function validateBranches(): void
protected function validateActivities(): void protected function validateActivities(): void
{ {
// Activities are optional, but if added, must be valid
if (empty($this->activities)) { if (empty($this->activities)) {
return; throw \Illuminate\Validation\ValidationException::withMessages([
'activities' => __('يجب إضافة نشاط واحد على الأقل حتى يعمل النظام'),
]);
} }
$validCategories = implode(',', array_column(ActivityCategory::cases(), 'value')); $validCategories = implode(',', array_column(ActivityCategory::cases(), 'value'));
...@@ -281,7 +286,9 @@ protected function validateActivities(): void ...@@ -281,7 +286,9 @@ protected function validateActivities(): void
protected function validatePrograms(): void protected function validatePrograms(): void
{ {
if (empty($this->programs)) { if (empty($this->programs)) {
return; throw \Illuminate\Validation\ValidationException::withMessages([
'programs' => __('يجب إضافة برنامج واحد على الأقل مع تحديد السعر'),
]);
} }
$this->validate([ $this->validate([
...@@ -289,19 +296,22 @@ protected function validatePrograms(): void ...@@ -289,19 +296,22 @@ protected function validatePrograms(): void
'programs.*.activity_index' => 'required|integer|min:0', 'programs.*.activity_index' => 'required|integer|min:0',
'programs.*.duration_months' => 'required|integer|min:1|max:24', 'programs.*.duration_months' => 'required|integer|min:1|max:24',
'programs.*.max_participants' => 'required|integer|min:1|max:500', 'programs.*.max_participants' => 'required|integer|min:1|max:500',
'programs.*.monthly_fee' => 'required|numeric|min:0', 'programs.*.monthly_fee' => 'required|numeric|min:1',
], [ ], [
'programs.*.name_ar.required' => __('اسم البرنامج بالعربية مطلوب'), 'programs.*.name_ar.required' => __('اسم البرنامج بالعربية مطلوب'),
'programs.*.duration_months.required' => __('مدة البرنامج مطلوبة'), 'programs.*.duration_months.required' => __('مدة البرنامج مطلوبة'),
'programs.*.max_participants.required' => __('الحد الأقصى للمشتركين مطلوب'), 'programs.*.max_participants.required' => __('الحد الأقصى للمشتركين مطلوب'),
'programs.*.monthly_fee.required' => __('الرسوم الشهرية مطلوبة'), 'programs.*.monthly_fee.required' => __('الرسوم الشهرية مطلوبة'),
'programs.*.monthly_fee.min' => __('يجب تحديد سعر البرنامج (لا يمكن أن يكون صفر)'),
]); ]);
} }
protected function validateFacilities(): void protected function validateFacilities(): void
{ {
if (empty($this->facilities)) { if (empty($this->facilities)) {
return; throw \Illuminate\Validation\ValidationException::withMessages([
'facilities' => __('يجب إضافة منشأة واحدة على الأقل (ملعب، قاعة، حمام سباحة...)'),
]);
} }
$validTypes = implode(',', array_column(FacilityType::cases(), 'value')); $validTypes = implode(',', array_column(FacilityType::cases(), 'value'));
...@@ -310,10 +320,14 @@ protected function validateFacilities(): void ...@@ -310,10 +320,14 @@ protected function validateFacilities(): void
'facilities.*.name_ar' => 'required|string|min:2|max:255', 'facilities.*.name_ar' => 'required|string|min:2|max:255',
'facilities.*.type' => "required|string|in:{$validTypes}", 'facilities.*.type' => "required|string|in:{$validTypes}",
'facilities.*.capacity' => 'nullable|integer|min:1|max:10000', 'facilities.*.capacity' => 'nullable|integer|min:1|max:10000',
'facilities.*.monthly_cost' => 'required|numeric|min:0',
'facilities.*.operating_start' => 'nullable|date_format:H:i',
'facilities.*.operating_end' => 'nullable|date_format:H:i',
'facilities.*.address' => 'nullable|string|max:500', 'facilities.*.address' => 'nullable|string|max:500',
], [ ], [
'facilities.*.name_ar.required' => __('اسم المنشأة بالعربية مطلوب'), 'facilities.*.name_ar.required' => __('اسم المنشأة بالعربية مطلوب'),
'facilities.*.type.required' => __('نوع المنشأة مطلوب'), 'facilities.*.type.required' => __('نوع المنشأة مطلوب'),
'facilities.*.monthly_cost.required' => __('الإيجار الشهري مطلوب (أدخل 0 إذا كانت ملك)'),
]); ]);
} }
...@@ -382,14 +396,16 @@ public function completeSetup(): void ...@@ -382,14 +396,16 @@ public function completeSetup(): void
]); ]);
} }
// Create programs // Create programs + base prices
foreach ($this->programs as $programData) { foreach ($this->programs as $programData) {
$activityIndex = (int) $programData['activity_index']; $activityIndex = (int) $programData['activity_index'];
$activity = $createdActivities[$activityIndex] ?? null; $activity = $createdActivities[$activityIndex] ?? null;
if ($activity) { if ($activity) {
$durationMonths = (int) $programData['duration_months']; $durationMonths = (int) $programData['duration_months'];
TrainingProgram::create([ $monthlyFeePiasters = (int) round((float) $programData['monthly_fee'] * 100);
$program = TrainingProgram::create([
'academy_id' => $academyId, 'academy_id' => $academyId,
'activity_id' => $activity->id, 'activity_id' => $activity->id,
'name_ar' => $programData['name_ar'], 'name_ar' => $programData['name_ar'],
...@@ -400,7 +416,23 @@ public function completeSetup(): void ...@@ -400,7 +416,23 @@ public function completeSetup(): void
'status' => 'active', 'status' => 'active',
'registration_open' => true, 'registration_open' => true,
'created_by' => $actor->id, 'created_by' => $actor->id,
'metadata' => ['monthly_fee_piasters' => (int) round((float) $programData['monthly_fee'] * 100)], 'metadata' => ['monthly_fee_piasters' => $monthlyFeePiasters],
]);
BasePrice::create([
'academy_id' => $academyId,
'priceable_type' => TrainingProgram::class,
'priceable_id' => $program->id,
'branch_id' => null,
'name_ar' => 'الاشتراك الشهري — ' . $programData['name_ar'],
'name' => 'Monthly — ' . $programData['name_ar'],
'amount' => $monthlyFeePiasters,
'currency' => 'EGP',
'effective_from' => now()->toDateString(),
'effective_to' => null,
'is_active' => true,
'priority' => 1,
'created_by' => $actor->id,
]); ]);
} }
} }
...@@ -414,8 +446,12 @@ public function completeSetup(): void ...@@ -414,8 +446,12 @@ public function completeSetup(): void
'name' => $facilityData['name_ar'], 'name' => $facilityData['name_ar'],
'type' => $facilityData['type'], 'type' => $facilityData['type'],
'capacity' => !empty($facilityData['capacity']) ? (int) $facilityData['capacity'] : null, 'capacity' => !empty($facilityData['capacity']) ? (int) $facilityData['capacity'] : null,
'monthly_rental_cost' => !empty($facilityData['monthly_cost']) ? (int) round((float) $facilityData['monthly_cost'] * 100) : 0,
'operating_start' => $facilityData['operating_start'] ?? null,
'operating_end' => $facilityData['operating_end'] ?? null,
'address' => $facilityData['address'] ?? null, 'address' => $facilityData['address'] ?? null,
'status' => 'active', 'status' => 'active',
'created_by' => $actor->id,
]); ]);
} }
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('facilities', function (Blueprint $table) {
$table->bigInteger('monthly_rental_cost')->nullable()->after('rental_cost_per_hour');
});
}
public function down(): void
{
Schema::table('facilities', function (Blueprint $table) {
$table->dropColumn('monthly_rental_cost');
});
}
};
...@@ -40,6 +40,7 @@ public function run(): void ...@@ -40,6 +40,7 @@ public function run(): void
$this->call(FinancialAccountsSeeder::class); $this->call(FinancialAccountsSeeder::class);
$this->call(RolesAndPermissionsSeeder::class); $this->call(RolesAndPermissionsSeeder::class);
$this->call(PermissionSeeder::class);
// Assign academy_owner role to the admin user // Assign academy_owner role to the admin user
$ownerRole = Role::where('academy_id', $academy->id) $ownerRole = Role::where('academy_id', $academy->id)
......
...@@ -190,6 +190,7 @@ private function getRolePermissions(array $allPermissions): array ...@@ -190,6 +190,7 @@ private function getRolePermissions(array $allPermissions): array
'branch_manager' => array_filter($allPermissions, fn ($p) => 'branch_manager' => array_filter($allPermissions, fn ($p) =>
str_starts_with($p, 'participants.') || str_starts_with($p, 'participants.') ||
str_starts_with($p, 'guardians.') || str_starts_with($p, 'guardians.') ||
str_starts_with($p, 'activities.') ||
str_starts_with($p, 'programs.') || str_starts_with($p, 'programs.') ||
str_starts_with($p, 'groups.') || str_starts_with($p, 'groups.') ||
str_starts_with($p, 'sessions.') || str_starts_with($p, 'sessions.') ||
...@@ -199,14 +200,22 @@ private function getRolePermissions(array $allPermissions): array ...@@ -199,14 +200,22 @@ private function getRolePermissions(array $allPermissions): array
str_starts_with($p, 'excuses.') || str_starts_with($p, 'excuses.') ||
str_starts_with($p, 'invoices.') || str_starts_with($p, 'invoices.') ||
str_starts_with($p, 'payments.') || str_starts_with($p, 'payments.') ||
str_starts_with($p, 'wallets.') ||
str_starts_with($p, 'cash_sessions.') || str_starts_with($p, 'cash_sessions.') ||
str_starts_with($p, 'pricing.') ||
str_starts_with($p, 'pos.') || str_starts_with($p, 'pos.') ||
str_starts_with($p, 'inventory.') ||
str_starts_with($p, 'products.') ||
str_starts_with($p, 'warehouses.') ||
str_starts_with($p, 'facilities.') || str_starts_with($p, 'facilities.') ||
str_starts_with($p, 'reservations.') || str_starts_with($p, 'reservations.') ||
str_starts_with($p, 'assignments.') || str_starts_with($p, 'assignments.') ||
str_starts_with($p, 'evaluations.') || str_starts_with($p, 'evaluations.') ||
str_starts_with($p, 'notifications.') ||
str_starts_with($p, 'reports.') || str_starts_with($p, 'reports.') ||
$p === 'dashboard.view' || $p === 'holidays.list' str_starts_with($p, 'audit.') ||
$p === 'dashboard.view' || $p === 'holidays.list' ||
$p === 'settings.view'
), ),
'head_trainer' => [ 'head_trainer' => [
'dashboard.view', 'dashboard.view',
...@@ -233,12 +242,12 @@ private function getRolePermissions(array $allPermissions): array ...@@ -233,12 +242,12 @@ private function getRolePermissions(array $allPermissions): array
'participants.list', 'participants.create', 'participants.update', 'participants.show', 'participants.list', 'participants.create', 'participants.update', 'participants.show',
'guardians.list', 'guardians.create', 'guardians.update', 'guardians.list', 'guardians.create', 'guardians.update',
'enrollments.list', 'enrollments.create', 'enrollments.list', 'enrollments.create',
'pos.access', 'pos.access', 'pos.sell', 'pos.list',
'invoices.list', 'invoices.show', 'invoices.create', 'invoices.list', 'invoices.show', 'invoices.create',
'payments.list', 'payments.create', 'payments.list', 'payments.create',
'cash_sessions.open', 'cash_sessions.close', 'cash_sessions.list', 'cash_sessions.open', 'cash_sessions.close', 'cash_sessions.list', 'cash_sessions.manage',
'pos_sessions.open', 'pos_sessions.close', 'pos_sessions.list', 'pos_sessions.open', 'pos_sessions.close', 'pos_sessions.list',
'wallets.list', 'wallets.credit', 'wallets.list', 'wallets.view', 'wallets.credit',
], ],
'accountant' => [ 'accountant' => [
'dashboard.view', 'dashboard.view',
...@@ -247,12 +256,13 @@ private function getRolePermissions(array $allPermissions): array ...@@ -247,12 +256,13 @@ private function getRolePermissions(array $allPermissions): array
'payments.list', 'payments.create', 'payments.refund', 'payments.export', 'payments.list', 'payments.create', 'payments.refund', 'payments.export',
'transactions.list', 'transactions.export', 'transactions.list', 'transactions.export',
'accounts.list', 'accounts.create', 'accounts.update', 'accounts.list', 'accounts.create', 'accounts.update',
'wallets.list', 'wallets.credit', 'wallets.debit', 'wallets.freeze', 'wallets.list', 'wallets.view', 'wallets.credit', 'wallets.debit', 'wallets.freeze',
'payment_plans.list', 'payment_plans.create', 'payment_plans.update', 'payment_plans.list', 'payment_plans.create', 'payment_plans.update',
'cash_sessions.open', 'cash_sessions.close', 'cash_sessions.list', 'cash_sessions.open', 'cash_sessions.close', 'cash_sessions.list', 'cash_sessions.manage',
'refunds.initiate', 'refunds.approve', 'refunds.initiate', 'refunds.approve',
'daily_closing.create', 'daily_closing.view', 'daily_closing.create', 'daily_closing.view',
'reports.financial', 'reports.export_pdf', 'reports.export_excel', 'reports.financial', 'reports.view', 'reports.export_pdf', 'reports.export_excel',
'pricing.list',
], ],
'data_entry' => [ 'data_entry' => [
'dashboard.view', 'dashboard.view',
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
]], ]],
['section' => 'المالية', 'items' => [ ['section' => 'المالية', 'items' => [
['label' => 'النظرة المالية', 'route' => 'financial.overview', 'icon' => 'chart-bar', 'permission' => 'invoices.list'],
['label' => 'الفواتير', 'route' => 'invoices.list', 'icon' => 'document', 'permission' => 'invoices.list'], ['label' => 'الفواتير', 'route' => 'invoices.list', 'icon' => 'document', 'permission' => 'invoices.list'],
['label' => 'المحافظ', 'route' => 'wallets.list', 'icon' => 'wallet', 'permission' => 'wallets.list'], ['label' => 'المحافظ', 'route' => 'wallets.list', 'icon' => 'wallet', 'permission' => 'wallets.list'],
['label' => 'جلسات الكاشير', 'route' => 'cash-sessions.list', 'icon' => 'calculator', 'permission' => 'cash_sessions.list'], ['label' => 'جلسات الكاشير', 'route' => 'cash-sessions.list', 'icon' => 'calculator', 'permission' => 'cash_sessions.list'],
......
...@@ -138,13 +138,22 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin ...@@ -138,13 +138,22 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
@error('width_m') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('width_m') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
{{-- Rental Cost --}} {{-- Rental Cost per Hour --}}
<div> <div>
<label for="rental_cost_display" class="block text-sm font-medium text-gray-700 mb-1">{{ __('تكلفة الإيجار/ساعة (ج.م)') }}</label> <label for="rental_cost_display" class="block text-sm font-medium text-gray-700 mb-1">{{ __('تكلفة الإيجار/ساعة (ج.م)') }}</label>
<input type="number" id="rental_cost_display" wire:model="rental_cost_display" min="0" step="0.01" dir="ltr" <input type="number" id="rental_cost_display" wire:model="rental_cost_display" min="0" step="0.01" dir="ltr"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('rental_cost_display') border-red-500 @enderror"> class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('rental_cost_display') border-red-500 @enderror">
@error('rental_cost_display') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror @error('rental_cost_display') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div> </div>
{{-- Monthly Rental Cost --}}
<div>
<label for="monthly_rental_cost_display" class="block text-sm font-medium text-gray-700 mb-1">{{ __('الإيجار الشهري (ج.م)') }}</label>
<input type="number" id="monthly_rental_cost_display" wire:model="monthly_rental_cost_display" min="0" step="0.01" dir="ltr"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('monthly_rental_cost_display') border-red-500 @enderror"
placeholder="{{ __('المبلغ الذي تدفعه شهرياً لاستئجار هذه المنشأة') }}">
@error('monthly_rental_cost_display') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
</div> </div>
{{-- Boolean Options --}} {{-- Boolean Options --}}
......
...@@ -189,9 +189,13 @@ class="mt-4 w-full border-2 border-dashed border-gray-300 rounded-xl p-4 text-gr ...@@ -189,9 +189,13 @@ class="mt-4 w-full border-2 border-dashed border-gray-300 rounded-xl p-4 text-gr
<div class="p-8"> <div class="p-8">
<div class="mb-6"> <div class="mb-6">
<h2 class="text-xl font-bold text-gray-800">{{ __('الأنشطة') }}</h2> <h2 class="text-xl font-bold text-gray-800">{{ __('الأنشطة') }}</h2>
<p class="text-sm text-gray-500 mt-1">{{ __('أضف الأنشطة الرياضية أو التعليمية التي تقدمها الأكاديمية. يمكنك تخطي هذه الخطوة وإضافتها لاحقاً.') }}</p> <p class="text-sm text-gray-500 mt-1">{{ __('أضف الأنشطة الرياضية أو التعليمية التي تقدمها الأكاديمية (مطلوب نشاط واحد على الأقل).') }}</p>
</div> </div>
@error('activities')
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{{ $message }}</div>
@enderror
<div class="space-y-4"> <div class="space-y-4">
@foreach($activities as $index => $activity) @foreach($activities as $index => $activity)
<div class="border border-gray-200 rounded-xl p-5 relative group hover:border-blue-200 transition-colors" <div class="border border-gray-200 rounded-xl p-5 relative group hover:border-blue-200 transition-colors"
...@@ -274,9 +278,13 @@ class="mt-4 w-full border-2 border-dashed border-gray-300 rounded-xl p-4 text-gr ...@@ -274,9 +278,13 @@ class="mt-4 w-full border-2 border-dashed border-gray-300 rounded-xl p-4 text-gr
<div class="p-8"> <div class="p-8">
<div class="mb-6"> <div class="mb-6">
<h2 class="text-xl font-bold text-gray-800">{{ __('البرامج التدريبية') }}</h2> <h2 class="text-xl font-bold text-gray-800">{{ __('البرامج التدريبية') }}</h2>
<p class="text-sm text-gray-500 mt-1">{{ __('أضف البرامج المرتبطة بالأنشطة التي أضفتها. يمكنك تخطي هذه الخطوة.') }}</p> <p class="text-sm text-gray-500 mt-1">{{ __('أضف البرامج التدريبية مع أسعارها. مطلوب برنامج واحد على الأقل مع تحديد الرسوم الشهرية.') }}</p>
</div> </div>
@error('programs')
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{{ $message }}</div>
@enderror
@if(empty($activities)) @if(empty($activities))
<div class="text-center py-8 border-2 border-dashed border-gray-200 rounded-xl"> <div class="text-center py-8 border-2 border-dashed border-gray-200 rounded-xl">
<svg class="w-12 h-12 text-amber-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-12 h-12 text-amber-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
...@@ -382,9 +390,16 @@ class="mt-4 w-full border-2 border-dashed border-gray-300 rounded-xl p-4 text-gr ...@@ -382,9 +390,16 @@ class="mt-4 w-full border-2 border-dashed border-gray-300 rounded-xl p-4 text-gr
<div class="p-8"> <div class="p-8">
<div class="mb-6"> <div class="mb-6">
<h2 class="text-xl font-bold text-gray-800">{{ __('المنشآت') }}</h2> <h2 class="text-xl font-bold text-gray-800">{{ __('المنشآت') }}</h2>
<p class="text-sm text-gray-500 mt-1">{{ __('أضف الملاعب والمنشآت الرياضية الخاصة بالأكاديمية. يمكنك تخطي هذه الخطوة.') }}</p> <p class="text-sm text-gray-500 mt-1">{{ __('أضف الملاعب والمنشآت الرياضية. حدد الإيجار الشهري وساعات العمل لحساب نقطة التعادل.') }}</p>
<div class="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg text-xs text-amber-700">
<strong>{{ __('مهم:') }}</strong> {{ __('الإيجار الشهري يستخدم في حساب الأرباح والخسائر. إذا كانت المنشأة ملكك أدخل 0.') }}
</div>
</div> </div>
@error('facilities')
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{{ $message }}</div>
@enderror
<div class="space-y-4"> <div class="space-y-4">
@foreach($facilities as $index => $facility) @foreach($facilities as $index => $facility)
<div class="border border-gray-200 rounded-xl p-5 relative group hover:border-blue-200 transition-colors" <div class="border border-gray-200 rounded-xl p-5 relative group hover:border-blue-200 transition-colors"
...@@ -396,7 +411,7 @@ class="absolute top-3 start-3 w-7 h-7 rounded-full bg-red-50 text-red-500 hover: ...@@ -396,7 +411,7 @@ class="absolute top-3 start-3 w-7 h-7 rounded-full bg-red-50 text-red-500 hover:
</svg> </svg>
</button> </button>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">
{{ __('اسم المنشأة') }} <span class="text-red-500">*</span> {{ __('اسم المنشأة') }} <span class="text-red-500">*</span>
...@@ -422,6 +437,17 @@ class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:r ...@@ -422,6 +437,17 @@ class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:r
<p class="mt-1 text-xs text-red-600">{{ $message }}</p> <p class="mt-1 text-xs text-red-600">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ __('الإيجار الشهري (ج.م)') }} <span class="text-red-500">*</span>
</label>
<input type="number" wire:model="facilities.{{ $index }}.monthly_cost" dir="ltr"
class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
min="0" step="0.01" placeholder="5000.00">
@error("facilities.{$index}.monthly_cost")
<p class="mt-1 text-xs text-red-600">{{ $message }}</p>
@enderror
</div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">
{{ __('السعة') }} {{ __('السعة') }}
...@@ -430,6 +456,18 @@ class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:r ...@@ -430,6 +456,18 @@ class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:r
class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
min="1" placeholder="50"> min="1" placeholder="50">
</div> </div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ __('ساعات العمل') }}
</label>
<div class="flex items-center gap-2">
<input type="time" wire:model="facilities.{{ $index }}.operating_start" dir="ltr"
class="flex-1 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm">
<span class="text-gray-400"></span>
<input type="time" wire:model="facilities.{{ $index }}.operating_end" dir="ltr"
class="flex-1 rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm">
</div>
</div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">
{{ __('العنوان') }} {{ __('العنوان') }}
...@@ -448,7 +486,8 @@ class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:r ...@@ -448,7 +486,8 @@ class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:r
<svg class="w-12 h-12 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-12 h-12 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg> </svg>
<p class="text-gray-400 text-sm">{{ __('لم تضف منشآت بعد. أضف منشأة أو تخطَّ هذه الخطوة.') }}</p> <p class="text-red-500 text-sm font-medium">{{ __('يجب إضافة منشأة واحدة على الأقل') }}</p>
<p class="text-gray-400 text-xs mt-1">{{ __('أضف الملعب أو القاعة التي تعمل فيها') }}</p>
</div> </div>
@endif @endif
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
use App\Livewire\Evaluations\EvaluationShow; use App\Livewire\Evaluations\EvaluationShow;
use App\Livewire\Facilities\FacilityForm; use App\Livewire\Facilities\FacilityForm;
use App\Livewire\Facilities\FacilityList; use App\Livewire\Facilities\FacilityList;
use App\Livewire\Financial\FinancialOverview;
use App\Livewire\Auth\Login; use App\Livewire\Auth\Login;
use App\Livewire\CashSessions\CashSessionList; use App\Livewire\CashSessions\CashSessionList;
use App\Livewire\CashSessions\CashSessionManage; use App\Livewire\CashSessions\CashSessionManage;
...@@ -178,6 +179,10 @@ ...@@ -178,6 +179,10 @@
Route::get('/cash-sessions/manage', CashSessionManage::class)->name('cash-sessions.manage') Route::get('/cash-sessions/manage', CashSessionManage::class)->name('cash-sessions.manage')
->middleware('permission:cash_sessions.manage'); ->middleware('permission:cash_sessions.manage');
// Financial Overview
Route::get('/financial-overview', FinancialOverview::class)->name('financial.overview')
->middleware('permission:invoices.list');
// Invoices // Invoices
Route::get('/invoices', InvoiceList::class)->name('invoices.list') Route::get('/invoices', InvoiceList::class)->name('invoices.list')
->middleware('permission:invoices.list'); ->middleware('permission:invoices.list');
......
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