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
'operating_start',
'operating_end',
'rental_cost_per_hour',
'monthly_rental_cost',
'address',
'latitude',
'longitude',
......@@ -62,6 +63,7 @@ protected function casts(): array
'has_lighting' => 'boolean',
'has_ac' => 'boolean',
'rental_cost_per_hour' => 'integer',
'monthly_rental_cost' => 'integer',
'amenities' => 'array',
'metadata' => 'array',
'sort_order' => 'integer',
......
......@@ -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)
$role = $user->primaryRole;
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
......@@ -56,7 +63,13 @@ public function applyScope(Builder $query, User $user, string $permission): Buil
$role = $user->primaryRole;
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);
......
......@@ -21,6 +21,8 @@ public function handle(Request $request, Closure $next): Response
// Eager-load role with permissions for permission checks
if ($user->role_id && !$user->relationLoaded('primaryRole')) {
$user->load('primaryRole.permissions');
} elseif (!$user->role_id && !$user->relationLoaded('roles')) {
$user->load('roles.permissions');
}
}
......
......@@ -37,6 +37,7 @@ class FacilityForm extends Component
public string $operating_start = '';
public string $operating_end = '';
public ?string $rental_cost_display = null;
public ?string $monthly_rental_cost_display = null;
public string $address = '';
public ?string $latitude = null;
public ?string $longitude = null;
......@@ -68,6 +69,7 @@ public function mount(?Facility $facility = null): void
$this->operating_start = $facility->operating_start ?? '';
$this->operating_end = $facility->operating_end ?? '';
$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->latitude = $facility->latitude;
$this->longitude = $facility->longitude;
......@@ -97,6 +99,7 @@ public function rules(): array
'operating_start' => 'nullable|date_format:H:i',
'operating_end' => 'nullable|date_format:H:i|after:operating_start',
'rental_cost_display' => 'nullable|numeric|min:0',
'monthly_rental_cost_display' => 'nullable|numeric|min:0',
'address' => 'nullable|string|max:255',
'latitude' => 'nullable|numeric|between:-90,90',
'longitude' => 'nullable|numeric|between:-180,180',
......@@ -123,8 +126,10 @@ public function messages(): array
'operating_start.date_format' => 'وقت البداية يجب أن يكون بصيغة ساعة:دقيقة',
'operating_end.date_format' => 'وقت النهاية يجب أن يكون بصيغة ساعة:دقيقة',
'operating_end.after' => 'وقت النهاية يجب أن يكون بعد وقت البداية',
'rental_cost_display.numeric' => 'تكلفة الإيجار يجب أن تكون رقم',
'rental_cost_display.min' => 'تكلفة الإيجار لا يمكن أن تكون سالبة',
'rental_cost_display.numeric' => 'تكلفة الإيجار بالساعة يجب أن تكون رقم',
'rental_cost_display.min' => 'تكلفة الإيجار بالساعة لا يمكن أن تكون سالبة',
'monthly_rental_cost_display.numeric' => 'تكلفة الإيجار الشهري يجب أن تكون رقم',
'monthly_rental_cost_display.min' => 'تكلفة الإيجار الشهري لا يمكن أن تكون سالبة',
'branch_id.required' => 'الفرع مطلوب',
'branch_id.exists' => 'الفرع غير موجود',
'sort_order.integer' => 'الترتيب يجب أن يكون رقم صحيح',
......@@ -158,6 +163,7 @@ public function save(FacilityService $service): void
'operating_start' => $this->operating_start ?: null,
'operating_end' => $this->operating_end ?: 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,
'latitude' => $this->latitude,
'longitude' => $this->longitude,
......
<?php
namespace App\Livewire\Financial;
use App\Domain\Facility\Models\Facility;
use App\Domain\Financial\Enums\AccountType;
use App\Domain\Financial\Models\FinancialAccount;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment;
use App\Domain\Financial\Models\Transaction;
use App\Domain\Identity\Models\Branch;
use App\Domain\Inventory\Models\PurchaseOrder;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('النظرة المالية العامة')]
class FinancialOverview extends Component
{
#[Url]
public string $period = 'this_month';
#[Url]
public string $branch_id = '';
public function mount()
{
$this->authorize('invoices.list');
}
public function render()
{
[$from, $to] = $this->resolvePeriod();
$branches = Branch::orderBy('name_ar')->get();
$facilityCosts = $this->getFacilityCosts();
$revenue = $this->getRevenue($from, $to);
$expenses = $this->getExpenses($from, $to);
$monthlyPL = $this->getMonthlyPL();
$breakEven = $this->getBreakEvenData($facilityCosts, $revenue);
$revenueBySource = $this->getRevenueBySource($from, $to);
$collectionRate = $this->getCollectionRate($from, $to);
return view('livewire.financial.financial-overview', [
'branches' => $branches,
'facilityCosts' => $facilityCosts,
'revenue' => $revenue,
'expenses' => $expenses,
'monthlyPL' => $monthlyPL,
'breakEven' => $breakEven,
'revenueBySource' => $revenueBySource,
'collectionRate' => $collectionRate,
'periodLabel' => $this->getPeriodLabel(),
'from' => $from,
'to' => $to,
]);
}
private function resolvePeriod(): array
{
return match ($this->period) {
'today' => [now()->startOfDay(), now()->endOfDay()],
'this_week' => [now()->startOfWeek(), now()->endOfWeek()],
'this_month' => [now()->startOfMonth(), now()->endOfMonth()],
'last_month' => [now()->subMonth()->startOfMonth(), now()->subMonth()->endOfMonth()],
'this_quarter' => [now()->firstOfQuarter(), now()->lastOfQuarter()->endOfDay()],
'this_year' => [now()->startOfYear(), now()->endOfYear()],
default => [now()->startOfMonth(), now()->endOfMonth()],
};
}
private function getPeriodLabel(): string
{
return match ($this->period) {
'today' => 'اليوم',
'this_week' => 'هذا الأسبوع',
'this_month' => 'هذا الشهر',
'last_month' => 'الشهر الماضي',
'this_quarter' => 'هذا الربع',
'this_year' => 'هذا العام',
default => 'هذا الشهر',
};
}
private function getFacilityCosts(): array
{
$query = Facility::query()->whereNotNull('monthly_rental_cost')->where('monthly_rental_cost', '>', 0);
if ($this->branch_id) {
$query->where('branch_id', $this->branch_id);
}
$facilities = $query->with('branch')->get();
$totalMonthly = $facilities->sum('monthly_rental_cost');
return [
'facilities' => $facilities->map(fn ($f) => [
'name' => $f->name_ar,
'branch' => $f->branch?->name_ar ?? '-',
'monthly_cost' => $f->monthly_rental_cost,
'hourly_cost' => $f->rental_cost_per_hour,
'status' => $f->status->value,
])->toArray(),
'total_monthly' => $totalMonthly,
'total_yearly' => $totalMonthly * 12,
];
}
private function getRevenue($from, $to): array
{
$query = Payment::where('direction', 'inbound')
->where('status', 'confirmed')
->whereBetween('payment_date', [$from, $to]);
if ($this->branch_id) {
$query->whereHas('invoice', fn ($q) => $q->whereHas('items', fn ($iq) => $iq->where('branch_id', $this->branch_id)));
}
$totalRevenue = $query->sum('amount');
$byMethod = Payment::where('direction', 'inbound')
->where('status', 'confirmed')
->whereBetween('payment_date', [$from, $to])
->select('method', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count'))
->groupBy('method')
->get();
return [
'total' => $totalRevenue,
'by_method' => $byMethod->map(fn ($p) => [
'method' => $p->method?->value ?? $p->method,
'total' => $p->total,
'count' => $p->count,
])->toArray(),
'transaction_count' => $query->count(),
];
}
private function getExpenses($from, $to): array
{
$outboundPayments = Payment::where('direction', 'outbound')
->where('status', 'confirmed')
->whereBetween('payment_date', [$from, $to])
->sum('amount');
$purchaseOrders = PurchaseOrder::whereIn('status', ['confirmed', 'received', 'partially_received'])
->whereBetween('created_at', [$from, $to])
->sum('total_amount');
$facilityCostsThisPeriod = $this->calculateProRatedFacilityCost($from, $to);
return [
'outbound_payments' => $outboundPayments,
'purchase_orders' => $purchaseOrders,
'facility_costs' => $facilityCostsThisPeriod,
'total' => $outboundPayments + $purchaseOrders + $facilityCostsThisPeriod,
];
}
private function calculateProRatedFacilityCost($from, $to): int
{
$query = Facility::whereNotNull('monthly_rental_cost')->where('monthly_rental_cost', '>', 0);
if ($this->branch_id) {
$query->where('branch_id', $this->branch_id);
}
$totalMonthly = $query->sum('monthly_rental_cost');
$days = max(1, now()->parse($from)->diffInDays(now()->parse($to)));
$daysInMonth = now()->daysInMonth;
return (int) round($totalMonthly * ($days / $daysInMonth));
}
private function getMonthlyPL(): array
{
$months = [];
for ($i = 5; $i >= 0; $i--) {
$month = now()->subMonths($i);
$start = $month->copy()->startOfMonth();
$end = $month->copy()->endOfMonth();
$income = Payment::where('direction', 'inbound')
->where('status', 'confirmed')
->whereBetween('payment_date', [$start, $end])
->sum('amount');
$outbound = Payment::where('direction', 'outbound')
->where('status', 'confirmed')
->whereBetween('payment_date', [$start, $end])
->sum('amount');
$purchases = PurchaseOrder::whereIn('status', ['confirmed', 'received', 'partially_received'])
->whereBetween('created_at', [$start, $end])
->sum('total_amount');
$facilityQuery = Facility::whereNotNull('monthly_rental_cost')->where('monthly_rental_cost', '>', 0);
if ($this->branch_id) {
$facilityQuery->where('branch_id', $this->branch_id);
}
$facilityCost = $facilityQuery->sum('monthly_rental_cost');
$totalExpenses = $outbound + $purchases + $facilityCost;
$months[] = [
'label' => $month->translatedFormat('M Y'),
'month_key' => $month->format('Y-m'),
'income' => $income,
'expenses' => $totalExpenses,
'profit' => $income - $totalExpenses,
];
}
return $months;
}
private function getBreakEvenData(array $facilityCosts, array $revenue): array
{
$monthlyFixedCosts = $facilityCosts['total_monthly'];
if ($monthlyFixedCosts <= 0) {
return [
'has_data' => false,
'message' => 'لم يتم تحديد تكاليف المنشآت الشهرية بعد',
];
}
$currentMonthRevenue = $revenue['total'];
$percentage = min(100, round(($currentMonthRevenue / $monthlyFixedCosts) * 100));
$remaining = max(0, $monthlyFixedCosts - $currentMonthRevenue);
return [
'has_data' => true,
'fixed_costs' => $monthlyFixedCosts,
'current_revenue' => $currentMonthRevenue,
'percentage' => $percentage,
'remaining' => $remaining,
'is_profitable' => $currentMonthRevenue >= $monthlyFixedCosts,
];
}
private function getRevenueBySource($from, $to): array
{
$revenueAccounts = FinancialAccount::where('type', AccountType::Revenue)->pluck('id', 'name_ar');
$bySource = Transaction::whereBetween('transaction_date', [$from, $to])
->whereIn('credit_account_id', $revenueAccounts->values())
->select('credit_account_id', DB::raw('SUM(amount) as total'))
->groupBy('credit_account_id')
->get();
$accountNames = FinancialAccount::whereIn('id', $bySource->pluck('credit_account_id'))->pluck('name_ar', 'id');
return $bySource->map(fn ($t) => [
'source' => $accountNames[$t->credit_account_id] ?? 'أخرى',
'total' => $t->total,
])->sortByDesc('total')->values()->toArray();
}
private function getCollectionRate($from, $to): array
{
$invoiced = Invoice::whereBetween('issue_date', [$from, $to])->sum('total_amount');
$collected = Invoice::whereBetween('issue_date', [$from, $to])->sum('paid_amount');
$overdue = Invoice::where('status', 'overdue')->sum('due_amount');
$overdueCount = Invoice::where('status', 'overdue')->count();
return [
'invoiced' => $invoiced,
'collected' => $collected,
'rate' => $invoiced > 0 ? round(($collected / $invoiced) * 100) : 0,
'overdue_amount' => $overdue,
'overdue_count' => $overdueCount,
];
}
}
......@@ -11,6 +11,7 @@
use App\Domain\Training\Models\Activity;
use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Facility\Models\Facility;
use App\Domain\Pricing\Models\BasePrice;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
......@@ -178,6 +179,9 @@ public function addFacility(): void
'name_ar' => '',
'type' => 'field',
'capacity' => '',
'monthly_cost' => '',
'operating_start' => '08:00',
'operating_end' => '22:00',
'address' => '',
];
}
......@@ -260,9 +264,10 @@ protected function validateBranches(): void
protected function validateActivities(): void
{
// Activities are optional, but if added, must be valid
if (empty($this->activities)) {
return;
throw \Illuminate\Validation\ValidationException::withMessages([
'activities' => __('يجب إضافة نشاط واحد على الأقل حتى يعمل النظام'),
]);
}
$validCategories = implode(',', array_column(ActivityCategory::cases(), 'value'));
......@@ -281,7 +286,9 @@ protected function validateActivities(): void
protected function validatePrograms(): void
{
if (empty($this->programs)) {
return;
throw \Illuminate\Validation\ValidationException::withMessages([
'programs' => __('يجب إضافة برنامج واحد على الأقل مع تحديد السعر'),
]);
}
$this->validate([
......@@ -289,19 +296,22 @@ protected function validatePrograms(): void
'programs.*.activity_index' => 'required|integer|min:0',
'programs.*.duration_months' => 'required|integer|min:1|max:24',
'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.*.duration_months.required' => __('مدة البرنامج مطلوبة'),
'programs.*.max_participants.required' => __('الحد الأقصى للمشتركين مطلوب'),
'programs.*.monthly_fee.required' => __('الرسوم الشهرية مطلوبة'),
'programs.*.monthly_fee.min' => __('يجب تحديد سعر البرنامج (لا يمكن أن يكون صفر)'),
]);
}
protected function validateFacilities(): void
{
if (empty($this->facilities)) {
return;
throw \Illuminate\Validation\ValidationException::withMessages([
'facilities' => __('يجب إضافة منشأة واحدة على الأقل (ملعب، قاعة، حمام سباحة...)'),
]);
}
$validTypes = implode(',', array_column(FacilityType::cases(), 'value'));
......@@ -310,10 +320,14 @@ protected function validateFacilities(): void
'facilities.*.name_ar' => 'required|string|min:2|max:255',
'facilities.*.type' => "required|string|in:{$validTypes}",
'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.*.name_ar.required' => __('اسم المنشأة بالعربية مطلوب'),
'facilities.*.type.required' => __('نوع المنشأة مطلوب'),
'facilities.*.monthly_cost.required' => __('الإيجار الشهري مطلوب (أدخل 0 إذا كانت ملك)'),
]);
}
......@@ -382,14 +396,16 @@ public function completeSetup(): void
]);
}
// Create programs
// Create programs + base prices
foreach ($this->programs as $programData) {
$activityIndex = (int) $programData['activity_index'];
$activity = $createdActivities[$activityIndex] ?? null;
if ($activity) {
$durationMonths = (int) $programData['duration_months'];
TrainingProgram::create([
$monthlyFeePiasters = (int) round((float) $programData['monthly_fee'] * 100);
$program = TrainingProgram::create([
'academy_id' => $academyId,
'activity_id' => $activity->id,
'name_ar' => $programData['name_ar'],
......@@ -400,7 +416,23 @@ public function completeSetup(): void
'status' => 'active',
'registration_open' => true,
'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
'name' => $facilityData['name_ar'],
'type' => $facilityData['type'],
'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,
'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
$this->call(FinancialAccountsSeeder::class);
$this->call(RolesAndPermissionsSeeder::class);
$this->call(PermissionSeeder::class);
// Assign academy_owner role to the admin user
$ownerRole = Role::where('academy_id', $academy->id)
......
......@@ -190,6 +190,7 @@ private function getRolePermissions(array $allPermissions): array
'branch_manager' => array_filter($allPermissions, fn ($p) =>
str_starts_with($p, 'participants.') ||
str_starts_with($p, 'guardians.') ||
str_starts_with($p, 'activities.') ||
str_starts_with($p, 'programs.') ||
str_starts_with($p, 'groups.') ||
str_starts_with($p, 'sessions.') ||
......@@ -199,14 +200,22 @@ private function getRolePermissions(array $allPermissions): array
str_starts_with($p, 'excuses.') ||
str_starts_with($p, 'invoices.') ||
str_starts_with($p, 'payments.') ||
str_starts_with($p, 'wallets.') ||
str_starts_with($p, 'cash_sessions.') ||
str_starts_with($p, 'pricing.') ||
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, 'reservations.') ||
str_starts_with($p, 'assignments.') ||
str_starts_with($p, 'evaluations.') ||
str_starts_with($p, 'notifications.') ||
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' => [
'dashboard.view',
......@@ -233,12 +242,12 @@ private function getRolePermissions(array $allPermissions): array
'participants.list', 'participants.create', 'participants.update', 'participants.show',
'guardians.list', 'guardians.create', 'guardians.update',
'enrollments.list', 'enrollments.create',
'pos.access',
'pos.access', 'pos.sell', 'pos.list',
'invoices.list', 'invoices.show', 'invoices.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',
'wallets.list', 'wallets.credit',
'wallets.list', 'wallets.view', 'wallets.credit',
],
'accountant' => [
'dashboard.view',
......@@ -247,12 +256,13 @@ private function getRolePermissions(array $allPermissions): array
'payments.list', 'payments.create', 'payments.refund', 'payments.export',
'transactions.list', 'transactions.export',
'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',
'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',
'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' => [
'dashboard.view',
......
......@@ -22,6 +22,7 @@
]],
['section' => 'المالية', 'items' => [
['label' => 'النظرة المالية', 'route' => 'financial.overview', 'icon' => 'chart-bar', 'permission' => 'invoices.list'],
['label' => 'الفواتير', 'route' => 'invoices.list', 'icon' => 'document', 'permission' => 'invoices.list'],
['label' => 'المحافظ', 'route' => 'wallets.list', 'icon' => 'wallet', 'permission' => 'wallets.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
@error('width_m') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Rental Cost --}}
{{-- Rental Cost per Hour --}}
<div>
<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"
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
</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>
{{-- Boolean Options --}}
......
<div>
{{-- Header --}}
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">{{ __('النظرة المالية العامة') }}</h1>
<p class="text-sm text-gray-500 mt-1">{{ __('ملخص الإيرادات والمصروفات والأرباح') }} — {{ $periodLabel }}</p>
</div>
<div class="flex items-center gap-3">
<select wire:model.live="branch_id" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">{{ __('كل الفروع') }}</option>
@foreach($branches as $branch)
<option value="{{ $branch->id }}">{{ $branch->name_ar }}</option>
@endforeach
</select>
<select wire:model.live="period" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="today">{{ __('اليوم') }}</option>
<option value="this_week">{{ __('هذا الأسبوع') }}</option>
<option value="this_month">{{ __('هذا الشهر') }}</option>
<option value="last_month">{{ __('الشهر الماضي') }}</option>
<option value="this_quarter">{{ __('هذا الربع') }}</option>
<option value="this_year">{{ __('هذا العام') }}</option>
</select>
</div>
</div>
{{-- Loading overlay --}}
<div wire:loading.class="opacity-50 pointer-events-none" class="transition-opacity">
{{-- Row 1: Summary Cards --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{{-- Total Revenue --}}
<div class="bg-white rounded-xl shadow-sm border border-emerald-200 p-5">
<div class="flex items-center gap-3">
<div class="w-11 h-11 bg-emerald-50 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
</svg>
</div>
<div class="min-w-0">
<p class="text-xs text-gray-500">{{ __('الإيرادات') }}</p>
<p class="text-xl font-bold text-emerald-700 truncate" dir="ltr">{{ number_format($revenue['total'] / 100, 2) }} <span class="text-sm font-normal">{{ __('ج.م') }}</span></p>
</div>
</div>
<p class="text-xs text-gray-400 mt-2" dir="ltr">{{ number_format($revenue['transaction_count']) }} {{ __('عملية') }}</p>
</div>
{{-- Total Expenses --}}
<div class="bg-white rounded-xl shadow-sm border border-red-200 p-5">
<div class="flex items-center gap-3">
<div class="w-11 h-11 bg-red-50 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"/>
</svg>
</div>
<div class="min-w-0">
<p class="text-xs text-gray-500">{{ __('المصروفات') }}</p>
<p class="text-xl font-bold text-red-700 truncate" dir="ltr">{{ number_format($expenses['total'] / 100, 2) }} <span class="text-sm font-normal">{{ __('ج.م') }}</span></p>
</div>
</div>
<p class="text-xs text-gray-400 mt-2">{{ __('إيجارات + مشتريات + مدفوعات') }}</p>
</div>
{{-- Net Profit/Loss --}}
@php $netProfit = $revenue['total'] - $expenses['total']; @endphp
<div class="bg-white rounded-xl shadow-sm border {{ $netProfit >= 0 ? 'border-blue-200' : 'border-orange-200' }} p-5">
<div class="flex items-center gap-3">
<div class="w-11 h-11 {{ $netProfit >= 0 ? 'bg-blue-50' : 'bg-orange-50' }} rounded-lg flex items-center justify-center shrink-0">
<svg class="w-5 h-5 {{ $netProfit >= 0 ? 'text-blue-600' : 'text-orange-600' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
</svg>
</div>
<div class="min-w-0">
<p class="text-xs text-gray-500">{{ __('صافي الربح/الخسارة') }}</p>
<p class="text-xl font-bold {{ $netProfit >= 0 ? 'text-blue-700' : 'text-orange-700' }} truncate" dir="ltr">
{{ $netProfit >= 0 ? '+' : '' }}{{ number_format($netProfit / 100, 2) }} <span class="text-sm font-normal">{{ __('ج.م') }}</span>
</p>
</div>
</div>
</div>
{{-- Collection Rate --}}
<div class="bg-white rounded-xl shadow-sm border border-purple-200 p-5">
<div class="flex items-center gap-3">
<div class="w-11 h-11 bg-purple-50 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-5 h-5 text-purple-600" 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 2m-6 9l2 2 4-4"/>
</svg>
</div>
<div class="min-w-0">
<p class="text-xs text-gray-500">{{ __('نسبة التحصيل') }}</p>
<p class="text-xl font-bold text-purple-700" dir="ltr">{{ $collectionRate['rate'] }}%</p>
</div>
</div>
@if($collectionRate['overdue_count'] > 0)
<p class="text-xs text-red-500 mt-2">
{{ $collectionRate['overdue_count'] }} {{ __('فاتورة متأخرة') }} ({{ number_format($collectionRate['overdue_amount'] / 100, 0) }} {{ __('ج.م') }})
</p>
@endif
</div>
</div>
{{-- Row 2: Break-Even + Expense Breakdown --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
{{-- Break-Even Gauge --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-base font-semibold text-gray-800 mb-4">{{ __('نقطة التعادل') }}</h3>
@if($breakEven['has_data'])
<div class="relative">
{{-- Progress ring visual --}}
<div class="flex items-center justify-center mb-4">
<div class="relative w-40 h-40">
<svg class="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="42" stroke="#e5e7eb" stroke-width="10" fill="none"/>
<circle cx="50" cy="50" r="42"
stroke="{{ $breakEven['is_profitable'] ? '#10b981' : '#f59e0b' }}"
stroke-width="10" fill="none"
stroke-dasharray="{{ $breakEven['percentage'] * 2.64 }} 264"
stroke-linecap="round"/>
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-3xl font-bold {{ $breakEven['is_profitable'] ? 'text-emerald-600' : 'text-amber-600' }}" dir="ltr">{{ $breakEven['percentage'] }}%</span>
<span class="text-xs text-gray-500">{{ $breakEven['is_profitable'] ? __('مربح') : __('نحو التعادل') }}</span>
</div>
</div>
</div>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-500">{{ __('التكاليف الثابتة الشهرية') }}</span>
<span class="font-medium" dir="ltr">{{ number_format($breakEven['fixed_costs'] / 100, 0) }} {{ __('ج.م') }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">{{ __('الإيرادات الحالية') }}</span>
<span class="font-medium text-emerald-600" dir="ltr">{{ number_format($breakEven['current_revenue'] / 100, 0) }} {{ __('ج.م') }}</span>
</div>
@if(!$breakEven['is_profitable'])
<div class="flex justify-between pt-2 border-t border-gray-100">
<span class="text-gray-600 font-medium">{{ __('المتبقي للتعادل') }}</span>
<span class="font-bold text-amber-600" dir="ltr">{{ number_format($breakEven['remaining'] / 100, 0) }} {{ __('ج.م') }}</span>
</div>
@else
<div class="flex justify-between pt-2 border-t border-gray-100">
<span class="text-gray-600 font-medium">{{ __('فائض فوق التعادل') }}</span>
<span class="font-bold text-emerald-600" dir="ltr">+{{ number_format(($breakEven['current_revenue'] - $breakEven['fixed_costs']) / 100, 0) }} {{ __('ج.م') }}</span>
</div>
@endif
</div>
</div>
@else
<div class="text-center py-8">
<svg class="w-12 h-12 mx-auto mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p class="text-gray-500 text-sm">{{ $breakEven['message'] }}</p>
<a href="{{ route('facilities.list') }}" wire:navigate class="inline-block mt-3 text-sm text-blue-600 hover:text-blue-700 font-medium">
{{ __('أضف تكاليف المنشآت') }} &larr;
</a>
</div>
@endif
</div>
{{-- Expense Breakdown --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-base font-semibold text-gray-800 mb-4">{{ __('تفاصيل المصروفات') }}</h3>
@if($expenses['total'] > 0)
<div class="space-y-4">
{{-- Facility Costs --}}
@php $pct = $expenses['total'] > 0 ? round(($expenses['facility_costs'] / $expenses['total']) * 100) : 0; @endphp
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">{{ __('إيجارات المنشآت') }}</span>
<span class="font-medium" dir="ltr">{{ number_format($expenses['facility_costs'] / 100, 0) }} {{ __('ج.م') }} <span class="text-gray-400">({{ $pct }}%)</span></span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2.5">
<div class="bg-amber-500 h-2.5 rounded-full" style="width: {{ $pct }}%"></div>
</div>
</div>
{{-- Purchase Orders --}}
@php $pct = $expenses['total'] > 0 ? round(($expenses['purchase_orders'] / $expenses['total']) * 100) : 0; @endphp
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">{{ __('مشتريات المخزون') }}</span>
<span class="font-medium" dir="ltr">{{ number_format($expenses['purchase_orders'] / 100, 0) }} {{ __('ج.م') }} <span class="text-gray-400">({{ $pct }}%)</span></span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2.5">
<div class="bg-blue-500 h-2.5 rounded-full" style="width: {{ $pct }}%"></div>
</div>
</div>
{{-- Outbound Payments --}}
@php $pct = $expenses['total'] > 0 ? round(($expenses['outbound_payments'] / $expenses['total']) * 100) : 0; @endphp
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">{{ __('مدفوعات أخرى') }}</span>
<span class="font-medium" dir="ltr">{{ number_format($expenses['outbound_payments'] / 100, 0) }} {{ __('ج.م') }} <span class="text-gray-400">({{ $pct }}%)</span></span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2.5">
<div class="bg-purple-500 h-2.5 rounded-full" style="width: {{ $pct }}%"></div>
</div>
</div>
{{-- Total Bar --}}
<div class="pt-3 border-t border-gray-100">
<div class="flex justify-between text-sm">
<span class="font-semibold text-gray-700">{{ __('الإجمالي') }}</span>
<span class="font-bold text-red-600" dir="ltr">{{ number_format($expenses['total'] / 100, 0) }} {{ __('ج.م') }}</span>
</div>
</div>
</div>
@else
<div class="text-center py-8">
<svg class="w-12 h-12 mx-auto mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<p class="text-gray-500 text-sm">{{ __('لا توجد مصروفات مسجلة في هذه الفترة') }}</p>
</div>
@endif
</div>
</div>
{{-- Row 3: Monthly P&L Chart --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
<h3 class="text-base font-semibold text-gray-800 mb-4">{{ __('الأرباح والخسائر — آخر ٦ أشهر') }}</h3>
<div class="overflow-x-auto">
<div class="min-w-[600px]">
{{-- Chart header --}}
<div class="flex items-center gap-4 mb-4 text-xs">
<span class="flex items-center gap-1"><span class="w-3 h-3 bg-emerald-500 rounded-sm"></span> {{ __('الإيرادات') }}</span>
<span class="flex items-center gap-1"><span class="w-3 h-3 bg-red-400 rounded-sm"></span> {{ __('المصروفات') }}</span>
<span class="flex items-center gap-1"><span class="w-3 h-3 bg-blue-500 rounded-sm"></span> {{ __('صافي الربح') }}</span>
</div>
{{-- Bar chart --}}
@php
$maxVal = max(1, collect($monthlyPL)->max('income'), collect($monthlyPL)->max('expenses'));
@endphp
<div class="grid grid-cols-6 gap-3">
@foreach($monthlyPL as $month)
<div class="text-center">
{{-- Bars --}}
<div class="relative h-40 flex items-end justify-center gap-1 mb-2">
<div class="w-5 bg-emerald-400 rounded-t transition-all" style="height: {{ ($month['income'] / $maxVal) * 100 }}%" title="{{ number_format($month['income'] / 100, 0) }} {{ __('ج.م') }}"></div>
<div class="w-5 bg-red-300 rounded-t transition-all" style="height: {{ ($month['expenses'] / $maxVal) * 100 }}%" title="{{ number_format($month['expenses'] / 100, 0) }} {{ __('ج.م') }}"></div>
</div>
{{-- Label --}}
<p class="text-xs text-gray-500 truncate">{{ $month['label'] }}</p>
{{-- Profit/Loss --}}
<p class="text-xs font-bold mt-1 {{ $month['profit'] >= 0 ? 'text-emerald-600' : 'text-red-600' }}" dir="ltr">
{{ $month['profit'] >= 0 ? '+' : '' }}{{ number_format($month['profit'] / 100, 0) }}
</p>
</div>
@endforeach
</div>
</div>
</div>
</div>
{{-- Row 4: Facility Costs Table + Revenue Sources --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
{{-- Facility Costs Detail --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-semibold text-gray-800">{{ __('تكاليف المنشآت') }}</h3>
<span class="text-sm font-medium text-gray-500" dir="ltr">{{ number_format($facilityCosts['total_monthly'] / 100, 0) }} {{ __('ج.م/شهر') }}</span>
</div>
@if(count($facilityCosts['facilities']) > 0)
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-100">
<th class="pb-2 text-start text-xs font-medium text-gray-500">{{ __('المنشأة') }}</th>
<th class="pb-2 text-start text-xs font-medium text-gray-500">{{ __('الفرع') }}</th>
<th class="pb-2 text-end text-xs font-medium text-gray-500">{{ __('شهري') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
@foreach($facilityCosts['facilities'] as $f)
<tr>
<td class="py-2 text-gray-700">{{ $f['name'] }}</td>
<td class="py-2 text-gray-500">{{ $f['branch'] }}</td>
<td class="py-2 text-end font-medium" dir="ltr">{{ number_format($f['monthly_cost'] / 100, 0) }} {{ __('ج.م') }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr class="border-t border-gray-200">
<td colspan="2" class="pt-3 font-semibold text-gray-700">{{ __('الإجمالي') }}</td>
<td class="pt-3 text-end font-bold text-red-600" dir="ltr">{{ number_format($facilityCosts['total_monthly'] / 100, 0) }} {{ __('ج.م') }}</td>
</tr>
<tr>
<td colspan="2" class="pt-1 text-xs text-gray-400">{{ __('سنوياً') }}</td>
<td class="pt-1 text-end text-xs text-gray-500" dir="ltr">{{ number_format($facilityCosts['total_yearly'] / 100, 0) }} {{ __('ج.م') }}</td>
</tr>
</tfoot>
</table>
</div>
@else
<div class="text-center py-6">
<p class="text-sm text-gray-500">{{ __('لم يتم تحديد تكاليف شهرية لأي منشأة') }}</p>
<a href="{{ route('facilities.list') }}" wire:navigate class="inline-block mt-2 text-sm text-blue-600 hover:text-blue-700 font-medium">{{ __('تعديل المنشآت') }}</a>
</div>
@endif
</div>
{{-- Revenue by Source --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-base font-semibold text-gray-800 mb-4">{{ __('مصادر الإيرادات') }}</h3>
@if(count($revenueBySource) > 0)
<div class="space-y-3">
@php $maxSource = max(1, collect($revenueBySource)->max('total')); @endphp
@foreach($revenueBySource as $source)
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">{{ $source['source'] }}</span>
<span class="font-medium" dir="ltr">{{ number_format($source['total'] / 100, 0) }} {{ __('ج.م') }}</span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2">
<div class="bg-emerald-500 h-2 rounded-full" style="width: {{ ($source['total'] / $maxSource) * 100 }}%"></div>
</div>
</div>
@endforeach
</div>
@else
{{-- Fallback: show payment method breakdown --}}
@if(count($revenue['by_method']) > 0)
<p class="text-xs text-gray-400 mb-3">{{ __('تفصيل حسب طريقة الدفع') }}</p>
<div class="space-y-3">
@php
$methodLabels = ['cash' => 'نقدي', 'card' => 'بطاقة', 'bank_transfer' => 'تحويل بنكي', 'wallet' => 'محفظة', 'online' => 'أونلاين', 'cheque' => 'شيك', 'other' => 'أخرى'];
$maxMethod = max(1, collect($revenue['by_method'])->max('total'));
@endphp
@foreach($revenue['by_method'] as $pm)
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-600">{{ $methodLabels[$pm['method']] ?? $pm['method'] }}</span>
<span class="font-medium" dir="ltr">{{ number_format($pm['total'] / 100, 0) }} {{ __('ج.م') }} <span class="text-gray-400">({{ $pm['count'] }})</span></span>
</div>
<div class="w-full bg-gray-100 rounded-full h-2">
<div class="bg-blue-500 h-2 rounded-full" style="width: {{ ($pm['total'] / $maxMethod) * 100 }}%"></div>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-6">
<p class="text-sm text-gray-500">{{ __('لا توجد إيرادات مسجلة في هذه الفترة') }}</p>
</div>
@endif
@endif
</div>
</div>
{{-- Row 5: Collection Rate Progress --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h3 class="text-base font-semibold text-gray-800 mb-4">{{ __('معدل تحصيل الفواتير') }}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center">
<p class="text-sm text-gray-500 mb-1">{{ __('إجمالي الفوترة') }}</p>
<p class="text-2xl font-bold text-gray-800" dir="ltr">{{ number_format($collectionRate['invoiced'] / 100, 0) }} <span class="text-sm font-normal text-gray-500">{{ __('ج.م') }}</span></p>
</div>
<div class="text-center">
<p class="text-sm text-gray-500 mb-1">{{ __('تم تحصيله') }}</p>
<p class="text-2xl font-bold text-emerald-600" dir="ltr">{{ number_format($collectionRate['collected'] / 100, 0) }} <span class="text-sm font-normal text-gray-500">{{ __('ج.م') }}</span></p>
</div>
<div class="text-center">
<p class="text-sm text-gray-500 mb-1">{{ __('نسبة التحصيل') }}</p>
<p class="text-2xl font-bold {{ $collectionRate['rate'] >= 80 ? 'text-emerald-600' : ($collectionRate['rate'] >= 50 ? 'text-amber-600' : 'text-red-600') }}" dir="ltr">{{ $collectionRate['rate'] }}%</p>
</div>
</div>
{{-- Progress bar --}}
<div class="mt-4 w-full bg-gray-100 rounded-full h-3">
<div class="h-3 rounded-full transition-all {{ $collectionRate['rate'] >= 80 ? 'bg-emerald-500' : ($collectionRate['rate'] >= 50 ? 'bg-amber-500' : 'bg-red-500') }}" style="width: {{ $collectionRate['rate'] }}%"></div>
</div>
</div>
</div>{{-- end loading overlay --}}
</div>
......@@ -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="mb-6">
<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>
@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">
@foreach($activities as $index => $activity)
<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
<div class="p-8">
<div class="mb-6">
<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>
@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))
<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">
......@@ -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="mb-6">
<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>
@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">
@foreach($facilities as $index => $facility)
<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:
</svg>
</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>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ __('اسم المنشأة') }} <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
<p class="mt-1 text-xs text-red-600">{{ $message }}</p>
@enderror
</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>
<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
class="w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
min="1" placeholder="50">
</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>
<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
<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"/>
</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>
@endif
......
......@@ -19,6 +19,7 @@
use App\Livewire\Evaluations\EvaluationShow;
use App\Livewire\Facilities\FacilityForm;
use App\Livewire\Facilities\FacilityList;
use App\Livewire\Financial\FinancialOverview;
use App\Livewire\Auth\Login;
use App\Livewire\CashSessions\CashSessionList;
use App\Livewire\CashSessions\CashSessionManage;
......@@ -178,6 +179,10 @@
Route::get('/cash-sessions/manage', CashSessionManage::class)->name('cash-sessions.manage')
->middleware('permission:cash_sessions.manage');
// Financial Overview
Route::get('/financial-overview', FinancialOverview::class)->name('financial.overview')
->middleware('permission:invoices.list');
// Invoices
Route::get('/invoices', InvoiceList::class)->name('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